天天看點

【Spring】 @Transaction事務未生效的原因及解決

現象描述

上周同僚發現其基于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

管理起來。

發現的兩個問題

  1. 在一個執行個體方法中調用被

    @Transactional

    注解标記的另一個方法,且兩個方法都屬于同一個類時,事務不會生效。
  2. 調用被

    @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,要保證在被代理類的外部調用被增強的方法。

轉自:https://segmentfault.com/a/1190000011440783

繼續閱讀