Реализуйте конвертеры для сущностей с помощью Java Generics
Я работаю над проектом JSF со Spring и Hibernate, который, помимо прочего, имеет ряд Converter
которые следуют той же схеме:
getAsObject
получает строковое представление идентификатора объекта, преобразует его в число и извлекает сущность заданного вида и заданный идентификатор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
конечно), мне было интересно, стоило ли использовать один универсальный конвертер. Само по себе применение дженерика несложно, но я не уверен в правильном подходе к объявлению Бинов.
Возможное решение заключается в следующем:
1 - Напишите обобщенную реализацию, давайте назовем ее MyGenericConverter
без каких-либо аннотаций Бина
2 - Написать конкретный конвертер объявления подкласса MyGenericConverter<T>
и аннотируйте его по мере необходимости:
@ManagedBean(name="myFooConverter")
@SessionScoped
public class MyFooConverter implements MyGenericConverter<Foo> {
/* ... */
}
При написании этого я понял, что, возможно, Generic на самом деле не нужен, поэтому, возможно, я мог бы просто написать базовый класс с реализацией двух методов и подкласс по мере необходимости.
Есть несколько нетривиальных деталей, о которых нужно позаботиться (например, тот факт, что я должен MyService
класс в некотором роде) поэтому мой первый вопрос: стоит ли хлопот?
И если да, есть ли другие подходы?
3 ответа
Проще всего было бы позволить всем вашим сущностям 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()
тесты должны избегать неудачных тестов, например, на прокси-серверах на основе Hibernate, без необходимости использовать Hibernate-специфичные 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's 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>
вместо того, чтобы делать (потенциально дорогие) вызовы БД каждый раз.
Вашим объектам не нужно наследовать от
BaseEntity
как
EntityManagerFactory
содержит всю необходимую (мета) информацию. Вы также можете повторно использовать JSF
Converters
для преобразования / анализа идентификатора.
@FacesConverter(value = "entityConverter", managed = true)
public class EntityConverter implements Converter<Object> {
@Inject
private EntityManager entityManager;
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
Class<?> entityType = component.getValueExpression("value").getType(context.getELContext());
Class<?> idType = entityManager.getMetamodel().entity(entityType).getIdType().getJavaType();
Converter idConverter = context.getApplication().createConverter(idType);
Object id = idConverter.getAsObject(context, component, value);
return entityManager.getReference(entityType, id);
}
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
Object id = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(value);
Converter idConverter = context.getApplication().createConverter(id.getClass());
return idConverter.getAsString(context, component, id);
}
}
Вот мое решение с этими соображениями:
- Я полагаю, что вы заинтересованы в JPA (не Hibernate)
- Мое решение не требует расширения какого-либо класса и должно работать с любым компонентом сущности JPA, это всего лишь простой класс, который вы используете, и не требует реализации какой-либо службы или DAO. Единственное требование заключается в том, что конвертер напрямую зависит от библиотеки JPA, которая может быть не очень элегантной.
- Он использует вспомогательные методы для сериализации / десериализации идентификатора компонента. Он только преобразует идентификатор объекта управления данными и соединяет строку с именем класса, а идентификатор сериализуется и преобразуется в base64. Это возможно благодаря тому, что в jpa идентификаторы сущностей должны реализовываться сериализуемо. Реализация этих методов в Java 1.7, но вы можете найти другие реализации для Java < 1.7 там
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; импорт 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; /** * Универсальный конвертер сущностей jpa для jsf * * Преобразует экземпляры jpa в строки с такой формой: @ Преобразует из строк в поиск экземпляров по идентификатору в * database * * Это возможно благодаря тому, что jpa требует всех идентификаторов сущностей * для реализации serializable * * Требуется: - Вы должны предоставить экземпляр с именем "entityManagerFactory" для * инъекции - Не забудьте реализовать equals и hashCode во всех ваших классах entity *! * */ @ManagedBean @RequestScoped открытый класс EntityConverter реализует конвертер { 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, UIComponent c, значение String) { 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); возвращаемый объект; } catch (ClassNotFoundException e) { throw new ConverterException("сущность Jpa не найдена" + clazz, e); } catch (IOException e) { throw new ConverterException("Не удалось десериализовать идентификатор класса jpa" + clazz, e); }finally{ if(entityManager!=null){ entityManager.close(); } } } @Override public String getAsString(контекст FacesContext, UIComponent c, значение объекта) { 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("Не удалось сериализовать идентификатор для класса" + clazz, e); } return clazz + CHARACTER_SEPARATOR + idBase64String; } // UTILITY METHODS, (может быть реорганизован при перемещении его в другое место) public static String convertToBase64String(Object o) throws IOException { return javax.xml.bind.DatatypeConverter.printBase64Binary(convertToBytes(o)); } открытый статический объект convertFromBase64String(String str) генерирует IOException, ClassNotFoundException { return convertFromBytes(javax.xml.bind.DatatypeConverter.parseBase64Binary(str)); } public static byte[] convertToBytes(Object object) выдает IOException { try (ByteArrayOutputStream bos = новый ByteArrayOutputStream(); ObjectOutput out = new ObjectOutputStream(bos)) { out.writeObject(object); return bos.toByteArray(); } } открытый статический объект convertFromBytes(byte[] bytes) генерирует IOException, ClassNotFoundException { try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes); ObjectInput in = new ObjectInputStream(bis)) { return in.readObject(); } } }
Используйте его как другой конвертер с
<h:selectOneMenu converter="#{entityConverter}" ...