最近在生产项目上出现一些问题,同一流程下涉及到多个数据库表的增改出现不一致的情况;
例如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事务管理功能已正常;生产服务器中的数据不准确以及脏数据也已经解决