使用 Java 泛型为实体实现转换器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/17343032/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-01 01:44:34  来源:igfitidea点击:

Implement converters for entities with Java Generics

javaspringjsfgenericsconverter

提问by Marco Righele

I'm working on JSF project with Spring and Hibernate which among other things has a number of Converters that follow the same pattern:

我正在使用 Spring 和 Hibernate 处理 JSF 项目,其中有许多Converter遵循相同模式的s:

  • getAsObjectreceives the string representation of the object id, converts it to a number, and fetch the entity of the given kind and the given id

  • getAsStringreceives and entity and returns the id of the object converted to String

  • getAsObject接收对象 id 的字符串表示,将其转换为数字,并获取给定种类和给定 id 的实体

  • getAsString接收和实体并返回转换为的对象的id String

The code is essentially what follows (checks omitted):

代码基本上如下(省略检查):

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

Given the large number of Converters that are exactly like this (except for the type of MyServiceand MyEntityof course), I was wondering if it was worth using a single generic converter. The implementation of the generic by itself is not difficult, but I'm not sure about the right approach to declare the Beans.

考虑到大量Converter与此完全相同的s(当然除了MyServiceand的类型MyEntity),我想知道是否值得使用单个通用转换器。泛型本身的实现并不困难,但我不确定声明 Bean 的正确方法。

A possible solution is the following:

一个可能的解决方案如下:

1 - Write the generic implementation, let's call it MyGenericConverter, without any Bean annotation

1 - 编写通用实现,我们称之为MyGenericConverter,没有任何 Bean 注释

2 - Write the specific converter ad a subclass of MyGenericConverter<T>and annotate it as needed:

2 - 编写特定转换器广告的子类MyGenericConverter<T>并根据需要对其进行注释:

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

While writing this I realized that maybe a Generic is not really needed, so maybe I could simply write a base class with the implementation of the two methods, and subclass as needed.

在写这篇文章时,我意识到可能并不真正需要 Generic,所以也许我可以简单地编写一个实现这两个方法的基类,并根据需要编写子类。

There a few non trivial details that have to be taken care of (like the fact that I'd have to abstract the MyServiceclass in some way) so my first question is : is it worth the hassle ?

有一些重要的细节需要处理(比如我必须以MyService某种方式抽象类的事实)所以我的第一个问题是:值得麻烦吗?

And if so, are there other approaches ?

如果是这样,还有其他方法吗?

回答by BalusC

Easiest would be to let all your JPA entities extend from a base entity like this:

最简单的方法是让所有 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());
    }

}

Note that it's important to have a proper equals()(and hashCode()), otherwise you will face Validation Error: Value is not valid. The Class#isAssignableFrom()tests are to avoid failing tests on e.g. Hibernate based proxies without the need to fall back to Hibernate-specific Hibernate#getClass(Object)helper method.

请注意,拥有正确的equals()(和hashCode())很重要,否则您将面临Validation Error: Value is not valid。这些Class#isAssignableFrom()测试是为了避免在例如基于 Hibernate 的代理上的测试失败,而无需退回到 Hibernate 特定的Hibernate#getClass(Object)辅助方法。

And have a base service like this (yes, I'm ignoring the fact that you're using Spring; it's just to give the base idea):

并拥有这样的基本服务(是的,我忽略了您正在使用 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);
    }

}

And implement the converter as follows:

并按如下方式实现转换器:

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

}

Note that it's registered as a @ManagedBeaninstead of a @FacesConverter. This trick allows you to inject a service in the converter via e.g. @EJB. See also How to inject @EJB, @PersistenceContext, @Inject, @Autowired, etc in @FacesConverter?So you need to reference it as converter="#{baseEntityConverter}"instead of converter="baseEntityConverter".

请注意,它注册为 a@ManagedBean而不是@FacesConverter。这个技巧允许您通过例如在转换器中注入服务@EJB。另请参阅如何在@FacesConverter 中注入@EJB、@PersistenceContext、@Inject、@Autowired 等?所以你需要将它引用为converter="#{baseEntityConverter}"而不是converter="baseEntityConverter".

If you happen to use such a converter more than often for UISelectOne/UISelectManycomponents (<h:selectOneMenu>and friends), you may find OmniFacesSelectItemsConvertermuch more useful. It converts based on the values available in <f:selectItems>instead of making (potentially expensive) DB calls everytime.

如果您碰巧经常将这样的转换器用于UISelectOne/UISelectMany组件(<h:selectOneMenu>和朋友),您可能会发现OmniFacesSelectItemsConverter更有用。它根据可用的值进行转换,<f:selectItems>而不是每次都进行(可能很昂贵的)数据库调用。

回答by Jesus M C

Here is my solution with this considerations:

这是我考虑到这一点的解决方案:

  • I asume you are interested in JPA (not Hibernate)
  • My solution does not require to extends any classand should work for any JPA entity bean, it's just only a simple class you use, nor does it require implementing any service or DAO. The only requirement is that the converter directly depends on the JPA library which may be not very elegant.
  • It uses auxiliary methods for serializing/deserializing the id of the bean. It only converts the id of the entity bean and compounds the string with the classname and the id serialized and converted to base64. This is possible due to the fact that in jpa the ids of the entities mustimplement serializable. The implementation of this methods is in java 1.7, but you could find another implementations for java < 1.7 over there
  • 我假设您对 JPA(不是 Hibernate)感兴趣
  • 我的解决方案不需要扩展任何类并且应该适用于任何 JPA 实体 bean,它只是您使用的一个简单类,也不需要实现任何服务或 DAO。唯一的要求是转换器直接依赖于 JPA 库,这可能不是很优雅。
  • 它使用辅助方法来序列化/反序列化 bean 的 id。它只转换实体 bean 的 id 并将字符串与类名和序列化并转换为 base64 的 id 复合。这是可能的,因为在 jpa 中实体的 id必须实现可序列化。此方法的实现在 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();
        }
    }

}

Use it like another converter with

像另一个转换器一样使用它

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