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事务管理功能已正常;生产服务器中的数据不准确以及脏数据也已经解决


    原文作者:_Hebrew
    原文地址: https://blog.csdn.net/qq_33571718/article/details/79730487
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞