在用 spring test + junit 4 + jpa 2.0 进行单元测试的使用,碰到一个如下的需求
LoadAnnotationEntityTestExecutionListener.java
LoadEntityPersistenceUnitPostProcessor.java
beans.xml
persistence.xml
因为很多测试用例创建在不同的包下面,但为了测试jap的持久化特性,需要创建一些可持久化的实体类,即package1.Entity 和 package2.Entity 因为偷懒每个包下面的实体类名都是一样的,但访问级别都是包级别,这样就不会出现导入混淆,但是需求又
希望在执行 Test1 的时候 jpa 只装载 package1.Entity,执行 Test2 的时候只装载 package2.Entity,这样一个简单的办法就是通过修改 META-INF/persistence.xml 文件中
<class>xxx.xxx.package1.Entity</class>标签对应的值,但是如果修改这里的值之后又将导致其他包中的测试用例无法通过,当然你也可以创建很多 persistence.xml 文件,然后指定每次加载的 persistence.xml (可以通过加载不同的 spring 配置文件 beans.xml 文件来指定),但即使这样做你也会发现你的 persistence.xml 文件中大部分内容都是一样的,只是 <class /> 标签中的值不一样,于是
开始考虑能否在测试用例类级别的注解中配置运行该测试用例时需要加载的实体类,让这个注解的效果等同于在 persistence.xml 文件中的 <class /> 标签的配置
xxx.xxx.package1.Entity
xxx.xxx.package1.Test1
-------------------------
xxx.xxx.package2.Entity
xxx.xxx.package2.Test1
在测试用例中指定需要JPA装载的实体类,方式如下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml")
@Transactional
@TransactionConfiguration(defaultRollback = false)
@LoadEntities({ "com.jqd.examples.jpa2.idmapping.IdentityEntity" })
public class IdMappingTests {
LoadEntities 注解的定义如下(自定义)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface LoadEntities {
String[] value();
}
这样就不需要在 persistence.xml 中配置 <class /> 标签了,因为标签中的值是跟着测试用例一起声明的,所有在 persistence.xml 中就只需要配置一下公共的信息即可
问题:如果在创建 EntityManagerFactory 实例之前如何将测试用例中配置的 LoadEntities 这个注解信息告知容器
分析:EntityManagerFactory 这个类的实例是由 spring 创建和管理的,根据 spring 的配置文件 beans.xml
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="mysql.persistence" />
</bean>
我们通过 LocalEntityManagerFactoryBean 这个 factory bean 创建 EntityManagerFactory,然后通过看这个 factory bean 的源代码中的注释可以发现在注释中发现如下描述:
LocalEntityManagerFactoryBean 类中部分注释如下:
这个注释告诉我们在 LocalEntityManagerFactoryBean 中只提供了有限的配置,如果需要更灵活的配置,可以使用 LocalContainerEntityManagerFactoryBean 来替代,于是在同一个包下找到这个这个类
跟踪到
LocalContainerEntityManagerFactoryBean 这个类的源代码,部分代码和注释如下:
这个类有两个依赖 PersistenceUnitManager 和 PersistenceUnitInfo,其中还声明了一个 final DefaultPersistenceUnitManager,可以估计的到 internalPersistenceUnitManager 这个是 persistenceUnitManager 的一个内部实现,也可以说是默认实现。persistenceUnitManager 这个属性提供了相应的 set 方法,而 persistenceUnitInfo 这个属性却没有提供 set 方法,通过跟踪代码我们可以发现 persistenceUnitInfo 这个属性的值是用过 persistenceUnitManager#obtainDefaultPersistenceUnitInfo 方法获得的
跟踪到
PersistenceUnitInfo 接口的一个实现类
MutablePersistenceUnitInfo 部分代码如下:
通过这个 add 方法我们就可以加入我们在测试用例中通过
@LoadEntities
注解指定的 class name,这就是我们终极的解决方案
问题:如果才能获得在 LocalContainerEntityManagerFactoryBean 中的 PersistenceUnitInfo 实现类的引用呢
继续观察
LocalContainerEntityManagerFactoryBean 的源码,我们发现如下一个方法
虽然在 LocalContainerEntityManagerFactoryBean 这个类中没有一个
private PersistenceUnitPostProcessors[] postProcessors
这样的属性声明,但是我们知道 spring 注入属性时是通过 set 方法,即即使该类没有声明一个这个的属性,但声明了 set 方法我们同样可以为这个类的实例注入引用
观察 PersistenceUnitPostProcessors 这个接口的声明如下:
PersistenceUnitPostProcessors 接口代码如下
这是一个回调接口,在JPA处理完 PersistenceUnitInfo 对象之后回调,然后接口方法中还传入的 MutablePersistenceUnitInfo 对象的引用(注:MutablePersistenceUnitInfo 类实现了 PersistenceUnitInfo 这个接口)
OMG 这不就是我们需要的东西吗?!,于是新建一个类,实现该接口,代码如下:
自定义
LoadEntityPersistenceUnitPostProcessor 类代码如下:
修改 spring beans.xml 配置文件如下:
问题:Junit 如何将测试用例中配置的 LoadEntities 的信息设置到 LoadEntityPersistenceUnitPostProcessor 类中的 loadEntityClassNames 属性中
这个问题可以通过 spring test 提供的一个回调接口 TestExecutionListener 来实现,自定义一个该接口的实现类,也可以通过继承 AbstractTestExecutionListener 来重写需要的方法
LoadAnnotationEntityTestExecutionListener 实现类代码如下:
相应的测试用例中的代码如下
运行测试用例,大功告成!
附录 I:整个实现的代码
IdMappingTests.java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml")
@Transactional
@TransactionConfiguration(defaultRollback = false)
@TestExecutionListeners(listeners = { LoadAnnotationEntityTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class })
@LoadEntities({ "com.jqd.examples.jpa2.idmapping.IdentityEntity" })
public class IdMappingTests {
@PersistenceContext(name = "mysql.persistence")
private EntityManager em;
@Test
public void saveIdentityEntity() {
Assert.assertNotNull(em);
}
}
LoadAnnotationEntityTestExecutionListener.java
public class LoadAnnotationEntityTestExecutionListener extends AbstractTestExecutionListener {
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
Class<?> testClass = testContext.getTestClass();
LoadEntities loadEntitiesAnnotation = testClass.getAnnotation(LoadEntities.class);
if (loadEntitiesAnnotation != null) {
String[] loadEntityClassNames = loadEntitiesAnnotation.value();
LoadEntityPersistenceUnitPostProcessor.registerLoadEntityClassNames(Arrays
.asList(loadEntityClassNames));
}
}
}
LoadEntityPersistenceUnitPostProcessor.java
public class LoadEntityPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor {
private static List<String> loadEntityClassNames = new ArrayList<String>();
public static void registerLoadEntityClassNames(List<String> loadEntities) {
loadEntityClassNames.clear();
loadEntityClassNames.addAll(loadEntities);
}
public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) {
for (String loadEntityClassName : loadEntityClassNames) {
pui.addManagedClassName(loadEntityClassName);
}
}
}
beans.xml
<!-- JPA EntityManager -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="mysql.persistence" />
<property name="persistenceUnitPostProcessors">
<bean class="com.jqd.examples.jpa2.LoadEntityPersistenceUnitPostProcessor" />
</property>
</bean>
persistence.xml
<persistence-unit name="mysql.persistence"
transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.hbm2ddl.auto" value="create" />
......
附录 II:LocalContainerEntityManagerFactoryBean 类部分依赖关系 UML图