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

8
我正在使用Spring和Hibernate进行JSF项目开发,其中有一些遵循相同模式的转换器Converter
  • getAsObject接收对象ID的字符串表示形式,将其转换为数字,并获取给定种类和ID的实体

  • getAsString接收实体并返回将对象ID转换为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(当然,除了MyServiceMyEntity的类型),我在思考是否值得使用单个通用转换器。 通用实现本身并不困难,但我不确定正确的声明Bean的方法。
一种可能的解决方案是:
1-编写通用实现,我们称之为MyGenericConverter,没有任何Bean注释
2-将特定的转换器编写为MyGenericConverter<T>的子类,并根据需要进行注释:
@ManagedBean(name="myFooConverter")
@SessionScoped
public class MyFooConverter implements MyGenericConverter<Foo> {
    /* ... */
}

在写这篇文章的时候,我意识到可能并不真正需要泛型,因此也许我可以只编写一个基类,并实现这两种方法,根据需要进行子类化。
有一些非平凡的细节需要处理(例如,我必须以某种方式抽象出“MyService”类),因此我的第一个问题是:值得吗?
如果值得,是否有其他方法?
3个回答

17
最简单的方法是让您所有的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在转换器中注入服务。另请参见如何在@FacesConverter中注入@EJB、@PersistenceContext、@Inject、@Autowired等?因此,您需要将其引用为converter="#{baseEntityConverter}"而不是converter="baseEntityConverter"
如果您经常使用这样的转换器用于 UISelectOne/UISelectMany 组件(<h:selectOneMenu> 和类似组件),您可能会发现 OmniFacesSelectItemsConverter 更加实用。它基于 <f:selectItems> 中提供的值进行转换,而不是每次都进行(潜在昂贵的)数据库调用。

2
不建议在equals和hashCode方法中使用"id"属性。他们建议使用业务键相等性实现equals()和hashCode(): https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes-equalshashcode.html 那么,复合ID(composite ids)呢? - Balaban Mario
没错。但这些不属于BaseEntity。你可以在特定的实体子类中重写equals/hashCode。将所有可能情况直接放入BaseEntity中是不可行且难看的。 - BalusC

1

您的实体不需要继承自BaseEntity,因为EntityManagerFactory包含所有必要的(元)信息。您还可以重用JSF Converters来转换/解析ID。

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

0

这是我的解决方案,考虑到以下几点:

  • 我假设您对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;
/** * JPA实体的通用JSF转换器 * * 将JPA实例转换为字符串形式:@ 通过在数据库中搜索ID从字符串转换为实例 * * 这是可能的,因为JPA要求所有实体ID都实现可序列化 * * 要求: - 您必须提供名称为“entityManagerFactory”的实例以进行注入 - 记得在所有实体类中实现equals和hashCode !! * */ @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实体 " + clazz, e); } catch (IOException e) { throw new ConverterException("无法反序列化JPA类的ID " + 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("无法为类 " + clazz + " 序列化ID", e); }
return clazz + CHARACTER_SEPARATOR + idBase64String; }
// 实用方法(可以通过将其移动到另一个位置来重构)
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(); } }
}

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接