现象描述
上周同事发现其基于mySql实现的分布式锁的线上代码存在问题,代码简化如下:
@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
管理起来。
发现的两个问题
- 在一个实例方法中调用被
@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方法的支持。
“自调用”的解决方法
1. 最好在被代理类的外部调用其方法
2. 自注入(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()
因此也能正确的指向增强后的方法。
一种错误的解决办法:改造为Java接口的形式
@Controller
class XService implements IXService {
@Autowired
private YService yService;
@Override
public void doOutside(){
this.doInside();
}
@Transactional
private void doInside(){
//do sql statement
}
}
@Controller
class Test {
@Autowired
private IXService iXService;
public test(){
iXService.doOutside();
}
}
原因是之前错误地理解事务未生效的原理:如果没有在xml中要设置只用CGLIB,@Transactional
只能使用JDK动态代理,所以如果没有用Java接口方式进行代理就不会生效。
实际上,这还是避免不了自调用的问题,因为这是动态代理的普遍问题,无论是JDK动态代理还是CGLIB动态代理。
总结
使用Spring AOP的时候一定要小心,如果是使用注解形式声明AOP,要保证在被代理类的外部调用被增强的方法。