top
Loading...
在Spring中集成Hibernate事務

設計:管理多個組件中的事務

現在我們討論一下"集成組件事務"到底是什么意思。你可能注意到了,提供給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_HOMEin

·CATALINA_HOMEin

·ANT_HOMEin

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_HOMEinant new -Dapp.name=myusers命令。它會在equinox的同一層次建立一個叫做myusers的文件夾。它的內容如下所示:


圖4:Equinox myusers應用程序文件夾模板

6.刪除myuserswebWEB-INF文件夾中所有的.xml文件。

7.把equinoxextrasstrutswebWEB-INFlibstruts*.jar文件復制到myuserswebWEB-INFlib文件夾中,這樣示例應用程序也支持Struts了。

8.把示例代碼中的myusersextra.zip解壓到某個目錄中。把當前目錄切換到新建立的myusersextra文件夾。復制myusersextra文件夾中的所有內容,粘貼(或覆蓋)到myusers文件夾中。

9.打開命令行提示符并把當前目錄切換到myusers文件夾。執行CATALINA_HOMEinstartup。從myusers文件夾中啟動Tomcat是很重要的,否則數據庫會被建立在myusers文件夾的外面,會導致我們在執行build.xml中定義的某些事務的時候出錯。

10.打開第二個命令提示符,把當前目錄切換到myusers。執行ANT_HOMEinant install。這個過程會建立應用程序并把它部署到Tomcat中。執行操作的時候,我們可能注意到在myusers中建立了一個db目錄,用于存放數據庫appfuse1和appfuse2。

11.打開瀏覽器,驗證myusers應用程序是否部署在http://localhost:8080/myusers/了。

12.如果需要重新安裝應用程序,需要執行ANT_HOMEinant remove,接著通過執行CATALINA_HOMEinshutdown來關閉Tomcat。接下來刪除CATALINA_HOMEwebapps文件夾中任意的myusers文件夾。接著通過執行CATALINA_HOMEinstartup重新啟動Tomcat,通過執行ANT_HOMEinant install安裝應用程序。

運行示例

為了運行測試示例,我們在myusersestcomexampleservice中提供了一個JUnit測試類OrderListManagerTest。要執行它,請在我們建立應用程序的命令行上執行下面的命令:

CATALINA_HOMEinant 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_HOMEinant browse1


圖6:appfuse1數據庫中的訂單


圖7:appfuse1數據庫中建立的訂單項

下一步,也是最重要的,示例顯示訂單和訂單項保存在appfuse1數據庫中,而審計對象保存在appfuse2數據庫中。實際上,OrderListManager中的服務方法與多個數據庫交互。用下面的命令打開appfuse2數據庫可以看到審計軌跡,如圖8所示:
CATALINA_HOMEinant 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這個輕量級組合的能力,這也是讀者未來需要探究的主題。

作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗