2013-06-27 8 views
6

Ich arbeite an JSF-Projekt mit Spring und Hibernate, die unter anderem eine Reihe von Converter s hat, die nach dem gleichen Muster:Implement Konvertern für Unternehmen mit Java Generics

  • getAsObject die Darstellung String erhält von die Objekt-ID, wandelt es in eine Zahl, und die Entität der gegebenen Art holen und die gegebene ID

  • getAsString empfängt und Einheit und gibt die ID des zu String umgewandelt Objekt

Der Code ist im Wesentlichen, was folgt (Schecks weggelassen):

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

die große Anzahl von Converter s gegeben, die genau wie diese sind (mit Ausnahme der Art von MyService und MyEntity natürlich), war ich frage mich, ob es sich lohnt, einen einzigen generischen Konverter zu verwenden. Die Implementierung der generischen selbst ist nicht schwierig, aber ich bin mir nicht sicher über den richtigen Ansatz, um die Bohnen zu deklarieren.

Eine mögliche Lösung ist die folgende:

1 - die generische Implementierung schreiben, machen wir es MyGenericConverter nennen, ohne Bean Anmerkung

2 - Schreiben Sie den spezifischen Konverter Anzeige eine Unterklasse von MyGenericConverter<T> und mit Anmerkungen versehen als benötigt:

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

Während ich dies schreibe ich erkannte, dass vielleicht ein Allgemein nicht wirklich nötig ist, so vielleicht nur eine Basisklasse mit der Umsetzung der beiden Methoden schreiben, ich könnte ein d Unterklasse nach Bedarf.

Dort ein paar nicht triviale Details, die beachtet werden müssen (wie die Tatsache, dass ich die MyService Klasse in irgendeiner Weise abstrahieren müsste), so ist meine erste Frage: ist es den Aufwand wert?

Und wenn ja, gibt es andere Ansätze?

Antwort

15

Einfachste wäre all Ihre JPA-Entitäten von einer Basiseinheit wie folgt verlängern lassen:

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

} 

Beachten Sie, dass es wichtig ist eine korrekte equals() (und hashCode()) haben, sonst werden Sie Validation Error: Value is not valid Gesicht. Die Class#isAssignableFrom() Tests sollen verhindern, dass Tests auf z.B. Hibernate-basierte Proxies, ohne auf die Hibernate-spezifische Helper-Methode Hibernate#getClass(Object) zurückgreifen zu müssen.

Und haben eine Basis-Service wie folgt aus (ja, ich ignoriere die Tatsache, dass Sie Frühling verwenden, es ist nur die Grundidee zu geben):

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

} 

und dem Konverter implementieren wie folgt:

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

} 

Beachten sie, dass es als @ManagedBean anstelle eines @FacesConverter registriert ist. Mit diesem Trick können Sie einen Dienst im Konverter über z.@EJB. Siehe auch How to inject @EJB, @PersistenceContext, @Inject, @Autowired, etc in @FacesConverter? So müssen Sie es als converter="#{baseEntityConverter}" anstelle von converter="baseEntityConverter" verweisen.

Wenn Sie UISelectOne/UISelectMany Komponenten (<h:selectOneMenu> und Freunde) passieren mehr einen solchen Konverter zu verwenden, als häufig, können Sie OmniFacesSelectItemsConverter viel nützlicher zu finden. Es konvertiert basierend auf den Werten, die in <f:selectItems> verfügbar sind, anstatt (potenziell teure) DB-Aufrufe jedes Mal zu machen.

+1

Mit der „id“ Eigenschaft zu finden ist Nicht empfohlen in equals und hashCode-Methoden: Sie empfehlen die Implementierung von equals() und hashCode() mithilfe der Geschäftsschlüsselgleichheit: https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes -equalshashcode.html Und was ist mit zusammengesetzten IDs? –

0

Hier ist meine Lösung mit diesen Überlegungen:

  • Ich gehe davon aus Sie in JPA interessiert sind (nicht Hibernate)
  • Meine Lösung ist nicht erforderlich jede Klasse zu erweitert und soll für jede JPA Einheit arbeiten Bean, es ist nur eine einfache Klasse, die Sie verwenden, noch erfordert es die Implementierung eines Dienstes oder DAO. Die einzige Voraussetzung ist, dass der Konverter direkt von der JPA-Bibliothek abhängt, die nicht sehr elegant sein kann.
  • Es verwendet Hilfsmethoden zum Serialisieren/Deserialisieren der ID der Bean. Es konvertiert nur die ID der Entity-Bean und fügt die Zeichenfolge mit dem Klassennamen und der ID serialisiert und in base64 konvertiert hinzu. Dies ist möglich, da in jpa die IDs der Entitäten serialisierbar implementieren müssen. Die Umsetzung dieser Methoden ist in Java 1.7, aber man könnte eine andere Implementierungen für Java < 1.7 dort
 
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(); 
     } 
    } 

} 

Verwenden Sie es wie ein zweiter Wandler mit

<h:selectOneMenu converter="#{entityConverter}" ...