本文主要探討怎么用Spring來裝配組件及其事務管理。在J2EE工程里連接到一個簡單的數據庫并不是什么難題,但是如果要綜合組裝企業類的組件就變得復雜了。一個簡單的組件有一個或多個數據庫支撐,所以,我們說到整合兩個或多個的組件時,我們希望能夠維持跨組件的許多數據庫的運作的原子性。
J2EE提供了這些組件的容器,可以保證處理的原子性和獨立性。在沒有J2EE的情況下我們可以用Spring。 Spring基于IoC模式(即反轉模式),不僅可以配置組件服務,還可以配置相應的方法。為了更好的實現本文的目的,我們使用Hibernate來做相應的后臺開發。
裝配組件事務
假設在組件庫里,我們已經有一個審核組件(audit component),里面有可以被客戶端調用的方法。接著,當我們想要構建一個處理訂單的體系,我們發現設計需要的OrderListManager組件服務同樣需要審核組件服務。OrderListManager創建和管理訂單,每一個服務都含有自己的事務屬性。當這時調用審核組件,就可以把 OrderListManager的處理內容傳給它。也許將來新的業務服務(business service)同樣需要審核組件,那這時它調用的事務內容已經不一樣了。在網絡上的結果就是,雖然審核的功能保持不變,但是可以和別的事件功能組合在一起,用這些方法屬性來提供不同的運行時的處理參數。
在圖1中有兩個分開的調用流程。在流程1里,如果客戶端含有一個TX內容,OrderListManager 要由一個新的TX開始或者參與其中,取決于客戶端在不在TX里以及OrderListManager方法指定的TX屬性。這在它調用 AuditManager方法的時候仍然適用。

圖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"
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 數據庫里去,而審核相關的到appfuse2。這樣,任何審核的時候 OrderListManager 組件都會調用AuditManager 組件。我們認為OrderListManager 組件里的所有方法都要執行, 因為我們通過服務來創建次序和具體項目。那么AuditManager 組件里的服務呢? 因為它做的是審核的動作,我們關心的是為系統里所有的事務記錄審核情況。這樣的需求是,“即使事務事件失敗了,我們也要記錄登錄的審核情況”。 AuditManager 組件同樣要有自己的事件,因為它同樣與自己的數據庫有關聯。如下所示:
<beans>
<!—其他代碼在這里-->
<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 和 TX_REQUIRED相同,而PROPAGATION_REQUIRES_NEW 和在EJB里的 TX_REQUIRES_NEW 相同。如果我們想讓方法一直在事務里運行,可以用PROPAGATION_REQUIRED。這時,如果有一個TX已經運行了,bean的方法就會加入到 TX里,或者Spring的TX管理器給你新建一個。如果我們想一旦方法被調用,就創建一個新的事務實例,我們可以用 PROPAGATION_REQUIRES_NEW 屬性。
我們同樣要讓addLineItem 一直都在拋出FacadeException異常時回滾事務。在我們有異常的情況下,使得我們可以很好的控制TX結束達到了另一個級別。前綴-符號表示回滾TX,而+ 符號表示提交TX。
接下來的問題是為什么我們給log方法設置一個PROPAGATION_REQUIRES_NEW呢?這是因為我們要做的是無論主函數怎么運行,我們都要為所有訂單的創建和項目的添加記錄審核情況。就是說即使在運行createOrderList 和 addLineItem 的時候拋出了異常也要記錄。這僅在我們開始一個新的TX并調用log的時候起作用。這就是為什么要給它設置的原因:
如果調用auditManager.log(new AuditObject(LINE_ITEM +
lineItemId, CREATE));成功了, auditManager.log() 將在新的TX里發生,這樣auditManager.log() 成功的話就會被提交 (前提是不拋出異常)。
設置試驗工程的環境
為了運行這個工程,我們來按Spring Live 這本書的流程進行:
1. 下載并安裝以下組件,注意版本,不然會引起一系列的問題
o JDK 1_5_0_01 或更高
o Apache Tomcat 5.5.7
o Apache Ant 1.6.2
o Equinox 1.2
2. 配置系統環境變量:
o JAVA_HOME
o CATALINA_HOME
o ANT_HOME
3. 把下列目錄添加到你的PATH變量中去或者用絕對路徑來運行你的程序:
o JAVA_HOME\bin
o CATALINA_HOME\bin
o 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 到相應位置。 把目錄下的所有內容拷貝到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組合的更多優點,這就留給我們的讀者去探索了。