Spring transaction AOP causes transaction failure problem

Situation Description

  • First, AOP was enabled, and transactions were enabled at the same time.
  • The TransactionAspect below is a simple AOP aspect with an Around notification.
@Aspect
@Component
public class TransactionAspect {<!-- -->

@Pointcut("execution(* com.qhyu.cloud.datasource.service.TransactionService.*(..))") // the pointcut expression
private void transactionLogInfo() {<!-- -->} // the pointcut signature


/**
*Title: around<br>
* Description: This Around ate the exception<br>
* It is not recommended to eat exceptions. The reason for this problem needs to be investigated. Why?
* author:candidate<br>
* date: 2023/11/10 14:11<br>
* @param
* @return
*/
@Around("transactionLogInfo()")
public Object around(ProceedingJoinPoint pjp){<!-- -->
Object proceed = null;
System.out.println("TransactionAspect before calling the target method: @Around");
try {<!-- -->
// aop interceptor
proceed = pjp.proceed();
} catch (Throwable e) {<!-- -->
e.printStackTrace();
}
System.out.println("After TransactionAspect calls the target method: @Around");
return proceed;
}
}
  • Create a Service and dao, and require transactions.
public interface TransactionService {<!-- -->

void doQuery(String id);

void doUpdate(String id);
}


@Component
public class TransactionServiceImpl implements TransactionService {<!-- -->

@Autowired
TransactionDao transactionDao;
  
@Override
public void doQuery(String id) {<!-- -->
System.out.println(transactionDao.UserQuery(id));
}

@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
@Override
public void doUpdate(String id) {<!-- -->
int i = transactionDao.UserUpdate(id);
System.out.println("Update user table information" + i + "item");
}
}

@Component
public class TransactionDao {<!-- -->

@Autowired
private JdbcTemplate jdbcTemplate;


// ID example: 0008cce0-3c92-45ea-957f-4f6dd568a3e2
public Object UserQuery(String id){<!-- -->
return jdbcTemplate.queryForMap("select * from skyworth_user where id = ?",id);
}


@SuppressWarnings({<!-- -->"divzero"})
public int UserUpdate(String id) throws RuntimeException{<!-- -->
Map<String, Object> resultMap = jdbcTemplate.queryForMap("select * from skyworth_user where id = ?", id);
int flag = 0;
if (resultMap.get("is_first_login") == Integer.valueOf("0")){<!-- -->
flag = 1;
}
int update = jdbcTemplate.update("update skyworth_user set is_first_login = ? where id ='0008cce0-3c92-45ea-957f-4f6dd568a3e2' ", flag);
int i=1/0;
return update;
}

}

You can see that the doUpdate method of TransactionServiceImpl is managed by transactions. Everything is ready, all we need is the east wind.

  • start up
private static void transactionTest(AnnotationConfigApplicationContext annotationConfigApplicationContext) {<!-- -->
// Has the transaction been rolled back? Use it when testing transactions and aop.
TransactionService bean2 = annotationConfigApplicationContext.getBean(TransactionService.class);
bean2.doQuery("0008cce0-3c92-45ea-957f-4f6dd568a3e2");
bean2.doUpdate("0008cce0-3c92-45ea-957f-4f6dd568a3e2");
bean2.doQuery("0008cce0-3c92-45ea-957f-4f6dd568a3e2");
}

public static void main(String[] args) {<!-- -->
AnnotationConfigApplicationContext annotationConfigApplicationContext =
new AnnotationConfigApplicationContext(AopConfig.class);
transactionTest(annotationConfigApplicationContext);
}
  • Phenomenon: The transaction is not rolled back, and the is_first_login flag is originally 0. After an exception occurs, the database shows that is_first_login is 1. The conclusion is that the transaction has failed.

Problem analysis

In the chapter of Spring Things @EnableTransactionManagemen, we said that the automatic proxy creator will be upgraded, so the AnnotationAwareAspectJAutoProxyCreator class is the class that implements its proxy.

My TransactionServiceImpl implements the interface and does not force the use of cglib, so the proxy object generated by JdkDynamicAopProxy is used here, so as long as I start the project, the invoke method will be called.

What I need to observe is the sorting problem of Advice, because here it is obvious that the exception is eaten by Around, which will cause the transaction to fail. But sometimes we have to use Around to eat the exception. How to deal with it is the problem we have to solve. .

So this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass) in the invoke method is to get all the Advice.

The role of ExposeInvocationInterceptor is to set the current proxy object to the AopContext during method invocation. It is the first interceptor in the entire AOP interceptor chain, ensuring that the current proxy object can be obtained through AopContext in subsequent interceptors or aspects.

So what we need to focus on are the next two Interceptors:

  • org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor: advice org.springframework.transaction.interceptor.TransactionInterceptor@3918c187
  • InstantiationModelAwarePointcutAdvisor: expression [transactionLogInfo()]; advice method [public java.lang.Object com.qhyu.cloud.aop.aspect.TransactionAspect.around(org.aspectj.lang.ProceedingJoinPoint)]; perClauseKind=SINGLETON

The advice calling process will first call TransactionInterceptor and then call ransactionAspect.around.

TransactionInterceptor

First, the invoke method of TransactionInterceptor will be called. The code is as follows:

 @Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {<!-- -->
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {<!-- -->
@Override
@Nullable
public Object proceedWithInvocation() throws Throwable {<!-- -->
//Execute the intercepted method, that is, the method annotated with @transaction
return invocation.proceed();
}
@Override
public Object getTarget() {<!-- -->
return invocation.getThis();
}
@Override
public Object[] getArguments() {<!-- -->
return invocation.getArguments();
}
});
}

This method is very simple, it is to execute and return the content of the invokeWithinTransaction method.

The following is the core code, which has been simplified for easier viewing.

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {<!-- -->
\t
// Declarative transactions, else logic is programmatic transactions, two different processing methods
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {<!-- -->
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

Object retVal;
try {<!-- -->
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
// execution method
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {<!-- -->
// target invocation exception
//Exception rollback transaction
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {<!-- -->
cleanupTransactionInfo(txInfo);
}

if (retVal != null & amp; & amp; vavrPresent & amp; & amp; VavrDelegate.isVavrTry(retVal)) {<!-- -->
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null & amp; & amp; txAttr != null) {<!-- -->
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}
// Submit transaction
commitTransactionAfterReturning(txInfo);
return retVal;
}

}

When invocation.proceedWithInvocation() is executed, the proceedWithInvocation method of the newly created CoroutinesInvocationCallback() will be executed. invocation.proceed(); will start calling the next one. That is our customized Around.

In fact, it is very clear after seeing this. We should let Around execute first, or let Around not eat the exception before the transaction can take effect.

TransactionAspect.around

Start executing our custom around method as follows:

@Around("transactionLogInfo()")
public Object around(ProceedingJoinPoint pjp){<!-- -->
Object proceed = null;
System.out.println("TransactionAspect before calling the target method: @Around");
try {<!-- -->
// aop interceptor
proceed = pjp.proceed();
} catch (Throwable e) {<!-- -->
e.printStackTrace();
}
System.out.println("After TransactionAspect calls the target method: @Around");
return proceed;
}

Therefore, the first line of the log will be printed out first, and then pjp.proceed() will be executed to call the destination method doUpdate(), but the destination method will be eaten with an exception. After the execution is completed, the second line of the Around log will be printed, and finally Back to invokeWithinTransaction, because the exception was eaten, the transaction was submitted directly.

Sort problem

In the chapter of advice sorting, we analyzed that order can change its sorting. The specific code is as follows:

AbstractAdvisorAutoProxyCreator#findEligibleAdvisors

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {<!-- -->
\t
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// Around before after afterReturing afterThrowing
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {<!-- -->
// This is sorted by order
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

Looking at the Order of the two Interceptors, they are both 2147483647, so we can adjust the order so that Around is executed in front, then TransactionInterceptor is executed after, and then after an exception is thrown, it enters the rollback logic, and finally follows the follow-up logic of Around.

Solution

  • Modify the Order of our Advice
@Aspect
@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE + 1)
public class TransactionAspect {<!-- -->
  • Modify @EnableTransactionManagement(order=)

  • Adjusted results were in line with expectations

Before TransactionAspect calls the target method: @Around
{id=0008cce0-3c92-45ea-957f-4f6dd568a3e2, is_first_login=1, lastlanddingtime=null, update_time=2023-10-07 09:24:45}
After TransactionAspect calls the target method: @Around
java.lang.ArithmeticException: / by zero
at com.qhyu.cloud.datasource.dao.TransactionDao.UserUpdate(TransactionDao.java:43)
at com.qhyu.cloud.datasource.service.impl.TransactionServiceImpl.doUpdate(TransactionServiceImpl.java:33)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:128)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:413)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:123)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)
at com.qhyu.cloud.aop.aspect.TransactionAspect.around(TransactionAspect.java:48)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:224)
at com.sun.proxy.$Proxy34.doUpdate(Unknown Source)
at com.qhyu.cloud.QhyuApplication.transactionTest(QhyuApplication.java:88)
at com.qhyu.cloud.QhyuApplication.main(QhyuApplication.java:22)

After TransactionAspect calls the target method: @Around