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方法,在調用被代理對象的方法時,執行其他操作。問題就在於被代理對象的方法中調用被代理對象的其他方法時,使用的是被代理對象本身,而非代理對象。這就導致了一個方法時代理對象調用的,一個是被代理對象調用的。他們的調用始終不出於同一個對象。

“自調用”的解決方法

  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()因此也能正確的指向增強后的方法。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM