1. 程式人生 > >Spring事務管理配置及異常詳解

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事務管理功能已正常;生產伺服器中的資料不準確以及髒資料也已經解決