Реализуйте конвертеры для сущностей с помощью 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}" ...
Другие вопросы по тегам