事務註解 @Transactional 失效的3種場景及解決辦法

作者 | 向北
來源 | urlify.cn/M3aA3i

 

Transactional失效場景

第一種 Transactional註解標註方法修飾符爲非public時,@Transactional註解將會不起作用。例如以下代碼,定義一個錯誤的@Transactional標註實現,修飾一個默認訪問符的方法:

/**
 * @author zhoujy
 **/

@Component
public class TestServiceImpl {
    @Resource
    TestMapper testMapper;
    
    @Transactional
    void insertTestWrongModifier() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }

}

在同一個包內,新建調用對象,進行訪問。

@Component
public class InvokcationService {
    @Resource
    private TestServiceImpl testService;
    public void invokeInsertTestWrongModifier(){
        //調用@Transactional標註的默認訪問符方法
        testService.insertTestWrongModifier();
    }
}

測試用例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests 
{
   @Resource
   InvokcationService invokcationService;

   @Test
   public void  testInvoke(){
      invokcationService.invokeInsertTestWrongModifier();
   }
}

以上的訪問方式,導致事務沒開啓,因此在方法拋出異常時,testMapper.insert(new Test(10,20,30));操作不會進行回滾。如果 TestServiceImpl#insertTestWrongModifier 方法改爲public的話將會正常開啓事務,testMapper.insert(new Test(10,20,30));將會進行回滾。

第二種失效場景

在類內部調用調用類內部@Transactional標註的方法,這種情況下也會導致事務不開啓。示例代碼如下,設置一個內部調用:

/**
 * @author zhoujy
 **/

@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Transactional
    public void insertTestInnerInvoke() {
        //正常public修飾符的事務方法
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }


    public void testInnerInvoke(){
        //類內部調用@Transactional標註的方法。
        insertTestInnerInvoke();
    }

}

測試用例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests 
{

   @Resource
   TestServiceImpl testService;

   /**
    * 測試內部調用@Transactional標註方法
    */

   @Test
   public void  testInnerInvoke(){
       //測試外部調用事務方法是否正常
      //testService.insertTestInnerInvoke();
       //測試內部調用事務方法是否正常
      testService.testInnerInvoke();
   }
}

上面就是使用的測試代碼,運行測試知道,外部調用事務方法能夠征程開啓事務,testMapper.insert(new Test(10,20,30))操作將會被回滾;
然後運行另外一個測試用例,調用一個方法在類內部調用內部被@Transactional標註的事務方法,運行結果是事務不會正常開啓,testMapper.insert(new Test(10,20,30))操作將會保存到數據庫不會進行回滾。

第三種失效場景

事務方法內部捕捉了異常,沒有拋出新的異常,導致事務操作不會進行回滾。示例代碼如下。

/**
 * @author zhoujy
 **/

@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Transactional
    public void insertTestCatchException() {
        try {
            int re = testMapper.insert(new Test(10,20,30));
            if (re > 0) {
                //運行期間拋異常
                throw new NeedToInterceptException("need intercept");
            }
            testMapper.insert(new Test(210,20,30));
        }catch (Exception e){
            System.out.println("i catch exception");
        }
    }
    
}

測試用例代碼如下。

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests 
{

   @Resource
   TestServiceImpl testService;

   @Test
   public void testCatchException(){
      testService.insertTestCatchException();
   }
}

運行測試用例發現,雖然拋出異常,但是異常被捕捉了,沒有拋出到方法 外, testMapper.insert(new Test(210,20,30))操作並沒有回滾。
以上三種就是@Transactional註解不起作用,@Transactional註解失效的主要原因。下面結合spring中對於@Transactional的註解實現源碼分析爲何導致@Transactional註解不起作用。

@Transactional註解不起作用原理分析

第一種場景分析

@Transactional 註解標註方法修飾符爲非public時, @Transactional 註解將會不起作用。這裏分析 的原因是, @Transactional 是基於動態代理實現的, @Transactional 註解實現原理中分析了實現方法,在bean初始化過程中,對含有 @Transactional 標註的bean實例創建代理對象,這裏就存在一個spring掃描 @Transactional 註解信息的過程,不幸的是源碼中體現,標註@Transactional的方法如果修飾符不是public,那麼就默認方法的 @Transactional 信息爲空,那麼將不會對bean進行代理對象創建或者不會對方法進行代理調用
@Transactional 註解實現原理中,介紹瞭如何判定一個bean是否創建代理對象,大概邏輯是。根據spring創建好一個aop切點 BeanFactoryTransactionAttributeSourceAdvisor 實例,遍歷當前bean的class的方法對象,判斷方法上面的註解信息是否包含 @Transactional ,如果bean任何一個方法包含 @Transactional 註解信息,那麼就是適配這個BeanFactoryTransactionAttributeSourceAdvisor切點。則需要創建代理對象,然後代理邏輯爲我們管理事務開閉邏輯。
spring源碼中,在攔截bean的創建過程,尋找bean適配的切點時,運用到下面的方法,目的就是尋找方法上面的@Transactional信息,如果有,就表示切點BeanFactoryTransactionAttributeSourceAdvisor能夠應用(canApply)到bean中,
AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)

public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
   Assert.notNull(pc, "Pointcut must not be null");
   if (!pc.getClassFilter().matches(targetClass)) {
      return false;
   }

   MethodMatcher methodMatcher = pc.getMethodMatcher();
   if (methodMatcher == MethodMatcher.TRUE) {
      // No need to iterate the methods if we're matching any method anyway...
      return true;
   }

   IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
   if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
      introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
   }

    //遍歷class的方法對象
   Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
   classes.add(targetClass);
   for (Class<?> clazz : classes) {
      Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
      for (Method method : methods) {
         if ((introductionAwareMethodMatcher != null &&
               introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
             //適配查詢方法上的@Transactional註解信息  
             methodMatcher.matches(method, targetClass)) {
            return true;
         }
      }
   }

   return false;
}

我們可以在上面的方法打斷點,一步一步調試跟蹤代碼,最終上面的代碼還會調用如下方法來判斷。在下面的方法上斷點,回頭看看方法調用堆棧也是不錯的方式跟蹤。
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
   // Don't allow no-public methods as required.
   //非public 方法,返回@Transactional信息一律是null
   if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
   }
   //後面省略.......
 }

不創建代理對象

所以,如果所有方法上的修飾符都是非public的時候,那麼將不會創建代理對象。以一開始的測試代碼爲例,如果正常的修飾符的testService是下面圖片中的,經過cglib創建的代理對象。
如果class中的方法都是非public的那麼將不是代理對象。

不進行代理調用

考慮一種情況,如下面代碼所示。兩個方法都被@Transactional註解標註,但是一個有public修飾符一個沒有,那麼這種情況我們可以預見的話,一定會創建代理對象,因爲至少有一個public修飾符的@Transactional註解標註方法。
創建了代理對象,insertTestWrongModifier就會開啓事務嗎?答案是不會。

/**
 * @author zhoujy
 **/

@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Override
    @Transactional
    public void insertTest() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }
    
    @Transactional
    void insertTestWrongModifier() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }
}

原因是在動態代理對象進行代理邏輯調用時,在cglib創建的代理對象的攔截函數中 CglibAopProxy.DynamicAdvisedInterceptor#intercept ,有一個邏輯如下,目的是獲取當前被代理對象的當前需要執行的method適配的aop邏輯。

   
   
   
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
而針對@Transactional註解查找aop邏輯過程,相似地,也是執行一次
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
也就是說還需要找一個方法上的@Transactional註解信息,沒有的話就不執行代理@Transactional對應的代理邏輯,直接執行方法。沒有了@Transactional註解代理邏輯,就無法開啓事務,這也是上一篇已經講到的。

第二種場景分析

在類內部調用調用類內部@Transactional標註的方法。這種情況下也會導致事務不開啓。
經過對第一種的詳細分析,對這種情況爲何不開啓事務管理,原因應該也能猜到;
既然事務管理是基於動態代理對象的代理邏輯實現的,那麼如果在類內部調用類內部的事務方法,這個調用事務方法的過程並不是通過代理對象來調用的,而是直接通過this對象來調用方法,繞過的代理對象,肯定就是沒有代理邏輯了。
其實我們可以這樣玩,內部調用也能實現開啓事務,代碼如下。

/**
 * @author zhoujy
 **/

@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Resource
    TestServiceImpl testServiceImpl;


    @Transactional
    public void insertTestInnerInvoke() {
        int re = testMapper.insert(new Test(10,20,30));
        if (re > 0) {
            throw new NeedToInterceptException("need intercept");
        }
        testMapper.insert(new Test(210,20,30));
    }


    public void testInnerInvoke(){
        //內部調用事務方法
        testServiceImpl.insertTestInnerInvoke();
    }

}

上面就是使用了代理對象進行事務調用,所以能夠開啓事務管理,但是實際操作中,沒人會閒的蛋疼這樣子玩~

第三種場景分析

事務方法內部捕捉了異常,沒有拋出新的異常,導致事務操作不會進行回滾。
這種的話,可能我們比較常見,問題就出在代理邏輯中,我們先看看源碼裏賣弄動態代理邏輯是如何爲我們管理事務的。
TransactionAspectSupport#invokeWithinTransaction
代碼如下。

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
      throws Throwable 
{

   // If the transaction attribute is null, the method is non-transactional.
   final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
   final PlatformTransactionManager tm = determineTransactionManager(txAttr);
   final String joinpointIdentification = methodIdentification(method, targetClass);

   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
      // Standard transaction demarcation with getTransaction and commit/rollback calls.
       //開啓事務
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
         // This is an around advice: Invoke the next interceptor in the chain.
         // This will normally result in a target object being invoked.
          //反射調用業務方法
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         // target invocation exception
          //異常時,在catch邏輯中回滾事務
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
       //提交事務
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }

   else {
     //....................
   }
}

所以看了上面的代碼就一目瞭然了,事務想要回滾,必須能夠在這裏捕捉到異常纔行,如果異常中途被捕捉掉,那麼事務將不會回滾。

有道無術,術可成;有術無道,止於術

歡迎大家關注Java之道公衆號


好文章,我在看❤️

本文分享自微信公衆號 - Hollis(hollischuang)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章