Let's talk about several scenarios and solutions for the failure of transaction annotation @ Transactional

Posted by zicco on Sun, 27 Feb 2022 02:03:48 +0100

Introduction to Transactional failure scenarios

When the modifier of the first Transactional annotation annotation method is non-public, @ Transactional annotation will not work. For example, the following code.

Define an incorrect @ Transactional annotation implementation and modify a default accessor method

/**
 * @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));
    }

}

In the same package, create a new calling object for access.

@Component
public class InvokcationService {
    @Resource
    private TestServiceImpl testService;
    public void invokeInsertTestWrongModifier(){
        //Call the default accessor method of the @ Transactional annotation
        testService.insertTestWrongModifier();
    }
}

test case

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

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

The above access methods cause the transaction not to be opened, so when the method throws an exception, testmapper insert(new Test(10,20,30)); The operation will not be rolled back. If the TestServiceImpl#insertTestWrongModifier method is changed to public, the transaction will be started normally, testmapper insert(new Test(10,20,30)); Rollback will occur.

Second

Call the method of @ Transactional annotation inside the class. In this case, the transaction will not be opened. The sample code is as follows.

Set an internal call

/**
 * @author zhoujy
 **/
@Component
public class TestServiceImpl implements TestService {
    @Resource
    TestMapper testMapper;

    @Transactional
    public void insertTestInnerInvoke() {
        //Transaction method of normal public modifier
        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(){
        //Class to call the method of @ Transactional annotation.
        insertTestInnerInvoke();
    }

}

Test cases.

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

   @Resource
   TestServiceImpl testService;

   /**
    * Call @ Transactional annotation method inside the test
    */
   @Test
   public void  testInnerInvoke(){
       //Test whether the external call transaction method is normal
      //testService.insertTestInnerInvoke();
       //Test whether the internal calling transaction method is normal
      testService.testInnerInvoke();
   }
}

The above is the test code used. Running the test shows that calling the transaction method externally can start the transaction. Testmapper The insert (new test (10,20,30)) operation will be rolled back;

Then run another test case, call a method, call the transaction method marked by @ Transactional inside the class, and the result is that the transaction will not start normally, testmapper The insert (new test (10,20,30)) operation will be saved to the database and will not be rolled back.              

Third

An exception was caught inside the transaction method and no new exception was thrown, so that the transaction operation will not be rolled back. The sample code is as follows.

/**
 * @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 exception during operation
                throw new NeedToInterceptException("need intercept");
            }
            testMapper.insert(new Test(210,20,30));
        }catch (Exception e){
            System.out.println("i catch exception");
        }
    }
    
}

The test case code is as follows.

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

   @Resource
   TestServiceImpl testService;

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

Running the test case, it is found that although an exception is thrown, the exception is caught and not thrown outside the method. Testmapper The insert (new test (210,20,30)) operation is not rolled back.

The above three are the main reasons why @ Transactional annotation does not work and @ Transactional annotation fails. In combination with the annotation of @ Transactional in spring, the source code is implemented to analyze why the @ Transactional annotation does not work.

@Analysis on the principle that Transactional annotation does not work

First kind

@When the modifier of the annotation method of Transactional annotation is non-public, @ Transactional annotation will not work. The reason for the analysis here is that @ Transactional is implemented based on dynamic proxy. The implementation method is analyzed in the implementation principle of @ Transactional annotation. During bean initialization, create proxy objects for bean instances with @ Transactional annotation. There is a process of spring scanning @ Transactional annotation information, which is unfortunately reflected in the source code, If the modifier of the method marked with @ Transactional is not public, the @ Transactional information of the default method is empty, and the bean will not be created as a proxy object or the method will not be called as a proxy

@In the implementation principle of Transactional annotation, it introduces how to determine whether a bean creates a proxy object. The logic is. Create an aop pointcut BeanFactoryTransactionAttributeSourceAdvisor instance according to spring, traverse the method object of the class of the current bean, and judge whether the annotation information on the method contains @ Transactional. If any method of the bean contains @ Transactional annotation information, it is to adapt the BeanFactoryTransactionAttributeSourceAdvisor pointcut. You need to create a proxy object, and then the proxy logic manages the transaction opening and closing logic for us.

In the spring source code, when intercepting the creation process of beans and looking for the pointcut of bean adaptation, the following methods are used to find the @ Transactional information on the method. If there is, it means that the pointcut BeanFactoryTransactionAttributeSourceAdvisor can be applied to beans,

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;
   }

    //Method object traversing 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)) ||
             //Adapt @ Transactional annotation information on query method
             methodMatcher.matches(method, targetClass)) {
            return true;
         }
      }
   }

   return false;
}

We can debug the tracking code step by step at the break point of the above method. Finally, the above code will call the following methods to judge. It is also a good way to trace breakpoints on the following methods and look back at the method call stack. recommend: Java interview practice dictionary

AbstractFallbackTransactionAttributeSource#getTransactionAttribute

  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
   // Don't allow no-public methods as required.
   //Non public method, the returned @ Transactional information is null
   if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
   }
   //Omit
 }

Do not create proxy objects

Therefore, if the modifiers on all methods are non-public, the proxy object will not be created. Take the initial test code as an example. If the testService of the normal modifier is the proxy object created by cglib in the following picture.

If the methods in the class are non-public, they will not be proxy objects.

No proxy call

Consider a case, as shown in the following code. Both methods are annotated with @ Transactional annotation, but one has a public modifier and the other does not. In this case, we can predict that a proxy object will be created because there is at least one @ Transactional annotation annotation method with a public modifier.

After the proxy object is created, will insertTestWrongModifier start the transaction? The answer is No.

/**
 * @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));
    }
}

The reason is that when the dynamic proxy object makes proxy logic calls, cglibaopproxy. Is in the intercepting function of the proxy object created by cglib Dynamic advised interceptor #intercept has a logic as follows, which aims to obtain the aop logic of the method adaptation of the current proxy object.

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

Similarly, the process of searching aop logic for @ Transactional annotation is also executed once

AbstractFallbackTransactionAttributeSource#getTransactionAttribute

  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

In other words, you need to find the @ Transactional annotation information on a method. If not, the agent logic corresponding to agent @ Transactional will not be executed, and the method will be executed directly. Without the @ Transactional annotation proxy logic, transactions cannot be started, which is also mentioned in the previous article.

Second

Call the method of @ Transactional annotation inside the class. In this case, the transaction will not be opened.

After a detailed analysis of the first, we can guess why transaction management is not enabled in this case;

Since the transaction management is implemented based on the proxy logic of dynamic proxy objects, if the transaction method inside the class is called inside the class, the process of calling the transaction method is not called through the proxy object, but directly through this object. The proxy object bypassed must have no proxy logic.

In fact, we can play this way, and the internal call can also start the transaction. The code is as follows.

/**
 * @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(){
        //Internal call transaction method
        testServiceImpl.insertTestInnerInvoke();
    }

}

The above is the use of proxy objects for transaction calls, so transaction management can be started, but in practice, no one will be idle to play like this~

Third

An exception was caught inside the transaction method and no new exception was thrown, so that the transaction operation will not be rolled back.

In this case, we may be more common. The problem lies in the agent logic. Let's first look at how the dynamic agent logic in the source code manages transactions for us.

TransactionAspectSupport#invokeWithinTransaction

The code is as follows.

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.
       //Open transaction
      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.
          //Reflection calling business method
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         // target invocation exception
          //When an exception occurs, the transaction is rolled back in the catch logic
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
       //Commit transaction
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }

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

Therefore, after reading the above code, it is clear at a glance that if a transaction wants to roll back, it must be able to catch exceptions here. If exceptions are caught halfway, the transaction will not roll back.

The above situations are summarized.

Topics: Java Back-end