UML软件工程组织

在Spring中集成Hibernate事务
出处: 天极网 作者: 陶刚
  本文试图解释如何使用Spring来集成组件(包括组件的事务关系)。在J2EE应用程序中,连接到单个存储数据没有什么困难。但是一旦要求集成企业级组件的时候,情况就复杂了。一个组件一般会受到一个或多个存储数据的支持,因此当我们提到集成一个或多个组件的时候,我们就认为需要跨越多个组件、维护多个数据存储中的原子操作。J2EE服务器为这些组件提供了一个容器,这个容器可以管理这些事务性原子操作和跨组件的隔离。如果我们没有使用J2EE服务器,Spring可以帮助我们。Spring在集成组件服务和它们相关的事务关系的时候,是基于控制倒置(Inversion of Control)的。

  集成(Assembling)组件事务

  假设在我们的企业组件库中,已经拥有了一个审计(audit)组件,客户端可以调用它的服务方法。后来,当我们希望建立一个订单处理系统的时候,我们发现了一个设计需求:OrderListManager组件服务也需要审计组件服务。OrderListManager建立和管理订单,因此所有的OrderListManager服务都有自己的事务属性。当我们在OrderListManager服务内部调用审计组件的时候,会把OrderListManager服务的事务关系(context)传递到审计服务中。也许在未来某个时候,某个新的业务服务组件也需要审计组件服务,但是该审计服务将会在一个完全不同的事务关系中被调用。其实际结果是,虽然审计组件的功能仍然没有变化,但是它可以与其它的业务服务功能组合使用,使用混合和匹配(mix-and-match)的事务属性来提供不同的运行时(run time)事务行为。

  图1显示了两个相互独立的调用关系流。在流1中,如果客户端拥有TX关系,OrderListManager要么参与它,要么启动一个新的TX,这依赖于Client是否在某个TX中,以及为OrderListManager方法提供了什么样的TX属性。OrderListManager服务接下来调用AuditManager方法的时候,这样的解释也是正确的。


图1:集成组件事务

  EJB架构通过允许组件集成器宣告式地(declaratively)提供正确的事务属性来实现这种灵活性。我们没有研究宣告式事务管理的替代物(称为编程式事务控制),因为它涉及到改变代码来影响不同的运行时事务行为。几乎所有的J2EE应用程序服务器都按照X/Open XA规范提供了分布式的事务管理器来适应两步提交(Two-Phase Commit)协议。现在的问题是,在EJB服务器之外,我们能利用相同的功能吗?Spring就是一个替代解决方案。让我们来看看Spring是如何帮助我们解决事务集成问题的。

  使用Spring进行事务管理

  我们将看到一个轻量级的事务架构,它能够管理组件级的事务集成。Spring就是一个解决方案,它的优势在于它没有像JNDI 数据源那样嵌入到J2EE容器服务中。还有一点值得注意,如果我们希望把这个轻量级的事务架构插入到已有的J2EE容器中,也没有任何困难。看起来它在两者之间的平衡性方面做得很好。

  另一方面,Spring 轻量级事务架构还使用了面向方面编程(AOP)框架。Spring AOP框架组件使用了激活了AOP的Spring bean工厂。通过在组件服务层(在一个Spring特定的配置文件applicationContext.xml中)指定事务特性,就把各种事务划分开来了。

<beans>

<!-其它一些代码... -->

<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显示了业务域对象模型(BDOM)中能够识别出来的主要对象:


图2:业务域对象模型(BDOM) 

  为了演示目的,我们列举了域中对象的一些非功能性的需求(NFR):

  · 业务对象必须保存在数据库中appfuse1。

  · 审计需要记录到一个独立的数据库appfuse2中,为了安全,它必须放在防火墙后面。

  · 业务组件应该能重复使用。

  · 必须审计业务服务层的每次尝试的所有活动。

  考虑到上面一些需求,我们决定OrderListManager服务将代理任何对已有的AuditManager组件的审计日志调用。这就形成了图3所示的详细设计:


图3:组件服务的设计 

  这儿要重点注意的是,由于NFR(非功能性需求)的约束,我们把与OrderListManager(订单管理)相关的对象映射到了appfuse1数据库,把与Audit(审计)相关的对象映射到了appfuse2。接着,为了达到审计的目的,OrderListManager组件调用AuditManager组件。我们都认为OrderListManager组件中的方法是事务性的,因为我们使用这个服务来建立订单和条目。那么AuditManager组件中的服务是什么样的呢?由于我们认为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 + ".");
  }
 }

 //其它代码...
}


  为了演示的目的,我们建立了一个异常处理模块,引入了另外一条业务规则:一个订单不能包含两个以上订单项。我们现在应该注意到在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等同于EJB中的TX_REQUIRED,PROPAGATION_REQUIRES_NEW等同于EJB中的TX_REQUIRES_NEW。如果我们希望服务方法一直在事务中运行,就可以使用PROPAGATION_REQUIRED。我们使用PROPAGATION_REQUIRED的时候,如果某个TX已经在运行中,那么bean方法加入那个TX,否则Spring轻量级TX管理器将为你重新启动一个。如果我们希望在组件服务被调用的时候,一般情况下启动新事务,那么就可以使用PROPAGATION_REQUIRES_NEW属性了。

  我们还说明了,如果addLineItem方法产生FacadeException类型的异常,它就应该回滚(roll back)事务。这是另外一种粒度(granularity)层次,通过它我们可能细微地控制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的步骤:

  1.下载和安装下面的组件。在操作的时候,要注意准确的版本号,否则可能遇到版本不匹配的问题。

  ·JDK 1_5_0_01 以上版本

  ·Apache Tomcat 5.5.7

  ·Apache Ant 1.6.2

  ·Equinox 1.2

  2.在系统中设置下面一些环境变量:

  ·JAVA_HOME

  ·CATALINA_HOME

  ·ANT_HOME

  3.给PATH环境变量添加下面一些内容,或者使用完整路径执行脚本:

  ·JAVA_HOME\bin

  ·CATALINA_HOME\bin

  ·ANT_HOME\bin

  4.为了设置Tomcat,在文本编辑器中打开$CATALINA_HOME/conf/tomcat-users.xml文件,检查下面的行是否存在。如果不存在,就加上:
<role rolename="manager"/>
<user username="admin" password="admin" roles="manager"/>

  5.建立一个基于Struts、Spring和Hibernate的web应用程序,我们把Equinox作为初始应用程序框架--它拥有预定义的文件夹结构、所有需要的.jar文件,以及Ant建立脚本。把Equinox解压到一个文件夹中,这个过程会建立一个equinox文件夹。把当前目录切换到equinox文件夹,输入ANT_HOME\bin\ant new -Dapp.name=myusers命令。它会在equinox的同一层次建立一个叫做myusers的文件夹。它的内容如下所示:


图4:Equinox myusers应用程序文件夹模板

  6.删除myusers\web\WEB-INF文件夹中所有的.xml文件。

  7.把equinox\extras\struts\web\WEB-INF\lib\struts*.jar文件复制到myusers\web\WEB-INF\lib文件夹中,这样示例应用程序也支持Struts了。

  8.把示例代码中的myusersextra.zip解压到某个目录中。把当前目录切换到新建立的myusersextra文件夹。复制myusersextra文件夹中的所有内容,粘贴(或覆盖)到myusers文件夹中。

  9.打开命令行提示符并把当前目录切换到myusers文件夹。执行CATALINA_HOME\bin\startup。从myusers文件夹中启动Tomcat是很重要的,否则数据库会被建立在myusers文件夹的外面,会导致我们在执行build.xml中定义的某些事务的时候出错。

  10.打开第二个命令提示符,把当前目录切换到myusers。执行ANT_HOME\bin\ant install。这个过程会建立应用程序并把它部署到Tomcat中。执行操作的时候,我们可能注意到在myusers中建立了一个db目录,用于存放数据库appfuse1和appfuse2。

  11.打开浏览器,验证myusers应用程序是否部署在http://localhost:8080/myusers/了。

  12.如果需要重新安装应用程序,需要执行ANT_HOME\bin\ant remove,接着通过执行CATALINA_HOME\bin\shutdown来关闭Tomcat。接下来删除CATALINA_HOME\webapps文件夹中任意的myusers文件夹。接着通过执行CATALINA_HOME\bin\startup重新启动Tomcat,通过执行ANT_HOME\bin\ant install安装应用程序。

  运行示例

  为了运行测试示例,我们在myusers\test\com\example\service中提供了一个JUnit测试类OrderListManagerTest。要执行它,请在我们建立应用程序的命令行上执行下面的命令:

CATALINA_HOME\bin\ant 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);
//我们知道此处会产生一个异常
try{
 orderListManager.addLineItem(orderId2,lineItem5);
}
catch(FacadeException facadeException){
 log("ERROR : " + facadeException.getMessage());
}

  控制台打印的输出信息如图5所示:


图5:客户端的控制台输出信息

  我们建立了Order1,并添加了订单项ID为1和2的订单项。接着我们建立了Order2,并试图给它添加三个订单项。添加前两个订单项(订单项ID分别是3和4)是成功的,但是图5显示,当我们试图给Order2添加第三个订单项(ID为5)的时候,业务方法遇到了异常。因此,业务方法TX被回滚了,订单项ID为5的订单项不会保存到数据库中。这在图6和图7中可以证实,从控制台上执行下面的命令可以看到这两个图:
CATALINA_HOME\bin\ant browse1


图6:appfuse1数据库中的订单


图7:appfuse1数据库中建立的订单项

  下一步,也是最重要的,示例显示订单和订单项保存在appfuse1数据库中,而审计对象保存在appfuse2数据库中。实际上,OrderListManager中的服务方法与多个数据库交互。用下面的命令打开appfuse2数据库可以看到审计轨迹,如图8所示:
CATALINA_HOME\bin\ant browse2


图8:appfuse2数据库中建立的审计轨迹,包含了失败的TX条目

  图8中的最后一行需要特别注意。RESOURCE数据列表明"它与LineItem5对应"。但是如果我们回头看图7会发现,没有与LineItem5对应的订单项。这儿出错了吗?实际上,没有出现任何错误,图8中这额外的一行也是本文全部内容所解释的部分。我们现在讨论发生了什么情况。
我们知道addLineItem()拥有PROPAGATION_REQUIRED,log()方法拥有PROPAGATION_REQUIRES_NEW。此外,addLineItem()内部调用了log()方法。因此,当我们试图给Order2添加第三个订单项的时候,就引发了异常(根据业务规则),它将回滚这个订单项的建立和链接操作。但是,由于log()也是在addLineItem()中调用的,并且由于log()拥有PROPAGATION_REQUIRES_NEW TX属性,addLineItem()的回滚并不会回滚log(),因为log()在新TX中发生。

  我们现在对log()的TX属性做一些修改。我们不使用PROPAGATION_REQUIRES_NEW,而是把它改变为PROPAGATION_SUPPORTS。PROPAGATION_SUPPORTS属性允许服务方法在客户端TX中运行(如果该客户端拥有TX上下文关系),否则该方法会不带TX运行。你可能需要重新安装应用程序,这样数据库中已有的数据就可以被清除了。如果要重新安装,请查看前面部分中的第12步。

  如果我们重新运行,我们将体验到稍微的不同。这次,当我们试图给Order 2添加第三个订单项的时候也碰到了异常。它会回滚(试图添加第三个订单项的)事务。接着这个方法调用了log()方法。但是,由于log()方法的TX属性为PROPAGATION_SUPPORTS,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组件。现在我们知道即使没有EJB服务器,使用Spring,我们也可以看到类似的结果。

  总结

  本文为J2EE世界中的两个强者:Spring和Hibernate之间的结合提供了一条光明大道。通过提升两者的能力,我们拥有了用于容器管理的持久性(CMP)、容器管理的关系(CMR)和宣告式事务管理的替代技术。即使Spring并非设计为替代EJB的,但是它提供的特性,例如无格式java对象的宣告式事务管理,也使用户在很多项目中可以省去EJB。

  找到EJB的替代物并非本文的目标,但是我们试图找到解决手头问题的最可行的技术方案。因此,我们需要进一步研究Spring和Hibernate这个轻量级组合的能力,这也是读者未来需要探究的主题。

 

版权所有:UML软件工程组织