如何使用EJB 3和JPA编程模型加速和简化多层Web应用程序开发
简单性不只是新的EJB 3规范的重要驱动因素之一,对于整个Java EE 5平台来说也是如此。基于POJO的新EJB 3和对独有的Java Persistence API (JPA)接口的持久化服务委托,给bean开发人员简化和加速整个编码-测试-调试周期带来了极好的机会。
新技术革新要求新的思想、新的开发实践。本文将探索一种加速构建多层应用程序的编码-测试-调试周期的面向服务开发模式。
为了说明,我们来考虑一个典型的业务方法:
public interface ReviewService { /** * Creates a new Review by the given reviewer for the given item. * * @param reviewerName name of the reviewer. The named reviewer must exist. * @param itemId unique identifier of the item being reviewed. The item must * exist. * @param rating an integer between BEST_RATING and WORST_RATING * @param comment a free form text as a comment * * @return newly created Review * @exception IllegalArgumentException if the named reviewer or item does not * exist. Or if the rating has non-permissible value. * @exception NullPointerException if comment is null or empty * **/ Review newReview (String reviewerName, String itemId, int rating, String comment);
我们需要一个EJB 3无状态会话Bean以实现此方法。所以我们编写如下代码:
01 @Stateless 02 @Remote(ReviewService.class) 03 public class ReviewSessionBean implements ReviewService 04 { 05 @TransactionAttribute(REQUIRED) 06 public Review newReview (String reviewerName, String itemId, 07 int rating, String comment) 08 { 09 Item item = em.find(Item.class,itemId); 10 Reviewer reviewer = em.find(Reviewer.class, reviewerName); 11 if (item==null) 12 throw new IllegalArgumentException("No item with id [" + itemId + "]"); 13 if (reviewer==null) 14 throw new IllegalArgumentException("No reviewer named [" + reviewerName + "]"); 15 if (rating < WORST_RATING || rating > BEST_RATING) 16 throw new IllegalArgumentException ("Illegal rating " + rating + 17 ". Must be between " + WORST_RATING + " and " + BEST_RATING); 18 if (comment==null || comment.trim().length()==0) 19 throw new NullPointerException("Null or empty comment is not allowed"); 20 21 Review review = reviewer.review(item, rating, comment); 22 em.persist(review); 23 24 return review; 25 } 26 @PersistenceContext(unitName="ejax.session") 27 private EntityManager em;
以上代码行有几点需要注意:
A. em是什么?它是由EJB容器注入的javax.persistence.EntityManager。容器注入指令为以下的两个代码行:
26 @PersistenceContext(unitName="ejax.session") 27 private EntityManager em;
这使用了Java EE 5的依赖注入或反向控制设计模式。
B. 没有启动或提交显式事务。
方法级注释
05 @TransactionAttribute(REQUIRED)
告知容器,在调用会话bean的方法之前,不仅要将EntityManager注入到该方法,还要确保EntityManager是一个活动事务。并且,在bean的方法完成后,容器提交事务。
C. 哪种类型的事务?
取决于在ejax.session持久化单元中为transaction-type属性指定的值。选项是JTA和RESOURCE_LOCAL,对于给定的示例,在META-INF/persistence.xml中持久化单元被指定为
<persistence-unit name="ejax.session" transaction-type="JTA">
新技术要求新思维
毫无疑问,该会话Bean是使用新的EJB 3技术编写的。但是遵循的仍然是传统的思维和传统的开发模式。为什么这么说?在做进一步解释之前,让我们分析一些对这些典型代码行的观察结果。
观察1:业务方法要受到业务逻辑和基础架构支持服务的相互影响。业务功能的正确执行要依赖于业务语义,例如:
——Reviewer.review()方法运行正常吗?
——如果传递了一个负的值,方法会抛出异常吗?
还依赖于基础架构服务:
——活动事务会注入正确的EntityManager吗?
——新的Review实例是否提交给了数据库?
观察2:bean开发人员关注的是第一类正确性,基础架构提供者关注的是第二类正确性。当然,bean开发人员/部署人员必须要验证基础架构服务的指定正确。例如,META-INF/persistence.xml中的持久化单元名称ejax.session与注入的 EntityManager的注释一致:
26 @PersistenceContext(unitName="ejax.session")
在编写此示例时,我把它弄乱成了下面的样子:
@PersistenceContext(name="ejax.session")
@PersistenceContext注释上的限定符名称是指实例的JNDI名称,而unitName则指要寻找的持久性单元名称。要点是,如果部署描述符的指定正确,那么应用服务器就应该执行其应做的工作——那么bean开发人员只需验证描述符是否正确。
但是,由于编码bean方法的方式,它不区分测试业务语义与验证开发描述符之间的区别。
后面的实现模式使得只有一种测试选项,即,在应用容器中部署该会话bean,于是可能会找到一些低级错误,例如:
if (rating > WORST_RATING || rating < BEST_RATING)
一旦在控制台上转储冗长的堆栈跟踪,即使立刻检测出错误,也需要编辑、编译、打包、部署、测试和验证错误是否排除。
分离业务逻辑和基础架构服务
该设计目标是将业务逻辑的实现与对应用服务器的依赖相分离。新EJB 3通过将实体Java Bean定义为POJO,已经朝这一简单性目标迈出了一大步。持久化服务也被当作独立的服务API(不管是否有容器支持)。
当前的设计策略扩展了类似的模式来捕获业务逻辑作为基于POJO的服务。一旦在没有容器的情况下在更短的编码-测试-调试周期中测试基于POJO的服务,被测试的业务服务就会逆行回到原始无状态会话Bean,后者在应用服务器的基础架构服务(例如,依赖注入、事务控制、多线程、远程处理)之间起中介作用。下图中描述了该策略:
业务接口ReviewService在Java类ReviewServiceImpl中实现,不存在基础架构问题。这样一来,业务逻辑可用于 Web服务、会话Bean或Swing GUI按钮的动作处理程序——业务服务开发人员完全没有必要去猜测究竟是哪一种。那是良好服务设计的特点——对调用方不可知。这也称为自私编程。
同一业务方法的新的基础架构无关的面向服务实现过程如下
01 public class ReviewServiceImpl 02 extends PersistenceService 03 implements ReviewService 04 { 05 public Review newReview (String reviewerName, String itemId, 06 int rating, String comment) 07 { 08 EntityManager em = beginTransaction(); 09 Item item = em.find(Item.class,itemId); 10 Reviewer reviewer = em.find(Reviewer.class, reviewerName); 11 if (item==null) 12 throw new IllegalArgumentException("No item with id [" + itemId + "]"); 13 if (reviewer==null) 14 throw new IllegalArgumentException("No reviewer named [" + reviewerName + "]"); 15 if (rating < WORST_RATING || rating > BEST_RATING) 16 throw new IllegalArgumentException ("Illegal rating " + rating + 17 ". Must be between " + WORST_RATING + " and " + BEST_RATING); 18 if (comment==null || comment.trim().length()==0) 19 throw new NullPointerException("Null or empty comment is not allowed"); 20 21 Review review = reviewer.review(item, rating, comment); 22 em.persist(review); 23 commit(); 24 25 return review; 26 }
如果将新的实现与原始无状态会话Bean相比较,我们可以看到:
- 类级注释@Stateless和@Remote没有了。
- 方法级注释@TransactionAttribute(REQUIRED)移除了。
- EntityManager的依赖注入移除了。
此外,通过beginTransaction()和commit()引入了新的显式事务划分。
实际上,所有有效的容器注释都被移除了,并提供了一种事务控制机制,它是正确操作业务方法的关键。
从基础架构服务分离业务逻辑的优点
业务逻辑与基础架构服务的分离为我们带来了什么?我们可以在容器外测试业务逻辑(在某种程度上)。这又会显著缩短编码-测试-调试周期,并最大限度地降低资源需求。
但是事情还没有结束。我们在实现过程中去掉了容器服务。但我们是否把有价值的东西和不必要的东西一起扔掉了?我们如何确保业务方法可以访问EntityManager以完成其功能?
首先是要获得同一持久化单元,创建一个可以提供所需EntityManager的EntityManagerFactory。
我们还需要更多。我们的最初目标是,通过把EntityManager注入到无状态会话Bean使服务可用。所以我们需要满足双重需求(稍微有些冲突) a)从基础架构分离业务功能,以便缩短部署周期;b) 创建一个目标解决方案,利用具有业务逻辑的容器基础架构。同时实现两种功能的解决方案是通过PersistenceService实现的,这是一个具有双重特性的类。该类可以根据其调用上下文显式地控制事务或将其事务控制权转交给容器。ReviewServiceImpl扩展自它,并使用了它的双重事务控制方法beginTransaction()和commit。
以下是PersistenceService.commit()方法的实现:
protected final void commit() { if (isManaged()) return; EntityManager em = getEntityManager(); if (em.getTransaction().isActive()) em.getTransaction().commit(); }
该类知道它是否处于托管环境。如果是,它就放弃其事务控制权,它的commit()实际成为一个空操作。否则,它就自我运行并提交事务。
它如何检测自己是否处于托管事务中?如果它与EntityManager一起注入,它就会认为它本身也是托管的,否则它就是独立的。它通过以下方法完成该逻辑:
private final EntityManagerFactory _emf; private final ThreadLocal_thread; private volatile boolean _isManaged; protected synchronized EntityManager getEntityManager() { EntityManager em = _thread.get(); if (em!=null) return em; else if (_emf!=null) { em = _emf.createEntityManager(); _thread.set(em); return em; } else throw new NullPointerException("no-entity-manager"); } protected synchronized void injectEntityManager (EntityManager em) { _isManaged = true; _thread.set(em); } public final boolean isManaged() { return _isManaged; }
它在ThreadLocal变量_thread中维持每个线程的EntityManager。对其getEntityManager方法的调用,提供了之前通过injectEntityManager()注入的EntityManager(即,它放弃其事务功能,处在托管状态),或者是一个基于每个线程从EntityManagerFactory创建的EntityManager。最后,其beginTransaction()方法成为 getEntityManager和EntityManager.getTransaction().begin()方法的组合。
protected final EntityManager beginTransaction() { EntityManager em = getEntityManager(); if (!isManaged() && !em.getTransaction().isActive()) em.getTransaction().begin(); return em; }
重构原始会话Bean
我们已经将ReviewServiceImpl中ReviewService的业务契约从原始无状态会话Bean ReviewSessionBean中分离了。
我们想出了一种通过对PersistenceService的抽象在容器内外提供EntityManager的技术。
让我们用这些工具来重构原始会话Bean:
01 @Stateless 02 @Remote(ReviewService.class) 03 public class ReviewSessionBean 04 extends ReviewServiceImpl 05 implements ReviewService 06 { 07 @TransactionAttribute(REQUIRES_NEW) 08 public Review newReview (Reviewer reviewer, Item item, 09 int rating, String comment) 10 { 11 injectEntityManager(em); 12 return super.newReview(reviewer, item, rating, comment); 13 } 14 @PersistenceContext(unitName="ejax.session") 15 private EntityManager em; 16 }
在该重构版本中:
- 会话Bean扩展了业务服务实现ReviewServiceImpl,它有效实现了ReviewService的核心外部契约。
- 重写了基础架构服务注释(@Stateless、@Remote、@PersistenceContext)。
- 已注入的EntityManager被重新注入到超类PersistenceService中,这意味着它要在托管环境中自我管理。
- 最后,通过在超类中的实现,实现了业务功能。
我们通过继承实现策略,不过也可以通过委托来实现。
JUnit测试用例
JUnit测试用例遵循同样的继承层次结构。两个JUnit测试用例TestService和TestSession都测试同一接口ReviewService。但是前者通过直接构造调用 ReviewServiceImpl实例,而后者则通过JNDI查找来连接到ReviewSessionBean。实际上,TestSession仅执行其setUp()来连接到远程会话Bean,它的测试方法早已在超类TestService中实现。
结束语
新版EJB 3和JPA简化了开发。在本文中,通过双重事务控制设计模式,以同样的方式简化了会话Bean的编码-测试-调试周期,并包括了代码示例。
在下一篇文章中,我将讨论另一实践问题:如何在不同的测试环境中转换数据库?
原文出处:http://dev2dev.bea.com/blog/pinaki.poddar/archive/2006/05/how_ejb_3_speed.html