本文主要探讨如何利用Spring来装配组件包括其事务上下文从JEE应用程序内部连接到单个的数据库并不是什么难事但是如果要装配或者集成企业级的组件情况就复杂了一个组件可以有一个或多个支持它的数据库因此当装配两个或更多的组件时我们希望能够保持在跨组件的多个数据库中进行的操作的原子性JEE服务器为这些组件提供了一个容器来保证事务原子性和跨组件独立性如果使用的不是JEE服务器则可以利用Spring来帮助我们Spring基于Inversion of Control(控制反转)模式(也称为依赖注入)它不仅可以连接组件服务还可以连接关联的事务上下文在本文中我们将Hibernate用作对象/关系持久性存储和查询服务
装配组件事务
假设在企业组件库里我们已经有一个审计组件里面有可以被客户端调用的服务方法然后当我们想要构建一个订单处理系统时我们发现存在这样的设计要求OrderListManager组件服务同样需要审计组件服务OrderListManager创建和管理订单因此所有的OrderListManager服务都有自己的事务属性当我们从OrderListManager服务内调用审计组件时我们实际上是在把OrderListManager服务的事务上下文传播给审计服务也许将来新的业务服务组件同样需要审计组件但那时将在一个不同的事务上下文中调用它实际结果就是即使审计组件的功能保持不变它也可能是由别的业务服务功能组成包含了混搭的(mixandmatch)事务属性来提供不同的运行时事务性行为
在图中有两个独立的调用上下文流程在流程里如果客户端有TX上下文那么OrderListManager既可以参与其中也可以启动一个新的TX这取决于客户端是否在TX中以及为OrderListManager方法指定了什么样的TX属性这同样适用于OrderListManager服务依次调用AuditManager方法的情况
图 装配组件事务
EJB架构允许组件装配者声明式地给出正确的事务属性从而为他们提供这种灵活性我们不探讨声明式事务管理的替代方案(即所谓的编程式事务控制)因为这会牵涉到代码更改从而产生不同的运行时事务行为几乎所有的JEE应用服务器都按照X/Open XA规范提供了服从两阶段提交协议的分布式事务管理器现在的问题是我们能不能利用EJB服务器来实现相同的功能?Spring就是其中的一种解决方案让我们来看一下Spring如何帮助我们解决事务组装的问题:
使用Spring进行事务管理
我们将看到一个轻量级的事务基础架构它实际上可以管理组件级的事务装配Spring是其中的一个解决方案它的优点在于我们不会被捆绑到JEE容器服务(如JNDI DataSource)上最棒的一点是如果我们想把这个轻量级事务基础架构关联到一个已可用的JEE容器基础架构将不会有任何问题看起来我们可以利用两者的优点
另一方面Spring这个轻量级事务基础架构使用了一个面向方面编程(AspectOriented ProgrammingAOP)框架Spring AOP框架使用了一个支持AOP的Spring bean工厂在特定于Spring的配置文件applicationContextxml中通过在组件服务级指定事务特性来划分事务
<beans>
<! other code goes here >
<bean id=orderListManager
class=orgspringframeworktransaction
interceptorTransactionProxyFactoryBean>
<property name=transactionManager>
<ref local=transactionManager/>
</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
comexampleexceptionFacadeException
</prop>
<prop key=getAllLineItems>
PROPAGATION_REQUIREDreadOnly
</prop>
<prop key=queryNumberOfLineItems>
PROPAGATION_REQUIREDreadOnly
</prop>
</props>
</property>
</bean>
</beans>
一旦我们在服务级指定了事务属性orgspringframeworktransactionPlatformTransactionManager接口的一个特定实现就会截获并解释它们该接口如下
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=transactionManager
class=orgspringframeworkormhibernate
HibernateTransactionManager>
<property name=sessionFactory>
<ref local=sessionFactory/>
</property>
</bean>
</beans>
设计多个组件中的事务的管理
现在我们来讨论什么是装配组件事务您也许注意到了为域中的服务级组件OrderListManager所指定的各种TX属性图所示的业务域对象模型(Business Domain Object ModelBDOM)显示了我们的域所确定的主要对象
图 业务域对象模型(BDOM)
图字Order订单Audit审计
为了更好的说明我们来列出我们的域中的一些非功能性需求(NonFunctional RequirementNFR)
业务对象需要保存在一个数据库中(appfuse)
审计时要登录到另一个数据库中(appfuse)出于安全的考虑数据库要有防火墙保护
业务组件应该可以重用
必须尽一切努力审计业务服务层的所有活动
考虑了以上要求之后我们决定OrderListManager服务会将所有的审计日志调用委托给已经可用的AuditManager组件这样就得出了详细设计如图所示
图 组件服务的设计
这里值得注意的一点是由于我们的NFR我们要将与OrderListManager相关的对象映射到appfuse数据库而将与审计相关的对象映射到appfuse这样无论要审计什么OrderListManager组件都会调用AuditManager组件我们会看到OrderListManager组件中的所有方法都应该是事务性的因为我们通过服务来创建订单和线项目(line item)那么AuditManager组件中的服务呢?因为它做的是审计跟蹤我们关心的是尽可能维持长时间的审计跟蹤并针对系统中所有可能的业务活动这就产生了如下的需求即使主要的业务活动失败了也要进行审计跟蹤记录AuditManager组件同样要有自己的事务因为它也与自己的数据库进行交互如下所示
<beans>
<! other code goes here >
<bean id=auditManager
class=orgspringframeworktransaction
interceptorTransactionProxyFactoryBean>
<property name=transactionManager>
<ref local=transactionManager/>
</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 = orderListDAOcreateOrderList(orderList);
auditManagerlog(new AuditObject(ORDER + orderId CREATE));
return orderId;
}
public void addLineItem
(Long orderId LineItem lineItem)
throws FacadeException{
Long lineItemId = orderListDAOaddLineItem(orderId lineItem);
auditManagerlog(new AuditObject(LINE_ITEM + lineItemId CREATE));
int numberOfLineItems = orderListDAO
queryNumberOfLineItems(orderId);
if(numberOfLineItems > ){
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中调用auditManagerlog()方法的您应该也注意到了为上述方法所指定的事务属性
<bean id=orderListManager
class=orgspringframeworktransaction
interceptorTransactionProxyFactoryBean>
<property name=transactionAttributes>
<props><prop key=createOrderList>
PROPAGATION_REQUIRED
</prop>
<prop key=addLineItem>
PROPAGATION_REQUIREDcom
exampleexceptionFacadeException
</prop>
</props>
</property>
</bean>
<bean id=auditManager class=org
springframeworktransactioninterceptor
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时如果已经运行了一个TXbean方法就会加入到该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属性的原因如果对下述方法的调用成功了
auditManagerlog(new AuditObject(LINE_ITEM +lineItemId CREATE));
auditManagerlog()就将在新的TX上下文中执行而且只要auditManagerlog()本身成功(即没有抛出异常)新的上下文就会被提交
设置演示环境
准备演示环境时我参考了Spring Live这本书的流程
下载并安装以下组件这时请注意使用准确的版本不然就会引起版本不兼容问题
JDK ___或更高版本
Apache Tomcat
Apache Ant
Equinox
在系统中设置以下环境变量
JAVA_HOME
CATALINA_HOME
ANT_HOME
把下列目录添加到您的PATH环境变量中或者使用完全路径来执行脚本
JAVA_HOMEin
CATALINA_HOMEin
ANT_HOMEin
要设置Tomcat在文本编辑器中打开/conf/tomcatusersxml文件验证以下各行是否存在如果不存在必须手动添加进去
<role rolename=manager/> <user username=admin
password=admin roles=manager/>
要创建基于StrutsSpring和Hibernate的Web应用程序必须用Equinox来构建一个基本的框架程序(barebones starter application)它将包含预定义的文件夹结构所有需要用到的jar文件以及Ant构建脚本把Equinox解压到一个文件夹中它将创建一个equinox文件夹将目录更改为equinox文件夹输入命令ANT_HOMEinnt new Dappname=myusers这样就会创建一个与equinox同级的文件夹myusers该文件夹的具体内容如下
图 Equinox的myusers应用程序文件夹模板
删除myuserswebWEBINF文件夹下的所有xml文件
复制equinoxxtrasstrutswebWEBINFibstruts*jar文件至myuserswebWEBINFib文件夹下这样这个示例应用程序就可以利用struts了
从参考资料小节的示例代码中解压myusersextrazip到一个合适的位置将目录更改为新创建的myusersextra文件夹复制myusersextra文件夹中的所有内容并将它们粘贴到myusers文件夹
打开命令提示符将目录转至myusers目录下执行CATALINA_HOMEinstartup要从myusers文件夹启动Tomcat这一点非常重要否则数据库将不会创建在myusers文件夹中从而导致在执行一些定义在buildxml中的任务时出现错误
再次打开命令提示符并将目录转至myusers目录下执行ANT_HOMEinnt install这将构建应用程序并把它部署到Tomcat中这时我们可以看到myusers中多了一个db目录以便存放数据库appfuse和appfuse
打开浏览器并验证myusers应用程序已经部署//localhost/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 orderList = new OrderList();
Long orderId = orderListManager
createOrderList(orderList);
log(Created OrderList with id
+ orderId + );
orderListManageraddLineItem(orderIdlineItem);
orderListManageraddLineItem(orderIdlineItem);
第二部分执行类似的操作但是这次我们试图向订单添加三个线项目这将产生一个异常
OrderList orderList = new OrderList();
Long orderId = orderListManager
createOrderList(orderList);
log(Created OrderList with id + orderId + );
orderListManageraddLineItem(orderIdlineItem);
orderListManageraddLineItem(orderIdlineItem);
//We know we will have an exception herestill want to proceedtry{
orderListManageraddLineItem
(orderIdlineItem);
}
catch(FacadeException facadeException){
log(ERROR : + facadeExceptiongetMessage());
}
控制台的输出如图所示
图 客户端控制台输出
我们创建了Order并向其添加了两个ID为和的线项目然后我们创建Order并尝试添加个项目前两个(ID为和)添加成功但是图显示添加第三个项目(ID为)时业务方法遇到了异常因此业务方法TX被回滚数据库中没有ID为的线项目从控制台执行以下命令就可以通过图和图进行验证
CATALINA_HOMEinnt browse
图 appfuse数据库中创建的订单
图 appfuse数据库中创建的线项目
在接下来的也是最重要的演示部分中可以看出订单和线项目保存在appfuse数据库中而审计对象保存在appfuse数据库中实际上OrderListManager中的服务方法可以与多个数据库交互启动appfuse数据库查看审计跟蹤如下所示
CATALINA_HOMEinnt browse
图 创建到appfuse数据库中的审计跟蹤包括失败TX的记录项
表最后一行尤其值得注意RESOURCE列显示这一行对应的是LineItem但是当我们回过来看图时却发现并没有对应于LineItem的线项目哪里出错了呢?事实上并没有出错图没有的那一行其实正是这篇文章的关键所在让我们来看看是怎么回事
我们知道addLineItem()方法包含PROPAGATION_REQUIRED属性而log()方法有PROPAGATION_REQUIRES_NEW属性而且addLineItem()在内部调用了log()方法因此当我们试图向Order添加第三个线项目时就(按照我们的业务规则)引发了异常于是这个线项目的创建以及将其链接到Order的操作都被回滚了但是因为还从addLineItem()中调用了log()还因为log()具有PROPAGATION_REQUIRES_NEW TX属性回滚addLineItem()将不会造成回滚log()因为log()是在一个新的TX中执行
让我们对log()的TX属性做一下改动把PROPAGATION_REQUIRES_NEW替换为PROPAGATION_SUPPORTSPROPAGATION_SUPPORTS属性允许服务方法在客户端有TX上下文时在客户端TX中运行如果客户端没有TX就不用TX而直接运行您可能必须重新安装应用程序以便数据库中已经可用的数据可以自动刷新重新安装请按照设置演示环境中的步骤进行
如果再次运行我们会发现一点不同这次在试图向Order添加第三个线项目时依然有异常这将回滚试图添加第三个线项目的事务而这个方法又调用了log()方法但是由于它的PROPAGATION_SUPPORTS TX属性log()将在与addLineItem()方法相同的TX上下文中调用由于addLineItem()回滚log()也回滚了没有留下回滚的TX的审计跟蹤所以在图中没有对应于失败TX的审计跟蹤记录项!
图 创建在appfuse数据库中的审计跟蹤没有失败TX的记录项
我们改动的仅仅是Spring配置文件中的TX属性就产生了这样不同的事务行为如下所示
<bean id=auditManager class=orgspringframeworktransactioninterceptorTransactionProxyFactoryBean>
<property name=transactionAttributes>
<props>
<! prop key=log>
PROPAGATION_REQUIRES_NEW
</prop >
<prop key=log>
PROPAGATION_SUPPORTS
</prop>
</props>
</property>
</bean>
这就是声明式事务管理的效果自从EJB出现以来我们就一直在使用这种方法但是我们需要一个高端的应用服务器来驻留EJB组件现在我们可以看到利用Spring没有EJB服务器也可以达到类似的结果
结束语这篇文章重点介绍了JEE领域的强大组合之一Spring和Hibernate利用二者的功能现在对于容器管理持久性(ContainerManaged PersistenceCMP)容器管理关系(ContainerManaged RelationshipsCMR)和声明式事务管理我们多了一种技术选择虽然Spring不能视为EJB的替代方案但是它提供的许多功能例如普通Java对象的声明式事务管理使得在许多项目中没有EJB也完全可以
本文的目的不是要寻找EJB的替代方案而是为当前的问题找出一个最可行的技术方案至于Spring和Hibernate的轻量级组合的更多功能就留给我们的读者去探索了