Spring事務管理配置及異常詳解
最近在生產專案上出現一些問題,同一流程下涉及到多個數據庫表的增改出現不一致的情況;
例如tableA,tableB,tableC:
三張表同時做insert操作(或者是update操作),其中tableA,tableB儲存成功,tableC卻未能儲存成功;這樣的話,就造成生產伺服器上的資料不準確;
系統環境:spring3.0.2+struts2.18+hibernate3.3.2
解決方案:
使用的是spring框架;所以想到的肯定是使用spring整合hibernate的事務管理機制
因為這個系統已經開發了一段時間,這個框架中也添加了spring事務管理機制,但是問題是沒有生效,這裡就說一下如何解決事務沒有生效的問題;
one:(貼配置檔案)
ApplicationContext.xml
<!-- 定義事務管理器 -->
<bean id="txManage"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!--<tx:annotation-driven transaction-manager="txManage"/>-->
<tx:advice id="txAdvice" transaction-manager="txManage">
<tx:attributes>
<!--注意 propagation="REQUIRED"... -->
<tx:method name="insert*" propagation="REQUIRED" rollback-for="com.rhxy .utils.SelfException"/>
<tx:method name="doA*" propagation="REQUIRED" rollback-for="com.rhxy.utils.SelfException"/>
<tx:method name="test*" propagation="REQUIRES_NEW" rollback-for="com.rhxy.utils.SelfException"/>
<tx:method name="get*" read-only="true" />
<tx:method name="find*" read-only="true" />
<tx:method name="search*" read-only="true" />
<tx:method name="query*" read-only="true" />
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="del*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="do*" propagation="REQUIRED" />
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="saveOrUpdate*" propagation="REQUIRED" />
<!--<tx:method name="*" propagation="REQUIRED" read-only="true" />-->
<tx:method name="*" propagation="SUPPORTS" />
</tx:attributes>
</tx:advice>
<aop:aspectj-autoproxy proxy-target-class="true" />
<aop:config> <!--注意 aop:pointcut 定義切入點-->
<aop:pointcut
expression="execution(* com.rhxy .dao.*.*(..))||execution(* com.rhxy.service.inventory.StockService.test(..)) || execution(* com.rhxy.action.InStoreRecordAction.*(..))"
id="serviceMethod" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod" />
</aop:config>
在這裡必須要注意的地方有兩點:
1.Spring事務的傳播機制
Propagation : key屬性確定代理應該給哪個方法增加事務行為。這樣的屬性最重要的部份是傳播行為。有以下選項可供使用:PROPAGATION_REQUIRED--支援當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。
PROPAGATION_SUPPORTS--支援當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY--支援當前事務,如果當前沒有事務,就丟擲異常。
PROPAGATION_REQUIRES_NEW--新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED--以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER--以非事務方式執行,如果當前存在事務,則丟擲異常。
1: PROPAGATION_REQUIRED
加入當前正要執行的事務不在另外一個事務裡,那麼就起一個新的事務
比如說,ServiceB.methodB的事務級別定義為PROPAGATION_REQUIRED, 那麼由於執行ServiceA.methodA的時候,
ServiceA.methodA已經起了事務,這時呼叫ServiceB.methodB,ServiceB.methodB看到自己已經執行在ServiceA.methodA
的事務內部,就不再起新的事務。而假如ServiceA.methodA執行的時候發現自己沒有在事務中,他就會為自己分配一個事務。
這樣,在ServiceA.methodA或者在ServiceB.methodB內的任何地方出現異常,事務都會被回滾。即使ServiceB.methodB的事務已經被
提交,但是ServiceA.methodA在接下來fail要回滾,ServiceB.methodB也要回滾
2: PROPAGATION_SUPPORTS
如果當前在事務中,即以事務的形式執行,如果當前不再一個事務中,那麼就以非事務的形式執行
3: PROPAGATION_MANDATORY
必須在一個事務中執行。也就是說,他只能被一個父事務呼叫。否則,他就要丟擲異常
4: PROPAGATION_REQUIRES_NEW
這個就比較繞口了。 比如我們設計ServiceA.methodA的事務級別為PROPAGATION_REQUIRED,ServiceB.methodB的事務級別為PROPAGATION_REQUIRES_NEW,
那麼當執行到ServiceB.methodB的時候,ServiceA.methodA所在的事務就會掛起,ServiceB.methodB會起一個新的事務,等待ServiceB.methodB的事務完成以後,
他才繼續執行。他與PROPAGATION_REQUIRED 的事務區別在於事務的回滾程度了。因為ServiceB.methodB是新起一個事務,那麼就是存在
兩個不同的事務。如果ServiceB.methodB已經提交,那麼ServiceA.methodA失敗回滾,ServiceB.methodB是不會回滾的。如果ServiceB.methodB失敗回滾,
如果他丟擲的異常被ServiceA.methodA捕獲,ServiceA.methodA事務仍然可能提交。
5: PROPAGATION_NOT_SUPPORTED
當前不支援事務。比如ServiceA.methodA的事務級別是PROPAGATION_REQUIRED ,而ServiceB.methodB的事務級別是PROPAGATION_NOT_SUPPORTED ,
那麼當執行到ServiceB.methodB時,ServiceA.methodA的事務掛起,而他以非事務的狀態執行完,再繼續ServiceA.methodA的事務。
6: PROPAGATION_NEVER
不能在事務中執行。假設ServiceA.methodA的事務級別是PROPAGATION_REQUIRED, 而ServiceB.methodB的事務級別是PROPAGATION_NEVER ,
那麼ServiceB.methodB就要丟擲異常了。
7: PROPAGATION_NESTED
理解Nested的關鍵是savepoint。他與PROPAGATION_REQUIRES_NEW的區別是,PROPAGATION_REQUIRES_NEW另起一個事務,將會與他的父事務相互獨立,
而Nested的事務和他的父事務是相依的,他的提交是要等和他的父事務一塊提交的。也就是說,如果父事務最後回滾,他也要回滾的。
而Nested事務的好處是他有一個savepoint。
2.Spring AOP pointcut的用法
1.什麼是AOP?
Aspect Orentied Programming (AOP,面向方面程式設計)
Object Orentied Programming (OOP,面向物件程式設計)
AOP程式設計是以OOP為基礎,OOP側重點是物件抽象和封裝,
AOP側重點是共通處理部分的封裝和使用.用於改善共通元件
和目標元件之間的關係(低耦合)
2.AOP使用示例
------AOP示例操作步驟------
a.引入Spring-AOP的開發包.
b.首先編寫一個方面元件,將共通處理封裝.
c.然後在Spring容器配置中新增AOP定義
--將方面元件Bean定義,採用<aop:aspect>指定為方面元件
--採用<aop:pointcut>指定切入點,確定目標物件
--採用<aop:after>或<aop:before>通知指定方面元件和目標物件方法的作用時機.
3.AOP相關概念
*a.Aspect(方面元件)
方面元件就是封裝共通處理邏輯的元件,其功能將來要作用到某一批目標方法上.例如日誌記錄,異常處理,事務處理等
*b.PointCut(切入點)
切入點是用於指定目標物件或方法的一個表示式.
c.JointPoint(連線點)
切入點是連線點的集合.指的是方面元件和目標元件作用的位置.
例如方法呼叫,異常發生位置.
*d.Advice(通知)
用於指定方面元件在目標方法上作用的時機.例如在目標方法之前執行,目標方法之後執行,目標方法之前和之後執行等.
e.Target(目標物件)
要使用方面功能的元件物件.或被切入點表示式指定的物件
f.AutoProxy(動態代理物件)
Spring使用了AOP機制後,採用的是動態代理技術實現的.
當採用了AOP之後,Spring通過getBean返回的物件是一個動態代理型別物件.當使用該物件的業務方法時,該物件會負責呼叫方面元件和目標元件的功能.
如果未採用AOP,Spring通過getBean返回的是原始型別物件,因此執行的是原有目標物件的處理.
Spring動態代理技術採用的是以下兩種:
--採用JDK Proxy API實現.(目標物件有介面定義)
--採用Cglib.jar工具包API實現.(目標物件沒有介面定義)
4.通知型別
通知主要負責指定方面功能和目標方法功能的作用關係.
Spring框架提供了以下5種類型通知.
a. 前置通知<aop:before>
方面功能在目標方法之前呼叫.
b. 後置通知<aop:after-returning>
方面功能在目標方法之後呼叫.目標方法無異常執行.
c. 最終通知 <aop:after>
方面功能在目標方法之後呼叫.目標方法有無異常都執行
e. 異常通知 <aop:after-throwing>
方面功能在目標方法丟擲異常之後執行.
f. 環繞通知 <aop:around>
方面功能在目標方法執行前和後呼叫.
try{
//環繞通知前置部分功能
//前置通知--執行方面功能
呼叫目標方法處理
//後置通知--執行方面功能
//環繞通知後置部分功能
}catch(){
//異常通知--執行方面功能
}finally{
//最終通知--執行方面功能
}
5.切入點表示式的指定
切入點表示式用於指定哪些物件和方法呼叫方面功能.
*1)方法限定表示式
execution(修飾符? 返回型別 方法名(引數) throws 異常型別? )
示例1--匹配所有Bean物件中以add開頭的方法
execution(* add*(..))
示例2--匹配UserService類中所有的方法
execution(* tarena.service.UserService.*(..))
示例3--匹配UserService類中有返回值的所有方法
execution(!void tarena.service.UserService.*(..))
示例4--匹配所有Bean物件中修飾符為public,方法名為add的方法
execution(public * add(..))
示例5--匹配tarena.service包下所有類的所有方法
execution(* tarena.service.*.*(..))
示例6--匹配tarena.service包及其子包中所有類所有方法
execution(* tarena.service..*.*(..))
*2)型別限定
within(型別)
示例1--匹配UserService類中的所有方法
within(tarena.service.UserService)
示例2--匹配tarena.service包下所有型別的所有方法
within(tarena.service.*)
示例3--匹配tarena.service包及其子包中所有型別的所有方法
within(tarena.service..*)
3)Bean名稱限定
bean(bean的id|name屬性值)
按<bean>元素定義的id或name值做匹配限定.
示例1--匹配容器中bean元素id="userService"物件
bean(userService)
示例2--匹配容器中所有id屬性以Service結束的bean物件
bean(*Service)
4)引數列表限定
arg(引數型別)
示例--匹配有且只有一個String引數的方法
arg(java.lang.String)
two:(不生效有哪些因素)
1.資料庫原因,因為有的資料庫引擎是不支援事務管理的。如果你用的是mysql資料庫,看看資料庫使用的是什麼引擎
使用下述語句之一檢查表的標型別:
SHOW TABLE STATUS LIKE 'tableName';
SHOW CREATE TABLE tableName;
2.使用的是Spring+mvc框架,有可能是因為spring配置的自動掃描重複掃描所造成的,仔細檢查一下spring配置檔案;或者是因為載入Spring配置檔案時,按照Spring配置檔案的載入順序,先載入SpringMVC的配置,再載入Spring的配置,一般情況下我們的事務管理都是配置在Spring的配置檔案,而先載入SpringMVC時,把Service也註冊了,但是這個時候事務還沒有載入,也就導致事務無法成功注入到Service中。
1、在主容器中(applicationContext.xml),將Controller的註解排除掉
<context:component-scan base-package="com">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
2、而在springMVC配置檔案中將Service註解給去掉
<context:component-scan base-package="com">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" />
</context:component-scan>
3.Aop的切入點出錯了
使用如下程式碼 確認你的bean 是不是代理物件
AopUtils.isAopProxy()
AopUtils.isCglibProxy() //cglib
AopUtils.isJdkDynamicProxy() //jdk動態代理
一般情況下 不使用AOP切面的話,所獲得的bean是一個普通物件,也就是目標物件;如果使用了AOP,則相關物件是一個代理物件,通過三面三種方法驗證你自己定義的aop:pointcut是否生效,如果生效的話,
AopUtils.isAopProxy() //返回true
AopUtils.isCglibProxy() //cglib 返回true
AopUtils.isJdkDynamicProxy() //jdk動態代理 返回false
4.事務的傳播屬性配置錯誤,比如使用的是NEVER或者NOT_SUPPORTED;仔細檢查配置檔案,一般情況下使用REQUIRED、REQUIRES_NEW
5.注意控制檯異常:
org.hibernate.HibernateException: save is not valid without active transaction
這是因為Spring整合的Hibernate配置中設定了hibernate.current_session_context_class=true,獲取session的方式是getCurrentSession()
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.connection.autocommit">true</prop>
<prop key="myeclipse.connection.profile">MySQL</prop>
<!--<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>-->
<prop key="hibernate.dialect">com.rhxy.dao_new.MySQLLocalDialect</prop>
<!--<prop key="hibernate.hbm2ddl.auto">create</prop>-->
<prop key="hibernate.hbm2ddl.auto">update</prop>
<prop key="hibernate.myeclipse.connection.profile">true</prop>
<!--<prop key="hibernate.show_sql">true</prop>-->
<!--<prop key="hibernate.format_sql">true</prop>-->
<!--<prop key="hibernate.current_session_context_class">thread</prop>這裡我已經註釋掉了-->
<prop key="hibernate.transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory
</prop>
<prop key="hibernate.query.substitutions">true 1, false 0</prop>
</props>
</property>
<property name="mappingLocations">
<list>
<value>classpath:com/xxxx/bean/settings/*.hbm.xml</value>
<value>classpath:com/xxxx/bean/common/*.hbm.xml</value>
<value>classpath:com/xxxx/bean/customservice/*.hbm.xml</value>
<value>classpath:com/xxxx/bean/finance/*.hbm.xml</value>
<!--...hbm.xml-->
</list>
</property>
當呼叫getCurrentSession()時,hibernate將session繫結到當前執行緒,事務結束後mhibernate將session從當前執行緒中釋放,並且關閉session。當再次呼叫時,得到一個新的session;
所以,將hibernate.current_session_context_class的值設定為thread。當我們呼叫getCurrentSession()時,獲取的不再是交由Spring託管的session了;所以獲取的seesion並非事務管理器鍾代理的那個session。所以不能自動開啟事務。
不能使用openSession();這樣的話雖然不會報錯,但是事務不起作用
three:(解決思路)
1.使用AopUtils驗證呼叫函式的物件是不是一個代理物件
這裡說明一下:
因為在專案中,Action主要處理關於前端的一寫操作,Service主要是處理業務邏輯,DAO是對資料庫進行操作;因為我是藥統一管理幾個表的增刪改,所以將事務管理放在Service中,但是真正的操作資料表是在DAO中;所以,要求Service每次都是開啟一個新的Session(REQUIRES_NEW),而DAO層,因為涉及多個表的操作,就需要使用同一個session,當session不存在時再建立(REQUIRED); 所以,這裡需要在Action中驗證Service是否是一個代理物件,在Service中驗證DAO是否是一個代理物件;
如果驗證返回false,那就是AOP定義的切點有問題;仔細檢查,例如之前專案中在Service中獲取到的是一個目標物件,並不是我們需要的代理物件,檢查後發現:
execution(* com.xxxx.dao*.*(..))
修改為:
execution(* com.xxxx.dao.*.*(..))
再次驗證,獲取到的是代理物件;如果你要定義切點在Action上時,一定要在Spring配置檔案中加入:
<aop:aspectj-autoproxy proxy-target-class="true" />
不然的話Action的代理物件會報找不到方法的異常
使用了Spring的事務管理後,在DAO層中就不需要手動提交
2.修改程式碼
新增Spring的事務傳播屬性,修改Aop pointcut, 修改session的獲取方式,使用getCurrentSession()獲取session,
修改Hibernate配置檔案,刪除current_session_context_class屬性, 新增AOPUtils 測試代理物件(Action和Service中);
刪除DAO層手動提交和回滾的程式碼
3.手動回滾或者手動丟擲異常讓Spring處理
以上都ok以後測試事務管理是否生效:
先按照正確流程走一遍(無報錯的情況下);在事務結束以後資料庫表中是否插入資料;如果正確,然後再測試不正確的流程(手動丟擲異常或者執行異常),測試是否會回滾。
注意,即使你之前的所有的都配置好了,都沒有問題了,還是有可能不生效:
Spring預設情況下會對執行期例外(RunTimeException)進行事務回滾。這個例外是unchecked
如果遇到checked意外就不回滾。
如何改變預設規則:
1 讓checked例外也回滾:在Spring事務管理配置上加入rollback-for="java.lang.Exception"
2 讓unchecked例外不回滾:在Spring事務管理配置上加入no-rollback-for="java.lang.Exception"
3 自定義異常回滾:在Spring事務管理配置上加入no-rollback-for="com.rhxy.Utils.SelfException"
所以在Service中,我們不捕獲異常直接Throws Exception,這樣的話 異常才能被Spring捕獲到,注意不指定的話,預設是隻有RunTimeException才生效
為了滿足業務需求,我使用try{}catch;當出現異常我們捕獲時,可以有兩種方法讓Spring事務管理器來處理:
A:在catch中手動丟擲自己指定的異常或者是預設的RunTimeException,有Spring事務管理器來回滾
B:手動讓事務進行回滾:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
four:(結果)
經測試,Spring事務管理功能已正常;生產伺服器中的資料不準確以及髒資料也已經解決