詳解阿里P7架構師是怎麼在Spring中實現事務暫停
摘要
Spring框架是一個流行的基於輕量級控制反轉容器的Java/J2EE應用框架,尤其在資料訪問和事務管理方面的能力是眾所周知的。Spring的宣告性事務分離可以應用到任何POJO目標物件,並且包含所有EJB基於容器管理事務中的已宣告事務。後臺的事務管理器支援簡單的基於JDBC的事務和全功能的基於JTA的J2EE事務。
這篇文章詳細的討論了Spring的事務管理特性。重點是如何在使用JTA作為後臺事務策略的基礎上讓POJO利用Spring的宣告性事務,這也顯示了Spring的事務服務可以無縫地與J2EE伺服器(如BEA WebLogic Server的事務協調器)的事務協調器進行互動,作為EJB CMT傳統事務分離方式的一個替代者。
POJO的宣告性事務
作為Spring宣告性事務分離方式的樣例,讓我們來看一下Spring的樣例應用PetClinic的中心服務外觀中的配置:
清單1:
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/jdbc/petclinic</value> </property> </bean> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/> <bean id="clinicTarget" class="org.springframework.samples.petclinic.jdbc.JdbcClinic"> <property name="dataSource"><ref bean="dataSource"/></property> </bean> <bean id="clinic" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="transactionManager"><ref bean="transactionManager"/></property> <property name="target"><ref bean="clinicTarget"/></property> <property name="transactionAttributes"> <props> <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop> </props> </property> </bean>
他遵循Spring的標準XMLBean定義格式。定義了:
- 一個DataSource引用,指向一個JNDI位置—在J2EE伺服器管理下這將從JNDI環境中獲取特定的DataSource。
- 一個應用服務實現—這是一個POJO,封裝了業務和資料訪問邏輯。在這裡實現了應用中的Clinic服務介面。
- 一個應用服務的事務代理—這個代理為目標服務定義了事務屬性,匹配特定的方法名模式併為之建立相應的事務。在實際的事務管理中,代理指向一個PlatformTransactionManager實現。
注意:除了顯式的代理定義,Spring還支援自動代理機制和通過Commons Attributes或J2SE 5.0註解實現源程式級的元資料使用。這些可選方法的討論超過了本文的範圍。可以參考Spring的文件來了解相關細節。
業務介面和業務實現是特定於應用的並且不需要關心Spring或者Spring的事務管理。普通Java物件可以作為服務的目標物件,而且任何普通Java介面可以作為服務的介面。下面是一個Clinic介面的示例:
清單2:
public interface Clinic { Pet loadPet(int id); void storePet(Pet pet); ... }
這個介面的實現如下顯示,假設他使用JDBC來執行必要的資料訪問。他通過bean屬性的設定方法來獲取JDBC的DataSource;這與上面的配置中的dataSource屬性定義相對應。
清單3:
public class JdbcClinic implements Clinic { private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } public Pet loadPet(int id) { try { Connection con = this.dataSource.getConnection(); ... } catch (SQLException ex) { ... } } public void storePet(Pet pet) { try { Connection con = this.dataSource.getConnection(); ... } catch (SQLException ex) { ... } } ... }
如你所見,程式碼相當直接。我們使用一個簡單的Java物件,而事務管理由事務代理來處理,這個我們會在下面討論。
注意在PetClinic示例應用中實際的基於JDBC的Clinic實現利用了Spring的JDBC支援類來避免直接使用JDBC的API。雖然Spring的事務管理也可以與普通的基於JDBC實現一起工作,就向上面的示例。
定義事務代理
除了JdbcClinic例項以外,配置中也定義了一個事務代理。如果願意這個代理所暴露的實際介面也可以顯式定義。預設情況下,所有由目標物件實現的介面都暴露出來,在這個例子中就是應用的Clinic服務介面。
從客戶端的觀點來看,"clinic" bean只是這個應用的Clinic介面的實現。客戶端不需要知道這會被一個事務代理所處理。這就是介面的能力:一個直接的目標物件的引用可以容易的被一個實現相同介面的代理所代替—在這兒就是一個隱式建立事務的代理。
代理的具體事務行為會由為根據特定的方法或方法命名模式而定義的事務屬性來驅動,就像下面的例子所示:
清單3:
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop> <prop key="store*">PROPAGATION_REQUIRED</prop>
Key屬性決定代理將為方法提供什麼樣的事務行為。這個屬性的最重要部分就是事務傳播行為。下面是一些可選的屬性值:
- PROPAGATION_REQUIRED --支援當前的事務,如果不存在就建立一個新的。這是最常用的選擇。
- PROPAGATION_SUPPORTS --支援當前的事務,如果不存在就不使用事務。
- PROPAGATION_MANDATORY --支援當前的事務,如果不存在就丟擲異常。
- PROPAGATION_REQUIRES_NEW --建立一個新的事務,並暫停當前的事務(如果存在)。
- PROPAGATION_NOT_SUPPORTED --不使用事務,並暫停當前的事務(如果存在)。
- PROPAGATION_NEVER --不使用事務,如果當前存在事務就丟擲異常。
- PROPAGATION_NESTED --如果當前存在事務就作為嵌入事務執行,否則與PROPAGATION_REQUIRED類似。
前6個事務策略與EJB的CMT類似,而且使用相同的常量名,因此對EJB開發人員來說是很親切的。第7個策略PROPAGATION_NESTED是Spring提供的一個變體:他需要事務管理器(如DataSourceTransactionManager)提供類似JDBC3.0那樣的儲存點API來巢狀事務行為或者通過
JTA支援巢狀事務。
事務屬性中的readOnly標識指示相應的事務應該作為一個只讀事務來優化。這是一個優化提示:一些事務策略在這種情況下可以得到很好的效能優化,如使用ORM工具如Hibernate或TopLink時避免髒資料檢查(“flush”嘗試)。
在事務屬性中還有一個“timeout”選項來定義事務的超時秒數。在JTA中,這個屬性會簡單地傳遞給J2EE伺服器的事務協調器並被正確地解釋。
使用事務代理
在執行時,客戶端會取得一個“clinic”引用並轉換為Clinic介面,然後呼叫如loadPet或storePet方法。這就隱式地使用了Spring的事務代理,通過“事務直譯器”在目標物件中註冊;這樣一個新的事務就建立了,然後具體的工作就會代理給JdbcClinic的目標方法。
圖1示例了一個使用“建議鏈”併到達最後目標的AOP代理的潛在概念。在這個示例中,唯一的建議是一個事務直譯器用來包裝目標方法的事務行為。這是一種用來在宣告性事務功能下使用的基於代理的AOP。
Figure 1. An AOP proxy with an advisor chain and a target at the end
例如,一個PetClinic應用的WEB層元件可以執行ServletContext定位來獲取Spring WebApplicationContext的引用並且獲取受管理的“clinic”BEAN:
清單4:
WebApplicationContext ctx = WebApplicationContexUtils.getWebApplicationContext(servletContext); Clinic clinic = (Clinic) ctx.getBean("clinic); Pet pet = new Pet(); pet.setName("my new cat"); clinic.storePet(pet);
在呼叫storePet()之前,Spring的事務代理隱式地建立一個事務。當storePet()呼叫返回時,事務將提交或回滾。預設情況下任何RuntimeException或Error將導致回滾。實際的提交或回滾可以是可以定義的:Spring的事務屬性支援“回滾規則”的概念。
例如,我們可以可以引入一個強制的PetClinicException並且告訴事務代理在丟擲異常時回滾:
清單5:
<prop key="load*">PROPAGATION_REQUIRED,readOnly,-PetClinicException</prop> <prop key="store*">PROPAGATION_REQUIRED,-PetClinicException</prop>
這兒也有一個類似的“提交規則”語法,指示特定的異常將觸發一次提交。
注意上面示例的顯式定位引用的方法只是一種訪問受Spring管理BEAN的方法的變化,可以用在任何WEB資源如servlet或filter。在構建基於Spring自身的MVC框架時,BEAN可以直接被注射到WEB控制器中。當然也支援在如Struts, WebWork, JSF, and Tapestry框架中訪問Spring管理BEAN。詳情可以參考Spring的文件。
PlatformTransactionManager策略
Spring事務支援的核心介面是org.springframework.transaction.PlatformTransactionManager。所有Spring的事務分離功能都會委託給PlatformTransactionManager(傳給相應的TransactionDefinition例項)來做實際的事務執行。雖然PlatformTransactionManager介面可以直接呼叫,但通常應用只需要配置一個具體的事務管理器並且通過宣告性事務來分離事務。
Spring提供幾種不同的PlatformTransactionManager實現,分為如下兩個類別:
- 本地事務策略—支援單一資源的事務(通常是單個數據庫),其包括
org.springframework.jdbc.datasource.DataSourceTransactionManager
和org.springframework.orm.hibernate.HibernateTransactionManager
。 - 全域性事務管理—支援可能跨越多個資源的全域性事務。其相應的類為org.springframework.transaction.jta.JtaTransactionManager,將事務委託給遵循JTA規範的事務協調器(通常為J2EE伺服器,但不是強制的)。
PlatformTransactionManager抽象的主要價值在於應用不再被繫結在特定的事務管理環境。相反,事務策略可以很容易地切換—通過選擇不同的PlatformTransactionManager實現類。這就使得應用程式碼與宣告事務分離保持一致,而不需要考慮應用元件所使用的環境了。
例如,應用的初始版本可能佈署在Tomcat上,與單個Oracle資料庫互動。這可以方便地利用Spring的事務分離特性,只要選擇基於JDBC的DataSourceTransactionManager作為使用的事務策略。Spring會分離事務,而JDBC驅動會執行相應的原始JDBC事務。
相同應用的另一個版本可能會佈署在WebLogic伺服器上,使用兩個Oracle資料庫。應用程式碼和事務分離不需要改變。唯一不同的是選擇作為JtaTransactionManager事務策略,讓Spring來分離事務而WebLogic伺服器的事務協調器來執行事務。
JTA UserTransaction與JTA TransactionManager比較
讓我們來看一下Spring對JTA支援的細節。雖然並非經常需要考慮這個細節但瞭解相關的細節還有必要的。對簡單的用例如前面章節的示例,標準的JtaTransactionManager定義已經足夠了,預設的Spring JtaTransactionManager設定會從標準JNDI位置(J2EE規範所定義的java:comp/UserTransaction)獲取JTA的javax.transaction.UserTransaction物件。這對大部分標準J2EE環境來說已經足夠了。
然而,預設的JtaTransactionManager不能執行事務暫停(也就是說不支援PROPAGATION_REQUIRES_NEW和PROPAGATION_NOT_SUPPORTED)。原因就在於標準的JTA UserTransaction介面不支援事務的暫停和恢復,而只支援開始和完成新的事務。
為了實現事務的暫停,需要一個javax.transaction.TransactionManager例項,他提供了JTA定義的標準的暫停和恢復方法。不幸的是,J2EE沒有為JTA TransactionManager定義標準的JNDI位置!因此,我們需要使用廠商自己的定位機制。
清單6:
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> <property name="transactionManagerName"> <value>vendorSpecificJndiLocation</value> </property> </bean>
J2EE本質上沒有考慮將JTA TransactionManager介面作為公共API的一部分。JTA規範自身定義了將TransactionManager介面作為容器整合的想法。雖然這是可以理解的,但是JTA TransactionManager的標準JNDI位置還是可以增加一定的價值,特別是對輕量級容器如Spring,這樣任何J2EE伺服器就可以用統一的方式來定位JTA TransactionManager了。
不僅Spring的JtaTransactionManager可以從訪問中獲益,O/R對映工具如Hibernate, Apache OJB, and Kodo JDO也能得到好處,因為他們需要在JTA環境中執行快取同步的能力(釋放快取意味著JTA事務的完成)。這種註冊事務同步的能力只有JTA TransactionManager接口才能提供,而UserTransaction是處理不了的。因此,這些工具都需要實現自己的TransactionManager定位器。
為JTA TransactionManager定義標準的JNDI位置是許多底層軟體供應商最期望J2EE實現的功能。如果J2EE5.0的規範制定團隊能夠認識到這個特性的重要性就太好了。幸運地是,高階J2EE伺服器如WebLogic Server已經考慮將JTA TransactionManager作為公共的API包含在擴充套件功能中。
在WebLogic JTA中實現Spring的事務分離
在WebLogic Server中,JTA TransactionManager官方的JNDI位置定義為javax.transaction.TransactionManager。這個值可以在Spring的JtaTransactionManager中作為“transactionManagerName”使用。原則上這樣就可以在WebLogic's JTA系統中實現事務暫停了,也就是說支援PROPAGATION_REQUIRES_NEW
和PROPAGATION_NOT_SUPPORTED
行為。
除了標準的JtaTransactionManager和其支援的通用配置選項外,Spring還提供了一個專用的WebLogicJtaTransactionManager介面卡來直接利用WebLogic的JTA擴充套件。
在享受自動探測WebLogic的JTA TransactionManager的便利之外,他提供超越標準JTA的三個重要特性:
- 事務命名—暴露出Spring的事務名給WebLogic Server,使得Spring事務在WebLogic的事務監聽器可見。預設的,Spring會使用宣告性事務的完整方法名。
- 每事務隔離級別—將Spring事務屬性中定義的隔離級別應用到WebLogic JTA事務中。這使得每個事務都可以定義資料庫的隔離級別,而這是標準JTA所不支援的。
- 強制事務恢復—即使在暫停的事務被標識為回滾時也可以恢復。這需要使用WebLogic的擴充套件TransactionManager介面來呼叫forceResume()方法。
Figure 2. WebLogic Server's transaction monitor (click the image for a full-size screen shot)
Spring的WebLogicJtaTransactionManager有效地為基於Spring的應用提供了WebLogic Server事務管理的全部功能。這使得Spring事務分離成為一種能與EJB CMT竟爭的產品,而且提供了相同級別的事務支援。
Spring and EJB CMT
如上所示,Spring的POJO宣告性事務分離可以作為一種除傳統EJB CMT這外的選擇。但是Spring與EJB並不是完成互斥的,Spring的應用上下文也可以作為EJB façade的後臺來管理資料訪問(DAO)和其他細紋理的業務物件。
在EJB情景中,事務是由EJB CMT來驅動的。對Spring來說,資料訪問支援特性會自動檢測到這樣的環境並且採用相應的事務。例如,Spring對Hibernate的支援能夠提供隱式的資源管理,即使是EJB驅動的事務,甚至可以在不需要修改任何DAO程式碼的情況下提供相同的語義。
Spring有效的解耦了DAO實現與實際的執行環境。DAO可以參與Spring的事務就像參與EJB CMT事務一樣。這不僅簡化在其他環境中的重用,而且更方便在J2EE容器外進行測試。
結論
Spring框架為J2EE和非J2EE環境提供了全量的事務分離的特性,特別表現在POJO的宣告性事務上。他用一種靈活而非侵入式的方式為非EJB環境中的事務分離提供了便利。與EJB不同,這樣的事務性POJO應用物件可以很容易的被測試和在J2EE容器外補重用。
Spring提供了各種事務策略,如JtaTransactionManager是用來代理J2EE伺服器的事務協調器,而JDBC DataSourceTransactionManager是用來為簡單的JDBC DataSource(就是單一目標資料庫)執行事務。Spring可以很容易為不同的環境通過後臺配置的簡單修改來調整事務策略。
超越標準的JTA支援,Spring為WebLogic Server的JTA擴充套件提供了完善的整合,可以支援高階特性如事務監視和每事務隔離級別。通過對WebLogic Server的特殊支援,基於Spring的應用可以完全利用WebLogic Server的事務管理功能。
Spring事務分離是繼EJB CMT之外的另一種可選方式,特別是對那些基於POJO的輕量級架構。在那只是因為選擇LSSB(本地無狀態會話BEAN)來應用宣告性事務的情況下,基於Spring的POJO服務模型是一種可行的選擇,他提供了非常高層的靈活性、可測試性和重用