分类:
2008-10-27 13:28:38
本文主要探讨如何利用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中,通过在组件服务级指定事务特性来划分事务。
.interceptor.TransactionProxyFactoryBean">
PROPAGATION_REQUIRED
PROPAGATION_REQUIRED
PROPAGATION_REQUIRED
PROPAGATION_REQUIRED,
-com.example.exception.FacadeException
PROPAGATION_REQUIRED,readOnly
PROPAGATION_REQUIRED,readOnly
一旦我们在服务级指定了事务属性,org.springframework.transaction.PlatformTransactionManager接口的一个特定实现就会截获并解释它们。该接口如下:
public interface PlatformTransactionManager{
TransactionStatus getTransaction
(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
Hibernate事务管理器
由于我们已决定使用Hibernate作为ORM工具,下一步要做的就是配置一个特定于Hibernate的事务管理器实现。
HibernateTransactionManager">
设计多个组件中的事务的管理
现在,我们来讨论什么是“装配组件事务”。您也许注意到了为域中的服务级组件OrderListManager所指定的各种TX属性。图2所示的业务域对象模型(Business Domain Object Model,BDOM)显示了我们的域所确定的主要对象:
图2 业务域对象模型(BDOM)
图字:Order:订单;Audit:审计
为了更好的说明,我们来列出我们的域中的一些非功能性需求(Non-Functional Requirement,NFR):
业务对象需要保存在一个数据库中(appfuse1)。
审计时要登录到另一个数据库中(appfuse2),出于的考虑,数据库要有保护。
业务组件应该可以重用。
必须尽一切努力审计业务服务层的所有活动。
考虑了以上要求之后,我们决定,OrderListManager服务会将所有的审计日志调用委托给已经可用的AuditManager组件。这样就得出了详细设计,如图3所示:
图3 组件服务的设计
这里值得注意的一点是,由于我们的NFR,我们要将与OrderListManager相关的对象映射到appfuse1数据库,而将与审计相关的对象映射到appfuse2。这样,无论要审计什么,OrderListManager组件都会调用AuditManager组件。我们会看到,OrderListManager组件中的所有方法都应该是事务性的,因为我们通过服务来创建订单和线项目(line item)。那么AuditManager组件中的服务呢?因为它做的是审计跟踪,我们关心的是尽可能维持长时间的审计跟踪,并针对系统中所有可能的业务活动。这就产生了如下的需求:“即使主要的业务活动失败了,也要进行审计跟踪记录”。AuditManager组件同样要有自己的事务,因为它也与自己的数据库进行交互。如下所示:
interceptor.TransactionProxyFactoryBean">
PROPAGATION_REQUIRES_NEW
现在,为了演示,我们把注意力放到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()方法的。您应该也注意到了为上述方法所指定的事务属性。
interceptor.TransactionProxyFactoryBean">
PROPAGATION_REQUIRED
PROPAGATION_REQUIRED,-com.
example.exception.FacadeException
PROPAGATION_REQUIRES_NEW
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()本身成功(即,没有抛出异常),新的上下文就会被提交。
设置演示环境
准备演示环境时,我参考了这本书的流程:
password="admin" roles="manager"/>
图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应用程序已经部署在: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所示:
图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属性,就产生了这样不同的事务行为,如下所示:
PROPAGATION_SUPPORTS
这就是声明式事务管理的效果。自从EJB出现以来我们就一直在使用这种方法,但是我们需要一个高端的应用服务器来驻留EJB组件。现在,我们可以看到,利用Spring,没有EJB服务器也可以达到类似的结果。
结束语
这篇文章重点介绍了J2EE领域的强大组合之一:Spring和Hibernate。利用二者的功能,现在对于容器管理持久性(Container-Managed Persistence,CMP)、容器管理关系(Container-Managed Relationships,CMR)和声明式事务管理,我们多了一种技术选择。虽然Spring不能视为EJB的替代方案,但是它提供的许多功能,例如普通对象的声明式事务管理,使得在许多项目中没有EJB也完全可以。
本文的目的不是要寻找EJB的替代方案,而是为当前的问题找出一个最可行的技术方案。至于Spring和Hibernate的轻量级组合的更多功能,就留给我们的读者去探索了。