現象描述
上週同事發現其基於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,要保證在被代理類的外部呼叫被增強的方法。