Implementar convertidores para entidades con Java Generics

Estoy trabajando en el proyecto JSF con Spring e Hibernate que, entre otras cosas, tiene una serie de Converter que siguen el mismo patrón:

El código es esencialmente lo que sigue (verificaciones omitidas):

 @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(); } } 

Dado el gran número de Converter que son exactamente así (excepto por el tipo de MyService y MyEntity por supuesto), me preguntaba si valía la pena usar un único convertidor genérico. La implementación del genérico en sí misma no es difícil, pero no estoy seguro del enfoque correcto para declarar los frijoles.

Una posible solución es la siguiente:

1 – Escribe la implementación genérica, llamémosla MyGenericConverter , sin ninguna anotación de Bean

2 – Escriba el convertidor específico y una subclase de MyGenericConverter y MyGenericConverter según sea necesario:

 @ManagedBean(name="myFooConverter") @SessionScoped public class MyFooConverter implements MyGenericConverter { /* ... */ } 

Mientras escribía esto, me di cuenta de que tal vez no se necesita realmente un genérico, así que tal vez podría simplemente escribir una clase base con la implementación de los dos métodos, y la subclase según sea necesario.

Hay algunos detalles no triviales que hay que tener en cuenta (como el hecho de que tendría que abstraer la clase MyService de alguna manera) así que mi primera pregunta es: ¿vale la pena la molestia?

Y si es así, ¿hay otros enfoques?

Lo más fácil sería permitir que todas sus entidades JPA se extiendan desde una entidad base como esta:

 public abstract class BaseEntity 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()); } } 

Tenga en cuenta que es importante tener un equals() (y hashCode() ) hashCode() , de lo contrario se enfrentará a un Error de validación: El valor no es válido . Las pruebas de Class#isAssignableFrom() son para evitar fallar las pruebas, por ejemplo, en proxies basados ​​en Hibernate sin la necesidad de recurrir al método de ayuda Hibernate#getClass(Object) específico de Hibernate#getClass(Object) .

Y tenga un servicio básico como este (sí, estoy ignorando el hecho de que está usando Spring; es solo para dar la idea base):

 @Stateless public class BaseService { @PersistenceContext private EntityManager em; public BaseEntity< ? extends Number> find(Class> type, Number id) { return em.find(type, id); } } 

E implemente el convertidor de la siguiente manera:

 @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>) type, Long.valueOf(submittedValue)); } catch (NumberFormatException e) { throw new ConverterException(new FacesMessage(String.format("%s is not a valid ID of BaseEntity", submittedValue)), e); } } } 

Tenga en cuenta que está registrado como @ManagedBean lugar de @FacesConverter . Este truco le permite inyectar un servicio en el convertidor a través de, por ejemplo, @EJB . Consulte también Cómo inyectar @EJB, @PersistenceContext, @Inject, @Autowired, etc. en @FacesConverter? Por lo tanto, debe referenciarlo como converter="#{baseEntityConverter}" lugar de converter="baseEntityConverter" .

Si usa un convertidor de este tipo más a menudo para UISelectOne / UISelectMany components ( y friends), puede encontrar que OmniFaces SelectItemsConverter mucho más útil. Se convierte en función de los valores disponibles en lugar de realizar llamadas DB (potencialmente costosas) cada vez.

Aquí está mi solución con estas consideraciones:

  • Supongo que estás interesado en JPA (no Hibernate)
  • Mi solución no requiere ampliar ninguna clase y debería funcionar para cualquier bean de entidad JPA, es solo una clase simple que utiliza, ni requiere la implementación de ningún servicio o DAO . El único requisito es que el convertidor dependa directamente de la biblioteca JPA, que puede no ser muy elegante.
  • Utiliza métodos auxiliares para serializar / deserializar la identificación del bean. Solo convierte el ID de la entidad Bean y compone la cadena con el nombre de clase y la ID serializados y convertidos a base64. Esto es posible debido al hecho de que en jpa los identificadores de las entidades deben implementarse serializables. La implementación de este método está en Java 1.7, pero podrías encontrar otras implementaciones para Java <1.7 allí
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 importar 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;

 / **
  * Conversor genérico de entidades jpa para jsf
  * 
  * Convierte las instancias jpa en cadenas con este formulario: @ Convierte de cadenas en instancias que buscan por id en
  * base de datos
  * 
  * Es posible gracias al hecho de que jpa requiere que todos los identificadores de entidad
  * implementar serializable
  * 
  * Requiere: - Debe proporcionar instancia con el nombre "entityManagerFactory" para ser
  * inyectado - Recuerde implementar equals y hashCode en toda su entidad
  * clases !!
  * 
  * /
 @ManagedBean
 @RequestScoped
 la clase pública EntityConverter implementa Converter {

     char final privado estático CHARACTER_SEPARATOR = '@';

     @ManagedProperty (value = "# {entityManagerFactory}")
     private EntityManagerFactory entityManagerFactory;

     public void setEntityManagerFactory (EntityManagerFactory entityManagerFactory) {
         this.entityManagerFactory = entityManagerFactory;
     }

     private static final String empty = "";

     @Anular
     Objeto público getAsObject (contexto FacesContext, UIComponent c, valor de cadena) {
         if (value == null || value.isEmpty ()) {
             devolver nulo;
         }

         int index = value.indexOf (CHARACTER_SEPARATOR);
         String clazz = value.substring (0, índice);
         Cadena idBase64String = value.substring (índice + 1, valor.length ());
 EntityManager entityManager = null;
         tratar {
             Clase entityClazz = Class.forName (clazz);
             ID de objeto = convertFromBase64String (idBase64String);

         entityManager = entityManagerFactory.createEntityManager ();
         Object object = entityManager.find (entityClazz, id);

             objeto de retorno;

         } catch (ClassNotFoundException e) {
             lanzar nueva ConverterException ("entidad Jpa no encontrada" + clazz, e);
         } catch (IOException e) {
             lanzar una nueva ConverterException ("Could not deserialize id of jpa class" + clazz, e);
         }finalmente{
         if (entityManager! = null) {
             entityManager.close ();  
         }
     }

     }

     @Anular
     public String getAsString (FacesContext context, UIComponent c, Object value) {
         if (value == null) {
             regresar vacío;
         }
         String clazz = value.getClass (). GetName ();
         Cadena idBase64String;
         tratar {
             idBase64String = convertToBase64String (entityManagerFactory.getPersistenceUnitUtil (). getIdentifier (value));
         } catch (IOException e) {
             lanzar una nueva ConverterException ("No se pudo serializar la identificación para la clase" + clazz, e);
         }

         return clazz + CHARACTER_SEPARATOR + idBase64String;
     }

     // MÉTODOS DE UTILIDAD, (Podría ser refactorizado moviéndolo a otro lugar)

     public static String convertToBase64String (Object o) throws IOException {
         devuelve javax.xml.bind.DatatypeConverter.printBase64Binary (convertToBytes (o));
     }

     public static Object convertFromBase64String (String str) arroja IOException, ClassNotFoundException {
         devuelve convertFromBytes (javax.xml.bind.DatatypeConverter.parseBase64Binary (str));
     }

     byte estático público [] convertToBytes (objeto Object) arroja IOException {
         try (ByteArrayOutputStream bos = new ByteArrayOutputStream (); ObjectOutput out = new ObjectOutputStream (bos)) {
             out.writeObject (objeto);
             return bos.toByteArray ();
         }
     }

     public static Objeto convertFromBytes (byte [] bytes) throws IOException, ClassNotFoundException {
         try (ByteArrayInputStream bis = new ByteArrayInputStream (bytes); ObjectInput in = new ObjectInputStream (bis)) {
             return in.readObject ();
         }
     }

 }

Úselo como otro convertidor con