5.Spring的事務
通常情況下,J2EE有2種事務管理方式:全局事務和本地事務,2種事務都比較明顯的缺陷。
全局事務:
全局事務允許跨多個事務資源的事務管理(通常是關系數據庫和消息隊列),應用服務器通過JTA(一個很復雜的api)管理全局事務,此外,一個JTA的事務通常通過JNDI進行資源查找,即如果你想使用JTA就必須連帶使用JNDI。JTA通常只能在應用服務器環境下使用,顯然使用全局事務會限制應用代碼的重用性。
更好的方式是通過EJB CMT提供全局事務管理,CMT是一種聲明式的事務管理。EJB CMT移除了事務相關的JNDI查找,雖然使用EJB本身就需要使用JNDI,但這確實節省了大量的事務管理代碼(并不是全部),重大缺陷是CMT綁定在JTA以及應用服務器環境上,并且你必須選擇EJB來處理業務邏輯。EJB太龐大,并不是一個很吸引人的選擇(僅僅是為了增加全局事務就使用EJB,確實....)。
本地事務:
本地事務則和底層所使用的持久化技術有關(使用JDBC處理持久化,事務管理需要JDBC中的connection對象,使用hibernate處理持久化,事務管理需要hibernate中的session對象),比起全局事務,本地事務更易使用,但是也有明顯的缺陷:1.不能跨事務資源,例如:使用JDBC事務來進行事務管理的代碼在JTA全局事務環境下就不能運行。2.使用本地事務,由于應用服務器不需要參與事務的管理,因此不能保證跨多個事務性資源的事務正確性(不需要特別注意這種情況,大多數應用使用單個事務資源)。3.顯然事務管理和代碼是耦合的,具有侵入性(這也是前面缺陷1的產生原因)。
Spring事務管理:
Spring解決了全局事務和本地事務的缺陷,允許應用開發者在任何環境下使用一致的編程模型。Spring同時支持編程式事務管理和聲明式事務管理。當使用Spring編程式事務管理時,開發者直接使用Spring框架的事務抽象(事務抽象這個翻譯有點拗口,原文為:transaction abstraction,不同具體事務策略的統一抽象接口,面向接口編程),使應用程序可以運行在任何具體的底層事務基礎之上。當使用Spring聲明式事務管理時,只需編寫少量和事務相關的代碼即可(只需編寫一些Spring配置文件),這些代碼和Spring的事務api或任何其他的事務api都沒有耦合。顯然Spring的聲明式事務管理更加簡單易用。
為了使用事務管理是否應該選擇應用服務器?
答案是否定的。
Spring對事務管理的支持改變了企業級Java應用必須要使用應用服務器的傳統觀念。需要特別指出的是,僅僅是為了使用EJB的聲明式事務管理就選擇使用應用服務器是完全沒有必要的(為了使用聲明式事務管理就使用EJB同樣可怕),盡管應用服務器提供了強勁的JTA功能,Spring的聲明式事務管理比起EJB CMT更加給力,并且提供更加高效的編程方式。通常選擇使用應用服務器是為了跨多個事務資源的事務管理,這種情況很少見并且許多高端應用會選擇使用高性能數據庫而不是多個事務資源。也可以選擇一些獨立的事務管理技術,但是大多數情況下這些技術都需要一些應用服務器功能(比如JMS或是JCA)的支持。Spring更符合一次編寫,隨處運行的原則,代價僅僅是修改一些Spring配置文件,而不是大量的重復性編碼工作。
Spring支持的事務策略:
Spring事務策略是通過PlatformTransactionManager接口體現的,該接口是Spring事務策略的核心。
即使使用容器管理的JTA,代碼依然不需要JNDI查找,無需與特定的JTA資源耦合在一起,通過配置文件,JTA資源傳給PlatformTransactionManager的實現類,因此,程序的代碼可以在JTA事務管理和非JTA事務管理之間輕松的切換。
Spring是否支持事務跨多個數據庫資源?
答:Spring完全支持這種跨多個事務性資源的全局事務,前提是底層的應用服務器(如WebLogic、WebSphere、tomcat等)支持JTA全局事務,可以這樣說:Spring本身沒有任何事務支持,它只是負責包裝底層的事務————當我們在程序中面向PlatformTransactionManager接口編程時,Spring在底層負責將這些操作轉換成具體的事務操作代碼,所以應用底層支持什么樣的事務策略,那么Spring就支持什么樣的事務策略。Spring事務管理的優勢是將應用從具體的事務API中分離出來,而不是真正提供事務管理的底層實現。
Spring的具體的事務管理由PlatformTransactionManager的不同實現類來完成,在Spring容器中配置PlatformTransactionManager時必須針對不同的環境提供不同的實現類,下面提供了不同的持久化訪問環境,及其對應的PlatformTransactionManager實現類的配置。
1,BC數據源的局部事務策略的配置文件如下:
<!-- 定義數據源Bean,使用C3P0數據源實現 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定連接數據庫的驅動 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定連接數據庫的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定連接數據庫的用戶名 -->
<property name="user" value="root"/>
<!-- 指定連接數據庫的密碼 -->
<property name="password" value="32147"/>
<!-- 指定連接數據庫連接池的最大連接數 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定連接數據庫連接池的最小連接數 -->
<property name="minPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的初始化連接數 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的連接的最大空閑時間 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 配置JDBC數據源的局部事務管理器,使用DataSourceTransactionManager 類 -->
<!-- 該類實現PlatformTransactionManager接口,是針對采用數據源連接的特定實現-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 配置DataSourceTransactionManager時需要依注入DataSource的引用 -->
<property name="dataSource" ref="dataSource"/>
</bean>
2.器管理JTA全局事務的配置文件如下:
<!-- 配置JNDI數據源Bean-->
<bean id="dateSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<!--容器管理數據源的JNDI-->
<property name="jndiName" value="jdbc/jpetstore"/>
</bean>
<!--使用JtaTransactionManager類,該類實現PlatformTransactionManager接口-->
<!--針對采用全局事務管理的特定實現-->
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
3.采用Hibernate持久層訪問策略時,局部事務的策略的配置文件如下:
!-- 定義數據源Bean,使用C3P0數據源實現 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定連接數據庫的驅動 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定連接數據庫的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定連接數據庫的用戶名 -->
<property name="user" value="root"/>
<!-- 指定連接數據庫的密碼 -->
<property name="password" value="32147"/>
<!-- 指定連接數據庫連接池的最大連接數 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定連接數據庫連接池的最小連接數 -->
<property name="minPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的初始化連接數 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的連接的最大空閑時間 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 定義Hibernate的SessionFactory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 依賴注入數據源,注入正是上面定義的dataSource -->
<property name="dataSource" ref="dataSource"/>
<!-- mappingResouces屬性用來列出全部映射文件 -->
<property name="mappingResources">
<list>
<!-- 以下用來列出Hibernate映射文件 -->
<value>Person.hbm.xml</value>
</list>
</property>
<!-- 定義Hibernate的SessionFactory的屬性 -->
<property name="hibernateProperties">
<props>
<!-- 指定數據庫方言 -->
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQLInnoDBDialect</prop>
<!-- 是否根據需要每次自動創建數據庫 -->
<prop key="hibernate.hbm2ddl.auto">update</prop>
<!-- 顯示Hibernate持久化操作所生成的SQL -->
<prop key="hibernate.show_sql">true</prop>
<!-- 將SQL腳本進行格式化后再輸出 -->
<prop key="hibernate.format_sql">true</prop>
</props>
</property>
</bean>
<!--配置Hibernate局部事務管理器,使用HibernateTransactionManager類-->
<!--該類是PlatformTransactionManager接口針對采用Hibernate的特定實現類-->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
4.底層采用Hibernate持久化技術,但事務依然采用JTA全局事務:
<!-- 定義數據源Bean,使用C3P0數據源實現 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定連接數據庫的驅動 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定連接數據庫的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定連接數據庫的用戶名 -->
<property name="user" value="root"/>
<!-- 指定連接數據庫的密碼 -->
<property name="password" value="32147"/>
<!-- 指定連接數據庫連接池的最大連接數 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定連接數據庫連接池的最小連接數 -->
<property name="minPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的初始化連接數 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的連接的最大空閑時間 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 定義Hibernate的SessionFactory -->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 依賴注入數據源,注入正是上面定義的dataSource -->
<property name="dataSource" ref="dataSource"/>
<!-- mappingResouces屬性用來列出全部映射文件 -->
<property name="mappingResources">
<list>
<!-- 以下用來列出Hibernate映射文件 -->
<value>Person.hbm.xml</value>
</list>
</property>
<!-- 定義Hibernate的SessionFactory的屬性 -->
<property name="hibernateProperties">
<props>
<!-- 指定數據庫方言 -->
<prop key="hibernate.dialect">
org.hibernate.dialect.MySQLInnoDBDialect</prop>
<!-- 是否根據需要每次自動創建數據庫 -->
<prop key="hibernate.hbm2ddl.auto">update</prop>
<!-- 顯示Hibernate持久化操作所生成的SQL -->
<prop key="hibernate.show_sql">true</prop>
<!-- 將SQL腳本進行格式化后再輸出 -->
<prop key="hibernate.format_sql">true</prop>
</props>
</property>
</bean>
<!--使用JtaTransactionManager類,該類實現PlatformTransactionManager接口-->
<!--針對采用全局事務管理的特定實現-->
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
從上面的配置文件可以看出,當使用Spring事務管理事務策略時,應用程序無需與具體的事務策略耦合,Spring提供了兩種事務管理方式:
編程式管理方式:即使使用Spring編程式事務時,程序也可直接獲取容器中的TransactionManagerBean,該Bean總是PlatformTransactionManager的實例,可以通過該接口提供的3個方法來開始事務,提交事務和回滾事務。
聲明式管理方式:無需在Java程序中書寫任何的事務操作代碼,而是通過在XML文件中為業務組件配置事務代理(AOP代理的一種),AOP事務代理所織入的增強處理也是由Spring提供:在目標方法前,織入開始事務;在目標方法后執行,織入結束事務。
注意:Spring編程事務還可以通過TransactionTemplate類來完成,該類提供了一個execute方法,可以更簡潔的方式來進行事務操作。
當使用聲明式事務時,開發者無需書寫任何事務管理代碼,不依賴Spring或或任何事務API,Spring的聲明式事務編程無需任何額外的容器支持,Spring容器本身管理聲明式事務,使用聲明式事務策略,可以讓開發者更好的專注于業務邏輯的實現。
使用TransactionProxyFactoryBean創建事務代理
在Spring1.X,聲明事務使用TransactionProxyFactoryBean來配置事務代理Bean,正如他的名字所暗示的,它是一個工廠Bean,該工程Bean專為目標Bean生成事務代理Bean,既然TransactionProxyFactoryBean產生的是事務代理Bean,可見Spring的聲明式事務策略是基于Spring AOP的。
例如:
<beans>
<!-- 定義數據源Bean,使用C3P0數據源實現 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定連接數據庫的驅動 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定連接數據庫的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定連接數據庫的用戶名 -->
<property name="user" value="root"/>
<!-- 指定連接數據庫的密碼 -->
<property name="password" value="32147"/>
<!-- 指定連接數據庫連接池的最大連接數 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定連接數據庫連接池的最小連接數 -->
<property name="minPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的初始化連接數 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的連接的最大空閑時間 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 配置JDBC數據源的局部事務管理器,使用DataSourceTransactionManager 類 -->
<!-- 該類實現PlatformTransactionManager接口,是針對采用數據源連接的特定實現-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 配置DataSourceTransactionManager時需要依注入DataSource的引用 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置一個業務邏輯Bean -->
<bean id="test" class="lee.TestImpl">
<property name="ds" ref="dataSource"/>
</bean>
<!-- 為業務邏輯Bean配置事務代理 -->
<bean id="testTrans" class=
"org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 為事務代理工廠Bean注入事務管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<property name="target" ref="test"/>
<!-- 指定事務屬性 -->
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
public class TestImpl implements Test
{
private DataSource ds;
public void setDs(DataSource ds)
{
this.ds = ds;
}
public void insert(String u)
{
JdbcTemplate jt = new JdbcTemplate(ds);
jt.execute("insert into mytable values('" + u + "')");
//兩次插入相同的數據,將違反主鍵約束
jt.execute("insert into mytable values('" + u + "')");
//如果增加事務控制,我們發現第一條記錄也插不進去。
//如果沒有事務控制,則第一條記錄可以被插入
}
}
配置事務代理的時。需要傳入一個事務管理器,一個目標Bena,并指定該事務的事務屬性,事務實行由transactionAttribute指定,上面只有一條事務傳播規則,該規則指定對于所有方法都使用PROPAGATION_REQUIRED的傳播屬性,Spring支持的事務傳播規則:
PROPAGATION_MANDATORY:要求調用該方法的線程已處于事務環境中,否則拋出異常;
PROPAGATION_NESTED:如果執行該方法的線程已處于事務的環境下,依然啟動新的事務,方法在嵌套的事務里執行,如果執行該方法的線程并未處于事務的環境下,也啟動新的事務,此時與PROPAGATION_REQUIRED;
PROPAGATION_NOT_SUPPORTED:如果調用該方法的線程處于事務中,則暫停事務,再執行方法。
PROPAGATION_NEVER:不允許執行該方法的線程處于事務的環境下,如果執行該方法的線程處于事務的環境下,則會拋出異常;
PROPAGATION_REQUIRED:要求在事務環境中執行該方法,如果當前執行線程已處于事務中,則直接調用;否則,則啟動新的事務后執行該方法。
PROPAGATION_REQUIREDS_NEW:該方法要求在新的事務中執行,如果當前執行線程已處于事務環境下,則暫停當前事務,啟動新事務后執行方法;否則,啟動新的事務后執行方法。
PROPAGATION_SUPPORTS:如果當前線程處于事務中,則使用當前事務,否則不使用事務。
主程序代碼:
public class MainTest
{
public static void main(String[] args)
{
//創建Spring容器
ApplicationContext ctx = new
ClassPathXmlApplicationContext("bean.xml");
//獲取事務代理Bean
Test t = (Test)ctx.getBean("testTrans");----------①
//執行插入操作
t.insert("bbb");
}
}
上面①處代碼獲取testTrans Bean,該Bean已不再是TestImpl的實例了,它只是Spring容器創建的事務代理,該事務代理以TestImpl實例為目標對象,且該目標對象也實現了Test接口,故代理對象也可當作Test實例使用。
實際上,Spring不僅支持對接口的代理,整合GBLIB后,Spring甚至可以對具體類生成代理,只要設置proxyTargetClass屬性為true就可以了,如果目標Bean沒有實現任何接口,proxyTargetClass默認為true,此時Spring會對具體的類生成代理。當然通常建議面向接口編程,而不面向具體的實現類編程。
Spring2.X的事務配置策略
上面介紹的TransactionProxyFactoryBean配置策略簡單易懂,但是配置起來極為繁瑣:每個目標Bean都需要額外配置一個TransactionProxyFactoryBean代理,這種方法將導致配置文件的急劇增加。
Spring2.X的XML Schema方式提供了更簡潔事務配置策略,Spring提供了tx命名空間來配置事務管理,tx命名空間下提供了<tx:advice../>元素來配置事務切面,一旦使用了該元素配置了切面,就可以直接使用<aop:advisor.../>元素啟動自動代理。
如:
<!-- 定義數據源Bean,使用C3P0數據源實現 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<!-- 指定連接數據庫的驅動 -->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<!-- 指定連接數據庫的URL -->
<property name="jdbcUrl" value="jdbc:mysql://localhost/javaee"/>
<!-- 指定連接數據庫的用戶名 -->
<property name="user" value="root"/>
<!-- 指定連接數據庫的密碼 -->
<property name="password" value="32147"/>
<!-- 指定連接數據庫連接池的最大連接數 -->
<property name="maxPoolSize" value="40"/>
<!-- 指定連接數據庫連接池的最小連接數 -->
<property name="minPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的初始化連接數 -->
<property name="initialPoolSize" value="1"/>
<!-- 指定連接數據庫連接池的連接的最大空閑時間 -->
<property name="maxIdleTime" value="20"/>
</bean>
<!-- 配置JDBC數據源的局部事務管理器,使用DataSourceTransactionManager 類 -->
<!-- 該類實現PlatformTransactionManager接口,是針對采用數據源連接的特定實現-->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 配置DataSourceTransactionManager時需要依注入DataSource的引用 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置一個業務邏輯Bean -->
<bean id="test" class="lee.TestImpl">
<property name="ds" ref="dataSource"/>
</bean>
<!-- 配置事務切面Bean,指定事務管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 用于配置詳細的事務語義 -->
<tx:attributes>
<!-- 所有以'get'開頭的方法是read-only的 -->
<tx:method name="get*" read-only="true"/>
<!-- 其他方法使用默認的事務設置 -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 配置一個切入點,匹配lee包下所有以Impl結尾的類
執行的所有方法 -->
<aop:pointcut id="leeService"
expression="execution(* lee.*Impl.*(..))"/>
<!-- 指定在txAdvice切入點應用txAdvice事務切面 -->
<aop:advisor advice-ref="txAdvice"
pointcut-ref="leeService"/>
</aop:config>
</beans>
提示:
Advisor的作用:將Advice和切入點綁在一起,保證Advice所包含的增強處理在對應的切入點被織入。
主程序:
public class MainTest
{
public static void main(String[] args)
{
//創建Spring容器
ApplicationContext ctx = new
ClassPathXmlApplicationContext("bean.xml");
//獲取事務代理Bean
Test t = (Test)ctx.getBean("test");
//執行插入操作
t.insert("bbb");
}
}
上面的配置文件直接獲取容器中的test Bean,因為Spring AOP會為該Bean自動織入事務增強處理的方式,所以test Bean的所有方法都具有事務性。
配置<tx:advice../>元素時除了需要一個transaction-Manager之外,重要的就是需要配置一個<attributes.../>子元素,該元素又可以包含多個<method../>子元素。
可以看出配置<tx:advice../>元素重點就是配置<method.../>子元素,每個<method../>子元素為一批方法指定所需的事務語義。
配置<method../>元素時可以指定如下屬性:
name:必選屬性,與該事務語義關聯的方法名,該屬性支持通配符。
propagation:指定事務傳播行為,該屬性可以為Propagation枚舉類的任意一個枚舉值,該屬性 的默認值是Propagation.REQUIRED;
isolation:指定事務隔離級別,該屬性值可為Isolation枚舉類的任意枚舉值。
timeout:指定事務超時時間(以秒為單位),指定-1意味不超時,默認值是-1;
read-only:指定事務是否只讀,該屬性默認為false;
rollback-for:指定觸發事務回滾的異常類(全名),該屬性可以指定多個異常類,用英文逗號隔開;
no-rollback-for:指定不觸發事務回滾的類,該屬性可以指定多個異常類,并且用英文的逗號隔開。
因此我們可以為不同的業務邏輯方法指定不同的事務策略,如:
<tx:advice id="aaa">
<tx:attributes>
<tx:method name="get*" read--only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<tx:advice id="bbb">
<tx:attributes>
<tx:method name="*" propagation="NEVER"/>
</tx:attributes>
</tx:advice>