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.