spring test + junit 4 + jpa 2.0 定制JPA执行环境/配置

7 篇文章 0 订阅
2 篇文章 0 订阅
在用 spring test + junit 4 + jpa 2.0 进行单元测试的使用,碰到一个如下的需求
因为很多测试用例创建在不同的包下面,但为了测试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图

Sure, here's an outline of how you can implement the shipping REST APIs using Spring Boot, Spring MVC, JPA/Batis, MySQL, and JWT: 1. Set up a Spring Boot project with the necessary dependencies for Spring MVC, JPA/Batis, MySQL, and JWT. You can use Spring Initializr to generate the project structure and add the dependencies. 2. Define the domain model for the shipping application, including entities such as Order, Product, Customer, and ShippingAddress. Map the entities to database tables using JPA annotations or MyBatis mapper XML files. 3. Implement the repository layer to perform CRUD operations on the database using JPA or MyBatis. You can use Spring Data JPA or MyBatis-Spring to simplify the implementation. 4. Define the REST API endpoints for the shipping application using Spring MVC annotations. Use JWT for authentication and authorization of the API endpoints. 5. Implement the service layer to perform business logic operations such as calculating shipping costs, validating orders, and processing payments. Use dependency injection to inject the repository and other services into the service layer. 6. Write unit tests to ensure that the application logic is working correctly. You can use JUnit and Mockito to write the tests. 7. Deploy the application to a server and test the API endpoints using a tool such as Postman. Here's some example code to get you started: ```java @RestController @RequestMapping("/api/orders") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/") public ResponseEntity<Order> createOrder(@RequestBody Order order) { Order createdOrder = orderService.createOrder(order); return ResponseEntity.ok(createdOrder); } @GetMapping("/{id}") public ResponseEntity<Order> getOrder(@PathVariable("id") Long orderId) { Order order = orderService.getOrder(orderId); return ResponseEntity.ok(order); } // Other API endpoints for updating and deleting orders } @Service public class OrderService { @Autowired private OrderRepository orderRepository; public Order createOrder(Order order) { // Calculate shipping costs and validate the order order.setShippingCosts(10.0); order.setTotalPrice(order.getProducts().stream() .mapToDouble(Product::getPrice) .sum() + order.getShippingCosts()); return orderRepository.save(order); } public Order getOrder(Long orderId) { return orderRepository.findById(orderId) .orElseThrow(() -> new NotFoundException("Order not found")); } // Other service methods for updating and deleting orders } @Repository public interface OrderRepository extends JpaRepository<Order, Long> { } ``` This code defines a REST API endpoint for creating orders and getting orders by ID. The order creation logic is implemented in the OrderService class, which calculates shipping costs and saves the order to the database using the OrderRepository interface. JWT authentication and authorization can be added to the API endpoints using Spring Security.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值