要点:通过反射机制得到父类的泛型参数
如果你用过 Seam,那就一定知道 Seam 又对JPA进行了封装,使得我们只需要让实体bean继承 Home<T>类而不需要编写任何代码就能实现CRUD操作。例如我们想持久化Customer实体bean,则只需要编写如下代码:
public class CustomerHome extends Home<Customer> {
}
就可以直接注入 CustomerHome 对象,然后调用
customer.setInstance(new Customer()); // 让此CustomerHome对象管理一个新的Customer实体
customer.persist(); // 持久化
customer.delete(); // 同 entityManager.remove()
customer.save(); // 同 entityManager.merge()
也就是说,只需要把你要进行CRUD操作的 entity bean 继承自Home类,然后就可以无需编写任何代码直接操作 EntityManager 进行持久化操作,从而减少了代码量。现在我们就自己实现一个简化的Home类。
先把最终代码贴出来:
package cn.fh.orm;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
/**
* 对EntityManager操作进行封装.
* <p>只需简单地继承此类,就可以实现CRUD操作
* @author whf
*
* @param <T> 指定此Home类要管理的实体对象
*/
@Repository
public abstract class Home<T> {
/**
* 实体主键
*/
private Integer id;
/**
* 实体对象
*/
private T instance;
/**
* 实体的Class对象
*/
private Class<T> instanceClass;
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = false)
public void persist() {
if (null == instance) {
throw new RuntimeException("未指定实体");
}
em.persist(instance);
}
@Transactional(readOnly = false)
public void update() {
if (null == instance) {
throw new RuntimeException("未指定实体");
}
em.merge(instance);
}
@Transactional(readOnly = false)
public void delete() {
if (null == instance) {
throw new RuntimeException("未指定实体");
}
instance = em.merge(instance);
em.remove(instance);
}
/**
* 清空实体
*/
public void clear() {
instance = null;
instanceClass = null;
id = null;
}
/**
* 根据主键查找指定实体
* @return
*/
@Transactional(readOnly = true)
private T find() {
if (null == id) {
throw new RuntimeException("未指定实体id");
}
return em.find(instanceClass, id);
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
clear();
this.id = id;
}
/**
* 如果有实体,则返回该对象.
* 如果没有,则根据实体id执行查找.
* 如果Class对象为空,则通过反射得到范形参数的Class对象
*
* @return 返回Home所管理的实体对象
* @throws IlligalArgumentException 无法得到范形参数的Class对象
*/
@SuppressWarnings("unchecked")
public T getInstance() {
if (null != instance) {
return instance;
}
// 通过反射得到T的实际Class对象
if (null == instanceClass) {
// 得到父类的范型参数的Class对象
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType parmType = (ParameterizedType)type;
instanceClass = (Class<T>)parmType.getActualTypeArguments()[0];
} else {
throw new IllegalArgumentException("Could not guess entity class by reflection");
}
}
instance = find();
return instance;
}
public void setInstance(T instance) {
clear();
this.instance = instance;
}
}
以上代码实现了之前说提到的功能。这个类最核心的部分是 getInstance()方法。我们知道,要想调用 EntityManager 的 find() 方法,则需要提供一个类型为Class的参数。如:
entityManager.find(Customer.class, 1); // 查找主键为1的Customer
所以,要想实现只需让实体继承Home<T>类就能进行CRUD操作,核心的问题就是如何在子类中获取 Home<T> 中的泛型参数<T>的真实类型。如,
public class CustomerHome extends Home<Customer> {
}
Home<T>类必须知道是 T 实际上是Customer,才能给 EntityManager的 find() 方法传递正确的 Class 参数。这里可以通过反射实现:
// 通过反射得到T的实际Class对象
if (null == instanceClass) {
// 得到父类的范型参数的Class对象
Type type = getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
ParameterizedType parmType = (ParameterizedType)type;
instanceClass = (Class<T>)parmType.getActualTypeArguments()[0];
} else {
throw new IllegalArgumentException("Could not guess entity class by reflection");
}
}
我们来分析一下,为什么在父类中编写 getClass().getGenericSuperclass() 得到的恰好就是 Home<T> 中T的实际类型呢?因为当我们让 CustomerHome 继承 Home<T> 后,通过 CustomerHome 调用 getInstance() 时,由于子类没有重写该方法,因此会执行父类的 getInstance()代码,而这时的执行环境是子类,因此getClass().getGenericSuperclass() 得到的恰好就是 Home<T> 中T的实际类型。
这一部分参考了 Seam 的源码(https://github.com/VaclavDedik/jboss-seam/blob/Seam_2_3/jboss-seam/src/main/java/org/jboss/seam/framework/Home.java),这给了我很大启发,反射机制让Java如此优秀,无数的框架都建立在反射之上。
有了我们自己编写的 Home<T> 类之后,我们就可以在 Spring 环境下方便地使用 JPA了。以上代码就是针对 Spring 编写的。如果你用过 Seam 框架,你肯定知道它还有一个 Query<T> 组件,用于执行JPA查询操作。大家可以参考着Seam的源码自己实现一个玩玩。