Spring嵌套事务失效问题
现象描述
代码简化如下:
@Controller
class XService {
@Autowired
private YService yService;public void doOutside(){
this.doInside(); //或者直接doInside();效果是一样的
}
@Transactional
private void doInside(){
//do sql statement
}
}
@Controller
class Test {
@Autowired
private XService xService;
public void test(){
xService.doOutside();
}
}
实际执行test()后发现doInside()的Sql执行过程没有被Spring Transaction Manager管理起来。
下面再看另一种情况:
/**
* 调用child , 由于child 抛出异常,查看事物回滚
* @return
*/
@Transactional(propagation = Propagation.SUPPORTS ,rollbackFor = Exception.class)
public Integer parent() {
try {
child();
}catch (Exception e){
}
Course course = new Course();
course.setName("childCourse");
course.setCreateTime(new Date());
testDao .insert(course);
return Integer.MAX_VALUE;
}
/**
* 被parent 调用,该事物传播机制为新启一个事物,关于事物传播,请另查询资料
*
*
* 插入一条course 记录
* @return
*/
@Transactional(propagation = Propagation.REQUIRES_NEW ,rollbackFor = Exception.class)
public Integer child() {
Course course = new Course();
course.setName("childCourse");
course.setCreateTime(new Date());
testDao .insert(course);
// 抛出异常
int i = 10 / 0 ;
return Integer.MAX_VALUE;
}
在child 方法中我声明事物传播为REQUIRES_NEW
,因此,child 在执行的时候应该挂起parent 方法的事物,等执行完毕child 方法的事物之后,唤醒parent 的事物,这种情况的预期结果是parent 插入成功,child 插入失败。但是 结果,确实 呵呵,全部成功。
发现的两个问题
在一个实例方法中调用被@Transactional注解标记的另一个方法,且两个方法都属于同一个类时,事务不会生效。
调用被@Transactional注解标记的非public方法,事务不会生效。
首先复习下相关知识:Spring AOP、JDK动态代理、CGLIB、AspectJ、@Aspect
@Transactional的实现原理是在业务方法外边通过Spring AOP包上一层事务管理器的代码(即插入切面),这是Java设计模式中常见的通过代理增强被代理类的做法。
Spring AOP的底层有2种实现:JDK动态代理、CGLIB。前者的原理是JDK反射,并且只支持Java接口的代理;后者的原理是继承(extend)与覆写(override),因此能支持普通的Java类的代理。两种方式都是动态代理,即运行时实时生成代理。
由于JVM的限制,CGLIB无法替换被代理类已经被载入的字节码,只能生成并载入一个新的子类作为代理类,被代理类的字节码依然存在于JVM中。
区别于前两者,AspectJ是一种静态代理的实现,即在编译时或者载入类时直接修改被代理类文件的字节码,而非运行时实时生成代理。因此这种方式需要额外的编译器或者JVM Agent支持,通过一些配置Spring和AspectJ也可以配合使用。
@Aspect一开始是AspectJ推出的Java注解形式,后来Spring AOP也支持使用这种形式表示切面,但实际上底层实现和AspectJ毫无关系,毕竟Spring AOP是动态代理,和静态代理是不兼容的。
进一步分析
既然事务管理器没有生效,那么首先需要确定一个问题:this到底是指向哪个对象,是未增强的XService还是增强后的XService?并且而且有没有可能已经调用增强后的实例和方法,但由于其他原因而导致事务管理器没有生效?
回忆下Java基础,this表示的是类的当前实例,那么关键就是确定类的实例是未被增强的XService(下面称其为XService),还是被CGLIB增强过的XService(下面称其为XService$$Cglib)。
在Test中,XService类的实例变量是一个由Spring框架管理的Bean,当执行test()时,根据@Autowired注解进行相应的注入,因此XService的实例实际为XService$$Cglib而不XService。被增强过的类的代码可以简化如下:
class XService$$Cglib extend XService {
@Override
public doInside(){
//开始事务的增强代码
super.doInside();
//结束事务的增强代码
}
}
当执行XService$$Cglib.doOutside()时,由于子类没有覆写父类同名方法,因此实际上执行了父类XService的doOutside()方法,所以在执行其this.doInside()时实际上调用的是父类未增强过的doInside(),因此事务管理器失效了。
这个问题在Spring AOP中广泛存在,即自调用,本质上是动态代理无法解决的盲区,只有AspectJ这类静态代理才能解决。
第二个问题则是Spring AOP不支持非public方法增强,与自调用类似,也是动态代理无法解决的盲区。
虽然CGLIB通过继承的方式是可以支持public、protected、package级别的方法增强的,但是由于JDK动态代理必须通过Java接口,只能支持public级别的方法,因此Spring AOP不得不取消非public方法的支持。
下面对接口进行动态代理进行分析
接口:
package proxy2;
public interface TestService {
Integer test1();
Integer test2();
Integer abcTest();
}
实现类:
package proxy2;
public class TestServiceImpl implements TestService {
// public Integer test1() {
// System.out.println("test1 被调用");
// return Integer.MAX_VALUE;
// }
//
// public Integer test2() {
// System.out.println("test2 被调用");
// return Integer.MAX_VALUE;
// }
//
// public Integer abcTest() {
// System.out.println("abcTest 被调用");
// return null;
// }
public Integer test1() {
System.out.println("test1 被调用");
test2();
System.out.println("-------------------------------------------------------");
return Integer.MAX_VALUE;
}
public Integer test2() {
System.out.println("test2 被调用");
System.out.println("-------------------------------------------------------");
return Integer.MAX_VALUE;
}
public Integer abcTest() {
System.out.println("abcTest 被调用");
System.out.println("-------------------------------------------------------");
return null;
}
}
代理:
package proxy2;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
private Object target ;
public MyInvocationHandler(Object target){
this.target = target ;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().startsWith("test")){
System.out.println("我被代理了");
}
Object invoke = method.invoke(target, args);
return invoke;
}
}
测试:
package proxy2;
import java.lang.reflect.Proxy;
public class TestProxy {
public static void main(String[] args) {
MyInvocationHandler invocationHandler = new MyInvocationHandler(new TestServiceImpl());
TestService testService =(TestService) Proxy.newProxyInstance(
TestService.class.getClassLoader(),
new TestServiceImpl().getClass().getInterfaces(),
invocationHandler);
testService.test1();
testService.test2();
testService.abcTest();
}
}
结果:
我被代理了
test1 被调用
test2 被调用
我被代理了
test2 被调用
abcTest 被调用
原因
Spring AOP使用JDK动态代理和CGLib,当方法被代理时,其实通过动态代理生成了代理对象,然后代理对象执行invoke方法,在调用被代理对象的方法时,执行其他操作。问题就在于被代理对象的方法中调用被代理对象的其他方法时,使用的是被代理对象本身,而非代理对象。这就导致了一个方法时代理对象调用的,一个是被代理对象调用的。他们的调用始终不出于同一个对象。
“自调用”的解决方法
- 最好在被代理类的外部调用其方法
- 自注入(Self Injection, from Spring 4.3)
@Controller
class XService {
@Autowired
private YService yService;
@Autowired
private XService xService;
public void doOutside(){
xService.doInside();//从this换成了xService
}
@Transactional
private void doInside(){
//do sql statement
}
}
@Controller
class Test {
@Autowired
private XService xService;
public void test(){
xService.doOutside();
}
}
由于xService变量是被Spring注入的,因此实际上指向XService$$Cglib对象,xService.doInside()因此也能正确的指向增强后的方法。
3.通过ThreadLocal暴露Aop代理对象
1、开启暴露Aop代理到ThreadLocal支持(如下配置方式从spring3开始支持)
<aop:aspectj-autoproxy expose-proxy="true"/><!—注解风格支持-->
<aop:config expose-proxy="true"><!—xml风格支持-->
2、修改我们的业务实现类
@Transactional(propagation = Propagation.SUPPORTS,rollbackFor = Exception.class)
public Integer parent() {
try {
((TestService) AopContext.currentProxy()). child();
}catch (Exception e){
}
Course course = new Course();
course.setName("parent");
course.setCreateTime(new Date());
testDao .insert(course);
return Integer.MAX_VALUE;
}
配置
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceForSqlServer" />
</bean>
注意事项
1.在需要事务管理的地方加@Transactional 注解。@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上 。
2.@Transactional 注解只能应用到 public 可见度的方法上 。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。
3.注意仅仅 @Transactional 注解的出现不足于开启事务行为,它仅仅 是一种元数据。必须在配置文件中使用配置元素,才真正开启了事务.
4.spring事物是基于类和接口的所以只能在类里面调用另一个类里面的事物,同一个类里面调用自己类的事物方法是无效的。spring事物也不要频繁使用,在事物处理的同时操作的第一张表会被限制查看的(即被临时锁住)。数据量大的时候会有一定影响。
摘自: https://blog.csdn.net/u014082714/article/details/80967103