在Spring中配置Hibernate事务
图1. 装配组件事务
EJB体系通过装配者声明正确的事务属性来获得这种适应性。我们不是在探讨是否声明事务管理,因为这会使运行时的事务参数代码发生改变。几乎所有的J2EE工程提供了分布的事务管理来配合提交协议例如X/Open XA specification。
现在的问题是我们能不能不用EJB来获得相同的功能?Spring是其中一种解决方案。来看一下Spring如何处理这样的问题:
用Spring来管理事务
我们将看到的是一个轻量级的事务机制,实际上,它可以管理组件层的事务集成。Spring就是如此。它的优点是我们可以不用捆绑在J2EE的服务例如JNDI数据库。最棒的是如果我们想把这个事务机制与已经存在的J2EE框架组合在一起,没有任何问题,就好像我们找到了杠杆中完美的支撑点一样。
Spring的另一个机制是使用了AOP框架。这个框架使用了一个可以使用AOP的Spring bean factory。在Spring特定的配置文件applicationContext.xml里通过特定的组件层的事件来指定。
<beans>
<!-- other code goes here... -->
<bean id="orderListManager"
??????? alt="在Spring中配备Hibernate事务" src="/img/2012/09/23/1550357115.jpg">
图 2. 业务领域对象模型 (BDOM)
为了用实例说明,我们来列出工程里的非功能需求(NFR):
---事务在数据库appfuse1里保存。
---审核时要登入到另一个数据库appfuse2里,出于安全的考虑,数据库有防火墙保护。
---事务组件可以重用。
---所有访问事件必须经过在事务服务层的审核。
出于以上的考虑,我们决定了OrderListManager 服务将委托任何审核记录来调用已有的AuditManager 组件.这产生了表3这样更细致的结构:
图 3. 组件服务结构设计
值得注意的是,由于我们的NFR,我们要映射OrderListManager相关的事物到appfuse1 数据库里去,而审核相关的到appfuse2。这样,任何审核的时候 OrderListManager 组件都会调用AuditManager 组件。我们认为OrderListManager 组件里的所有方法都要执行, 因为我们通过服务来创建次序和具体项目。那么AuditManager 组件里的服务呢? 因为它做的是审核的动作,我们关心的是为系统里所有的事务记录审核情况。这样的需求是,“即使事务事件失败了,我们也要记录登录的审核情况”。AuditManager 组件同样要有自己的事件,因为它同样与自己的数据库有关联。如下所示:
<beans>
<!—其他代码在这里-->
<bean id="auditManager"
??????? + 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"
??????? 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 到相应位置。 把目录下的所有内容拷贝到myusers目录下。
9. 打开命令行转到myusers目录下。输入CATALINA_HOME\bin\startup 从myusers 目录启动Tomcat可以保证数据库在myusers 文件夹里创建,这样可以避免在运行build.xml里定义的任务发生错误。
10. 再次打开命令行转到myusers。执行 Execute 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重构工程。
执行工程
如果要测试,OrderListManagerTest,在myusers\test\com\example\service 目录下可以运行作为JUnit测试。要运行的话,在构建工程时加入以下代码:
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, 试图添加3个项目,前两个(ID是3和4)成功了,如图5所示添加ID为5的项目时抛出了异常。然后,事务回滚,数据库里没有ID为5的项目。执行以下代码从图6和图7可以看出:
CATALINA_HOME\bin\ant browse1
图 6. appfuse1 数据库里的排列
?
图 7. appfuse1里的项目
接下来,试验中可以看出次序和项目存在appfuse1 里,而审核部分在appfuse2里. OrderListManager 同时访问两个数据库。打开 appfuse2 数据库,看审核记录的细节:
CATALINA_HOME\bin\ant browse2
图 8. appfuse2数据库里的审核记录, 包括失败的TX
表8最后一列尤其值得注意,RESOURCE这一栏上显示这一行对应着LineItem5。 但是当我们回过来看图7,却发现并没有这种对应。这是个错误吗?事实上,没有问题,图7里没有的那行其实是这篇文章的精华所在,让我们来看看是怎么回事。
首先addLineItem() 方法有 PROPAGATION_REQUIRED 属性而 log() 方法有PROPAGATION_REQUIRES_NEW。进而, addLineItem() 在内部调用log() 方法。所以我们往第二个排列里添加第三个表项时,发生了异常 (由于我们的事务规则),就将这个创建过程和链接都回滚了。但是,因为已经调用了log(),而log()有PROPAGATION_REQUIRES_NEW TX 属性,回滚了addLineItem() 不会回滚 log(), 因为 log() 是在一个新的TX里。
让我们现在改变一下log()的TX属性。把PROPAGATION_REQUIRES_NEW 替换成PROPAGATION_SUPPORTS。ROPAGATION_SUPPORTS 属性允许方法在客户端的TX里运行,如果客户端有TX,否则就不用TX。你需要重建工程让这些变化自动被刷新。请按照设置工程环境的第12步。
重新开始的话,我们会发现有一点不同。这次,我们在往排列2添加第三项时依然有异常。发生回滚。这时方法调用了log()方法。由于它有着PROPAGATION_SUPPORTSTX属性, log() 将在同一个addLineItem() 方法环境下调用。由于 addLineItem() 回滚,log() 也回滚了,没有留下审核记录。所以在图9里没有这项失败的记录!
Figure 9. appfuse2数据库的审核记录,没有失败的TX
我们所改变的仅仅是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以来就开始寻找杠杆的最佳位置。我们需要一个高端的应用服务器 来管理我们的our EJB组件。现在我们不用EJB服务器就达到了一样的结果,用Spring。
这篇文章介绍了J2EE里十分强大的组合之一:Spring 和 Hibernate。通过两者的有机结合,我们现在多了对Container-Managed Persistence (CMP), Container-Managed Relationships (CMR), 和生成实例管理的新选择。虽然Spring不能完全代替EJB,但是它提供了很多功能,例如一般Java程序的实例生成,使得用户可以在大部分工程中和EJB搭配使用。
我们不是要为了寻找EJB的代替品,而是对于现在的问题得出一个最理想的解决方案。我们仍然要寻找Spring 和 Hibernate组合的更多优点,这就留给我们的读者去探索了。