上週同事發現其基於mySql實現的分佈式鎖的線上代碼存在問題,代碼簡化以下:html
@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
管理起來。java
@Transactional
註解標記的另外一個方法,且兩個方法都屬於同一個類時,事務不會生效。@Transactional
註解標記的非public方法,事務不會生效。@Transactional
的實現原理是在業務方法外邊經過Spring AOP包上一層事務管理器的代碼(即插入切面),這是Java設計模式中常見的經過代理加強被代理類的作法。spring
Spring AOP的底層有2種實現:JDK動態代理、CGLIB。前者的原理是JDK反射,而且只支持Java接口的代理;後者的原理是繼承(extend
)與覆寫(override
),所以能支持普通的Java類的代理。兩種方式都是動態代理,即運行時實時生成代理。sql
因爲JVM的限制,CGLIB沒法替換被代理類已經被載入的字節碼,只能生成並載入一個新的子類做爲代理類,被代理類的字節碼依然存在於JVM中。設計模式
區別於前二者,AspectJ是一種靜態代理的實現,即在編譯時或者載入類時直接修改被代理類文件的字節碼,而非運行時實時生成代理。所以這種方式須要額外的編譯器或者JVM Agent支持,經過一些配置Spring和AspectJ也能夠配合使用。框架
@Aspect一開始是AspectJ推出的Java註解形式,後來Spring AOP也支持使用這種形式表示切面,但實際上底層實現和AspectJ毫無關係,畢竟Spring AOP是動態代理,和靜態代理是不兼容的。分佈式
既然事務管理器沒有生效,那麼首先須要肯定一個問題:this
究竟是指向哪一個對象,是未加強的XService仍是加強後的XService?而且並且有沒有可能已經調用加強後的實例和方法,但因爲其餘緣由而致使事務管理器沒有生效?ide
回憶下Java基礎,this
表示的是類的當前實例,那麼關鍵就是肯定類的實例是未被加強的XService(下面稱其爲XService
),仍是被CGLIB加強過的XService(下面稱其爲XService$$Cglib
)。this
在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方法的支持。
@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()
所以也能正確的指向加強後的方法。
@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,要保證在被代理類的外部調用被加強的方法。