本文主要探討如何利用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 proceed
try{
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的輕量級組合的更多功能就留給我們的讀者去探索了
From:http://tw.wingwit.com/Article/program/Java/ky/201311/28139.html