本文主要探讨如何利用Spring来装配组件,包括其事务上下文。从J2EE应用程序内部连接到单个的数据库并不是什么难事。但是,如果要装配或者集成企业级的组件,情况就复杂了。一个组件可以有一个或多个支持它的数据库,因此,当装配两个或更多的组件时,我们希望能够保持在跨组件的多个数据库中进行的操作的原子性。J2EE服务器为这些组件提供了一个容器来保证事务原子性和跨组件独立性。如果使用的不是J2EE服务器,则可以利用Spring来帮助我们。Spring基于Inversion of Control(控制反转)模式(也称为依赖注入),它不仅可以连接组件服务,还可以连接关联的事务上下文。在本文中,我们将Hibernate用作对象/关系持久性存储和查询服务。
装配组件事务
假设在企业组件库里,我们已经有一个审计组件,里面有可以被客户端调用的服务方法。然后,当我们想要构建一个订单处理系统时,我们发现存在这样的设计要求:OrderListManager组件服务同样需要审计组件服务。OrderListManager创建和管理订单,因此所有的OrderListManager服务都有自己的事务属性。当我们从OrderListManager服务内调用审计组件时,我们实际上是在把OrderListManager服务的事务上下文传播给审计服务。也许将来新的业务服务组件同样需要审计组件,但那时将在一个不同的事务上下文中调用它。实际结果就是,即使审计组件的功能保持不变,它也可能是由别的业务服务功能组成,包含了混搭的(mix-and-match)事务属性来提供不同的运行时事务性行为。
在图1中有两个独立的调用上下文流程。在流程1里,如果客户端有TX上下文,那么OrderListManager既可以参与其中,也可以启动一个新的TX,这取决于客户端是否在TX中,以及为OrderListManager方法指定了什么样的TX属性。这同样适用于OrderListManager服务依次调用AuditManager方法的情况。
图1 装配组件事务
EJB架构允许组件装配者声明式地给出正确的事务属性,从而为他们提供这种灵活性。我们不探讨声明式事务管理的替代方案(即所谓的编程式事务控制),因为这会牵涉到代码更改,从而产生不同的运行时事务行为。几乎所有的J2EE应用服务器都按照X/Open XA规范提供了服从两阶段提交协议的分布式事务管理器。现在的问题是,我们能不能利用EJB服务器来实现相同的功能?Spring就是其中的一种解决方案。让我们来看一下Spring如何帮助我们解决事务组装的问题:
使用Spring进行事务管理
我们将看到一个轻量级的事务基础架构,它实际上可以管理组件级的事务装配。Spring是其中的一个解决方案。它的优点在于,我们不会被捆绑到J2EE容器服务(如JNDI DataSource)上。最棒的一点是,如果我们想把这个轻量级事务基础架构关联到一个已可用的J2EE容器基础架构,将不会有任何问题。看起来我们可以利用两者的优点。
另一方面,Spring这个轻量级事务基础架构使用了一个面向方面编程(Aspect-Oriented Programming,AOP)框架。Spring AOP框架使用了一个支持AOP的Spring bean工厂。在特定于Spring的配置文件applicationContext.xml中,通过在组件服务级指定事务特性来划分事务。
<beans>
<!-- other code goes here... -->
<bean id="orderListManager"
class="org.springframework.transaction
.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager1"/>
</property>
<property name="target">
<ref local="orderListManagerTarget"/>
</property>
<property name="transactionAttributes">
<props>
<prop key="getAllOrderList">
PROPAGATION_REQUIRED
</prop>
<prop key="getOrderList">
PROPAGATION_REQUIRED
</prop>
<prop key="createOrderList">
PROPAGATION_REQUIRED
</prop>
<prop key="addLineItem">
PROPAGATION_REQUIRED,
-com.example.exception.FacadeException
</prop>
<prop key="getAllLineItems">
PROPAGATION_REQUIRED,readOnly
</prop>
<prop key="queryNumberOfLineItems">
PROPAGATION_REQUIRED,readOnly
</prop>
</props>
</property>
</bean>
</beans>
一旦我们在服务级指定了事务属性,org.springframework.transaction.PlatformTransactionManager接口的一个特定实现就会截获并解释它们。该接口如下:
public interface PlatformTransactionManager{
TransactionStatus getTransaction
(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
Hibernate事务管理器
由于我们已决定使用Hibernate作为ORM工具,下一步要做的就是配置一个特定于Hibernate的事务管理器实现。
<beans>
<!-- other code goes here... -->
<bean id="transactionManager1"
class="org.springframework.orm.hibernate.
HibernateTransactionManager">
<property name="sessionFactory">
<ref local="sessionFactory1"/>
</property>
</bean>
</beans>
设计多个组件中的事务的管理
现在,我们来讨论什么是“装配组件事务”。您也许注意到了为域中的服务级组件OrderListManager所指定的各种TX属性。图2所示的业务域对象模型(Business Domain Object Model,BDOM)显示了我们的域所确定的主要对象:
图2 业务域对象模型(BDOM)
图字:Order:订单;Audit:审计
为了更好的说明,我们来列出我们的域中的一些非功能性需求(Non-Functional Requirement,NFR):
业务对象需要保存在一个数据库中(appfuse1)。
审计时要登录到另一个数据库中(appfuse2),出于安全的考虑,数据库要有防火墙保护。
业务组件应该可以重用。
必须尽一切努力审计业务服务层的所有活动。
考虑了以上要求之后,我们决定,OrderListManager服务会将所有的审计日志调用委托给已经可用的AuditManager组件。这样就得出了详细设计,如图3所示:
这里值得注意的一点是,由于我们的NFR,我们要将与OrderListManager相关的对象映射到appfuse1数据库,而将与审计相关的对象映射到appfuse2。这样,无论要审计什么,OrderListManager组件都会调用AuditManager组件。我们会看到,OrderListManager组件中的所有方法都应该是事务性的,因为我们通过服务来创建订单和线项目(line item)。那么AuditManager组件中的服务呢?因为它做的是审计跟踪,我们关心的是尽可能维持长时间的审计跟踪,并针对系统中所有可能的业务活动。这就产生了如下的需求:“即使主要的业务活动失败了,也要进行审计跟踪记录”。AuditManager组件同样要有自己的事务,因为它也与自己的数据库进行交互。如下所示:
<beans>
<!-- other code goes here... -->
<bean id="auditManager"
class="org.springframework.transaction.
interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref local="transactionManager2"/>
</property>
<property name="target">
<ref local="auditManagerTarget"/>
</property>
<property name="transactionAttributes">
<props>
<prop key="log">
PROPAGATION_REQUIRES_NEW
</prop>
</props>
</property>
</bean>
</beans>
现在,为了演示,我们把注意力放到createOrderList和addLineItem这两个业务服务上。同时请注意,我们并没有要求最佳设计策略——你可能注意到了,addLineItem方法抛出了FacadeException异常,而createOrderList却没有。在生产设计中,您也许希望每一个服务方法都可以处理异常场景。
public class OrderListManagerImpl
implements OrderListManager{
private AuditManager auditManager;
public Long createOrderList
(OrderList orderList){
Long orderId = orderListDAO.createOrderList(orderList);
auditManager.log(new AuditObject(ORDER + orderId, CREATE));
return orderId;
}
public void addLineItem
(Long orderId, LineItem lineItem)
throws FacadeException{
Long lineItemId = orderListDAO.addLineItem(orderId, lineItem);
auditManager.log(new AuditObject(LINE_ITEM + lineItemId, CREATE));
int numberOfLineItems = orderListDAO.
queryNumberOfLineItems(orderId);
if(numberOfLineItems > 2){
log("Added LineItem " + lineItemId +" to Order " + orderId + ";
But rolling back *** !");
throw new FacadeException("Make a new Order for this line item");
}
else{
log("Added LineItem " + lineItemId +" to Order " + orderId + ".");
}
}
//Other code goes here...
}
为了创建一个异常场景来进行演示,我们引入了另一种业务规则,它规定一个特定的订单不能包含多于两个的线项目。现在应该注意,我们是从createOrderList和addLineItem中调用auditManager.log()方法的。您应该也注意到了为上述方法所指定的事务属性。
<bean id="orderListManager"
class="org.springframework.transaction.
interceptor.TransactionProxyFactoryBean">
<property name="transactionAttributes">
<props>
<prop key="createOrderList">
PROPAGATION_REQUIRED
</prop>
<prop key="addLineItem">
PROPAGATION_REQUIRED,-com.
example.exception.FacadeException
</prop>
</props>
</property>
</bean>
<bean id="auditManager" class="org.
springframework.transaction.interceptor.
TransactionProxyFactoryBean">
<property name="transactionAttributes">
<props>
<prop key="log">
PROPAGATION_REQUIRES_NEW
</prop>
</props>
</property>
</bean>
PROPAGATION_REQUIRED等效于TX_REQUIRED,而PROPAGATION_REQUIRES_NEW等效于EJB中的TX_REQUIRES_NEW。如果我们想让服务方法始终在事务中运行,我们可以使用PROPAGATION_REQUIRED。当使用PROPAGATION_REQUIRED时,如果已经运行了一个TX,bean方法就会加入到该TX中;否则的话,Spring的轻量级TX管理器就会启动一个TX。如果在调用组件服务时我们总是希望开始新的事务,那么可以利用PROPAGATION_REQUIRES_NEW属性。
我们还指定,当方法抛出FacadeException类型的异常时,addLineItem就总是回滚事务。这就达到了另一个粒度级别:在异常场景中,我们的控制可以精细到TX的具体结束方式。前缀符号“-”指定回滚TX,而前缀符号“+”指定提交TX。
接下来的问题是,为什么我们要为log方法设置PROPAGATION_REQUIRES_NEW属性呢?这是由我们的以下需求决定的:无论主服务方法发生什么情况,对所有创建订单以及向系统添加线项目的尝试都要记录审计跟踪。也就是说,即使在createOrderList和addLineItem的实现过程中出现了异常也要记录审计跟踪。这仅在启动一个新的TX并在这个新的TX上下文中调用log的时候起作用。这就是为什么要为log设置PROPAGATION_REQUIRES_NEW TX属性的原因:如果对下述方法的调用成功了
auditManager.log(new AuditObject (LINE_ITEM +lineItemId, CREATE));
,auditManager.log()就将在新的TX上下文中执行,而且只要auditManager.log()本身成功(即,没有抛出异常),新的上下文就会被提交。
设置演示环境
准备演示环境时,我参考了Spring Live这本书的流程:
- 下载并安装以下组件,这时请注意使用准确的版本,不然就会引起版本不兼容问题。
- JDK 1_5_0_01或更高版本
- Apache Tomcat 5.5.7
- Apache Ant 1.6.2
- Equinox 1.2
- 在系统中设置以下环境变量:
- JAVA_HOME
- CATALINA_HOME
- ANT_HOME
- 把下列目录添加到您的PATH环境变量中,或者使用完全路径来执行脚本:
- JAVA_HOMEin
- CATALINA_HOMEin
- ANT_HOMEin
- 要设置Tomcat,在文本编辑器中打开/conf/tomcat-users.xml文件,验证以下各行是否存在。如果不存在,必须手动添加进去:
<role rolename="manager"/> <user username="admin" password="admin" roles="manager"/>
- 要创建基于Struts、Spring和Hibernate的Web应用程序,必须用Equinox来构建一个基本的框架程序(bare-bones starter application),它将包含预定义的文件夹结构、所有需要用到的.jar文件以及Ant构建脚本。把Equinox解压到一个文件夹中,它将创建一个equinox文件夹。将目录更改为equinox文件夹,输入命令ANT_HOMEinnt new -Dapp.name=myusers。这样就会创建一个与equinox同级的文件夹myusers。该文件夹的具体内容如下:
图4 Equinox的myusers应用程序文件夹模板
删除myuserswebWEB-INF文件夹下的所有。xml文件。
复制equinoxxtrasstrutswebWEB-INFibstruts*.jar文件至myuserswebWEB-INFib文件夹下,这样,这个示例应用程序就可以利用struts了。
从参考资料小节的示例代码中,解压myusersextra.zip到一个合适的位置。将目录更改为新创建的myusersextra文件夹,复制myusersextra文件夹中的所有内容,并将它们粘贴到myusers文件夹。
打开命令提示符,将目录转至myusers目录下。执行CATALINA_HOMEinstartup.要从myusers文件夹启动Tomcat,这一点非常重要,否则数据库将不会创建在myusers文件夹中,从而导致在执行一些定义在build.xml中的任务时出现错误。
再次打开命令提示符并将目录转至myusers目录下。执行ANT_HOMEinnt install.这将构建应用程序并把它部署到Tomcat中。这时,我们可以看到myusers中多了一个db目录,以便存放数据库appfuse1和appfuse2.
打开浏览器并验证myusers应用程序已经部署在http://localhost:8080/myusers/上了。
要重新安装应用程序,执行ANT_HOMEinnt remove,然后执行CATALINA_HOMEinshutdown关闭Tomcat.现在,从CATALINA_HOMEwebapps文件夹删除所有的myusers文件夹。然后执行CATALINA_HOMEinstartup重新启动Tomcat,并通过执行ANT_HOMEinnt install重新安装应用程序。
运行演示
为了运行测试用例,myusers estmxampleservice中提供了一个JUnit测试类,OrderListManagerTest.要执行它,可以在构建应用程序的命令提示符中输入以下命令:
CATALINA_HOMEinnt test -Dtestcase=OrderListManager 测试用例分为两个主要部分:第一部分创建一个由两个线项目组成的订单,然后把这两个线项目链接到订单中。它可以成功运行,如下所示:
OrderList orderList1 = new OrderList();
Long orderId1 = orderListManager.
createOrderList(orderList1);
log("Created OrderList with id '"
+ orderId1 + "'...");
orderListManager.addLineItem(orderId1,lineItem1);
orderListManager.addLineItem(orderId1,lineItem2);
第二部分执行类似的操作,但是这次我们试图向订单添加三个线项目,这将产生一个异常:
OrderList orderList2 = new OrderList();
Long orderId2 = orderListManager.
createOrderList(orderList2);
log("Created OrderList with id '" + orderId2 + "'...");
orderListManager.addLineItem(orderId2,lineItem3);
orderListManager.addLineItem(orderId2,lineItem4);
//We know, we will have an exception here,still want to proceed
try{
orderListManager.addLineItem
(orderId2,lineItem5);
}
catch(FacadeException facadeException){
log("ERROR : " + facadeException.getMessage());
}
控制台的输出如图5所示:
我们创建了Order1,并向其添加了两个ID为1和2的线项目。然后我们创建Order2,并尝试添加3个项目,前两个(ID为3和4)添加成功,但是图5显示,添加第三个项目(ID为5)时业务方法遇到了异常。因此,业务方法TX被回滚,数据库中没有ID为5的线项目。从控制台执行以下命令,就可以通过图6和图7进行验证:
CATALINA_HOMEinnt browse1
图6 appfuse1数据库中创建的订单
图7 appfuse1数据库中创建的线项目
在接下来的也是最重要的演示部分中可以看出,订单和线项目保存在appfuse1数据库中,而审计对象保存在appfuse2数据库中。实际上,OrderListManager中的服务方法可以与多个数据库交互。启动appfuse2数据库,查看审计跟踪,如下所示:
CATALINA_HOMEinnt browse2
图8 创建到appfuse2数据库中的审计跟踪,包括失败TX的记录项
表8最后一行尤其值得注意,RESOURCE列显示这一行对应的是LineItem5。但是当我们回过来看图7时,却发现并没有对应于LineItem5的线项目。哪里出错了呢?事实上并没有出错,图7没有的那一行其实正是这篇文章的关键所在,让我们来看看是怎么回事。
我们知道,addLineItem()方法包含PROPAGATION_REQUIRED属性,而log()方法有PROPAGATION_REQUIRES_NEW属性。而且addLineItem()在内部调用了log()方法。因此,当我们试图向Order2添加第三个线项目时,就(按照我们的业务规则)引发了异常,于是这个线项目的创建以及将其链接到Order2的操作都被回滚了。但是,因为还从addLineItem()中调用了log(),还因为log()具有PROPAGATION_REQUIRES_NEW TX属性,回滚addLineItem()将不会造成回滚log(),因为log()是在一个新的TX中执行。
让我们对log()的TX属性做一下改动,把PROPAGATION_REQUIRES_NEW替换为PROPAGATION_SUPPORTS。PROPAGATION_SUPPORTS属性允许服务方法在客户端有TX上下文时在客户端TX中运行;如果客户端没有TX,就不用TX而直接运行。您可能必须重新安装应用程序,以便数据库中已经可用的数据可以自动刷新。重新安装请按照“设置演示环境”中的步骤12进行。
如果再次运行,我们会发现一点不同。这次,在试图向Order2添加第三个线项目时依然有异常,这将回滚试图添加第三个线项目的事务。而这个方法又调用了log()方法。但是由于它的PROPAGATION_SUPPORTS TX属性,log()将在与addLineItem()方法相同的TX上下文中调用。由于addLineItem()回滚,log()也回滚了,没有留下回滚的TX的审计跟踪。所以在图9中没有对应于失败TX的审计跟踪记录项!
图9 创建在appfuse2数据库中的审计跟踪,没有失败TX的记录项
我们改动的仅仅是Spring配置文件中的TX属性,就产生了这样不同的事务行为,如下所示:
<bean id="auditManager" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionAttributes">
<props>
<!-- prop key="log">
PROPAGATION_REQUIRES_NEW
</prop -->
<prop key="log">
PROPAGATION_SUPPORTS
</prop>
</props>
</property>
</bean>
这就是声明式事务管理的效果。自从EJB出现以来我们就一直在使用这种方法,但是我们需要一个高端的应用服务器来驻留EJB组件。现在,我们可以看到,利用Spring,没有EJB服务器也可以达到类似的结果。
结束语
这篇文章重点介绍了J2EE领域的强大组合之一:Spring和Hibernate。利用二者的功能,现在对于容器管理持久性(Container-Managed Persistence,CMP)、容器管理关系(Container-Managed Relationships,CMR)和声明式事务管理,我们多了一种技术选择。虽然Spring不能视为EJB的替代方案,但是它提供的许多功能,例如普通Java对象的声明式事务管理,使得在许多项目中没有EJB也完全可以。
本文的目的不是要寻找EJB的替代方案,而是为当前的问题找出一个最可行的技术方案。至于Spring和Hibernate的轻量级组合的更多功能,就留给我们的读者去探索了。
3 楼 mebeyfeng 2010-12-20 17:52
[*]
2 楼 zhengyutong 2010-10-13 11:54
1 楼 yuantong 2009-04-26 22:20