Внедрение преобразователей для объектов с Java Generics
Я работаю над проектом JSF с Spring и Hibernate, который среди прочего имеет несколько Converter
, которые следуют одному и тому же шаблону:
-
getAsObject
получает строковое представление идентификатора объекта, преобразует его в число и извлекает объект данного вида и указанный id
-
getAsString
принимает и сущность и возвращает идентификатор объекта, преобразованного в String
Код по существу следующий (проверки опущены):
@ManagedBean(name="myConverter")
@SessionScoped
public class MyConverter implements Converter {
private MyService myService;
/* ... */
@Override
public Object getAsObject(FacesContext facesContext, UIComponent uiComponent, String value) {
int id = Integer.parseInt(value);
return myService.getById(id);
}
@Override
public String getAsString(FacesContext facesContext, UIComponent uiComponent, Object value) {
return ((MyEntity)value).getId().toString();
}
}
Учитывая большое количество Converter
, которые точно такие же (за исключением типа MyService
и MyEntity
, конечно), мне было интересно, стоит ли использовать один общий конвертер.
Внедрение родового само по себе не сложно, но я не уверен в правильном подходе к объявлению Beans.
Возможное решение состоит в следующем:
1 - Напишите общую реализацию, позвоните ей MyGenericConverter
, без аннотации Bean
2 - Напишите конкретный конвертер объявлений подкласса MyGenericConverter<T>
и аннотируйте его по мере необходимости:
@ManagedBean(name="myFooConverter")
@SessionScoped
public class MyFooConverter implements MyGenericConverter<Foo> {
/* ... */
}
При написании этого я понял, что, возможно, Generic действительно не нужен, поэтому, возможно, я мог бы просто написать базовый класс с реализацией двух методов и подкласса по мере необходимости.
Есть несколько нетривиальных деталей, о которых нужно позаботиться (например, тот факт, что мне пришлось бы каким-то образом отделить класс MyService
), поэтому мой первый вопрос: стоит ли хлопот?
И если да, есть ли другие подходы?
Ответы
Ответ 1
Проще всего было бы позволить всем вашим объектам JPA проходить от базового объекта следующим образом:
public abstract class BaseEntity<T extends Number> implements Serializable {
private static final long serialVersionUID = 1L;
public abstract T getId();
public abstract void setId(T id);
@Override
public int hashCode() {
return (getId() != null)
? (getClass().getSimpleName().hashCode() + getId().hashCode())
: super.hashCode();
}
@Override
public boolean equals(Object other) {
return (other != null && getId() != null
&& other.getClass().isAssignableFrom(getClass())
&& getClass().isAssignableFrom(other.getClass()))
? getId().equals(((BaseEntity<?>) other).getId())
: (other == this);
}
@Override
public String toString() {
return String.format("%s[id=%d]", getClass().getSimpleName(), getId());
}
}
Обратите внимание, что важно иметь правильный equals()
(и hashCode()
), иначе вы столкнетесь с Ошибка проверки: значение недействительно. Тесты Class#isAssignableFrom()
состоят в том, чтобы избежать неудачных тестов, например. Прокси-серверы на спящем режиме без необходимости возвращаться к hperernate-специфическому Hibernate#getClass(Object)
вспомогательному методу.
И у меня есть базовая услуга (да, я игнорирую тот факт, что вы используете Spring, просто чтобы дать базовую идею):
@Stateless
public class BaseService {
@PersistenceContext
private EntityManager em;
public BaseEntity<? extends Number> find(Class<BaseEntity<? extends Number>> type, Number id) {
return em.find(type, id);
}
}
И реализуйте преобразователь следующим образом:
@ManagedBean
@ApplicationScoped
@SuppressWarnings({ "rawtypes", "unchecked" }) // We don't care about BaseEntity actual type here.
public class BaseEntityConverter implements Converter {
@EJB
private BaseService baseService;
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
if (value == null) {
return "";
}
if (modelValue instanceof BaseEntity) {
Number id = ((BaseEntity) modelValue).getId();
return (id != null) ? id.toString() : null;
} else {
throw new ConverterException(new FacesMessage(String.format("%s is not a valid User", modelValue)), e);
}
}
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isEmpty()) {
return null;
}
try {
Class<?> type = component.getValueExpression("value").getType(context.getELContext());
return baseService.find((Class<BaseEntity<? extends Number>>) type, Long.valueOf(submittedValue));
} catch (NumberFormatException e) {
throw new ConverterException(new FacesMessage(String.format("%s is not a valid ID of BaseEntity", submittedValue)), e);
}
}
}
Обратите внимание, что он зарегистрирован как @ManagedBean
вместо @FacesConverter
. Этот трюк позволяет вам вводить услугу в преобразователь, например, @EJB
. См. Также Как вставить @EJB, @PersistenceContext, @Inject, @Autowired и т.д. В @FacesConverter? Поэтому вам нужно ссылаться на него как converter="#{baseEntityConverter}"
вместо converter="baseEntityConverter"
.
Если вы используете такой конвертер более чем часто для UISelectOne
/UISelectMany
компоненты (<h:selectOneMenu>
и друзья), вы можете найти OmniFaces SelectItemsConverter
гораздо полезнее. Он преобразуется на основе значений, доступных в <f:selectItems>
, вместо того, чтобы делать (потенциально дорогостоящие) вызовы БД каждый раз.
Ответ 2
Вот мое решение с этими соображениями:
- Я предполагаю, что вас интересует JPA (не спящий режим).
- Мое решение не требует расширения любого класса и должно работать для любого объекта JPA bean, это всего лишь простой класс, который вы используете, , а также не требует внедрения какой-либо службы или DAO. Единственное требование - конвертер напрямую зависит от библиотеки JPA, которая может быть не очень элегантной.
- Он использует вспомогательные методы для сериализации/десериализации идентификатора bean. Он преобразует только идентификатор объекта bean и связывает строку с именем класса и идентификатором, сериализованным и преобразованным в base64. Это возможно из-за того, что в jpa идентификаторы объектов должны реализовывать сериализуемое. Реализация этих методов в java 1.7, но вы можете найти другие реализации для java < 1.7 там
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.persistence.EntityManagerFactory;
/**
* Generic converter of jpa entities for jsf
*
* Converts the jpa instances to strings with this form: @ Converts from strings to instances searching by id in
* database
*
* It is possible thanks to the fact that jpa requires all entity ids to
* implement serializable
*
* Requires: - You must provide instance with name "entityManagerFactory" to be
* injected - Remember to implement equals and hashCode in all your entity
* classes !!
*
*/
@ManagedBean
@RequestScoped
public class EntityConverter implements Converter {
private static final char CHARACTER_SEPARATOR = '@';
@ManagedProperty(value = "#{entityManagerFactory}")
private EntityManagerFactory entityManagerFactory;
public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
private static final String empty = "";
@Override
public Object getAsObject(FacesContext context, UIComponent c, String value) {
if (value == null || value.isEmpty()) {
return null;
}
int index = value.indexOf(CHARACTER_SEPARATOR);
String clazz = value.substring(0, index);
String idBase64String = value.substring(index + 1, value.length());
EntityManager entityManager=null;
try {
Class entityClazz = Class.forName(clazz);
Object id = convertFromBase64String(idBase64String);
entityManager = entityManagerFactory.createEntityManager();
Object object = entityManager.find(entityClazz, id);
return object;
} catch (ClassNotFoundException e) {
throw new ConverterException("Jpa entity not found " + clazz, e);
} catch (IOException e) {
throw new ConverterException("Could not deserialize id of jpa class " + clazz, e);
}finally{
if(entityManager!=null){
entityManager.close();
}
}
}
@Override
public String getAsString(FacesContext context, UIComponent c, Object value) {
if (value == null) {
return empty;
}
String clazz = value.getClass().getName();
String idBase64String;
try {
idBase64String = convertToBase64String(entityManagerFactory.getPersistenceUnitUtil().getIdentifier(value));
} catch (IOException e) {
throw new ConverterException("Could not serialize id for the class " + clazz, e);
}
return clazz + CHARACTER_SEPARATOR + idBase64String;
}
// UTILITY METHODS, (Could be refactored moving it to another place)
public static String convertToBase64String(Object o) throws IOException {
return javax.xml.bind.DatatypeConverter.printBase64Binary(convertToBytes(o));
}
public static Object convertFromBase64String(String str) throws IOException, ClassNotFoundException {
return convertFromBytes(javax.xml.bind.DatatypeConverter.parseBase64Binary(str));
}
public static byte[] convertToBytes(Object object) throws IOException {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutput out = new ObjectOutputStream(bos)) {
out.writeObject(object);
return bos.toByteArray();
}
}
public static Object convertFromBytes(byte[] bytes) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInput in = new ObjectInputStream(bis)) {
return in.readObject();
}
}
}
Используйте его как другой конвертер с
<h:selectOneMenu converter="#{entityConverter}" ...