情况说明
- 首先开启了AOP,并且同时开启了事务。
- 下面这个TransactionAspect就是一个简单的AOP切面,有一个Around通知。
@Aspect
@Component
public class TransactionAspect {@Pointcut("execution(* com.qhyu.cloud.datasource.service.TransactionService.*(..))") // the pointcut expressionprivate void transactionLogInfo() {} // the pointcut signature/*** Title:around <br>* Description:这个Around吃掉了异常 <br>* 不太建议吃掉异常,出现这个问题的原因需要排查下为什么?* 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调用目标方法前:@Around");try {// aop拦截器proceed = pjp.proceed();} catch (Throwable e) {e.printStackTrace();}System.out.println("TransactionAspect调用目标方法后:@Around");return proceed;}
}
- 创建一个Service和dao,并且需要事务。
public interface TransactionService {void doQuery(String id);void doUpdate(String id);
}@Component
public class TransactionServiceImpl implements TransactionService {@AutowiredTransactionDao transactionDao;@Overridepublic void doQuery(String id) {System.out.println(transactionDao.UserQuery(id));}@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)@Overridepublic void doUpdate(String id) {int i = transactionDao.UserUpdate(id);System.out.println("更新用户表信息"+i+"条");}
}@Component
public class TransactionDao {@Autowiredprivate JdbcTemplate jdbcTemplate;// id例子:0008cce0-3c92-45ea-957f-4f6dd568a3e2public 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;}}
可以看到TransactionServiceImpl的doUpdate方法是被事务管理的。万事具备,只欠东风。
- 启动
private static void transactionTest(AnnotationConfigApplicationContext annotationConfigApplicationContext) {// 事务回滚了吗,测试事务和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);}
- 现象:事务没有回滚,is_first_login标识原来是0,出现异常后数据库显示is_first_login是1,结论就是事务失效了。
问题分析
Spring事物之@EnableTransactionManagemen一章我们说过自动代理创建器是会升级的,所以AnnotationAwareAspectJAutoProxyCreator类就是实现其代理的类。
我这儿TransactionServiceImpl是实现了接口的,并没有强制使用cglib,所以此处用的是JdkDynamicAopProxy生成的代理对象,所以只要我启动项目,就会调用invoke方法。
我需要观察的是Advice的排序问题,因为此处明显是应为Around吃掉了异常才会导致事务失效,但是有时候我们不得不使用Around吃掉异常的时候应该怎么处理就是我们要解决的问题。
所以invoke方法中this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)就是去获取所有的Advice。
ExposeInvocationInterceptor的作用就是在方法调用期间将当前代理对象设置到AopContext中。它是整个AOP拦截器链中的第一个拦截器,确保在后续的拦截器或切面中可以通过AopContext获取到当前代理对象。
所以我们需要关注的就是后面两个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
advice调用过程会先调用TransactionInterceptor,然后才会调用ransactionAspect.around。
TransactionInterceptor
首先会调用TransactionInterceptor的invoke方法,代码如下:
@Override@Nullablepublic 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@Nullablepublic Object proceedWithInvocation() throws Throwable {// 执行被拦截的方法,也就是加了@transaction注解的方法return invocation.proceed();}@Overridepublic Object getTarget() {return invocation.getThis();}@Overridepublic Object[] getArguments() {return invocation.getArguments();}});}
这个方法非常简单,就是执行并返回方法invokeWithinTransaction的内容。
下面是核心代码,这边进行了精简,为了方便观看。
@Nullableprotected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,final InvocationCallback invocation) throws Throwable {// 声明式事务,else逻辑是编程式事务,两种不同的处理方法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.// 执行方法retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// target invocation exception// 异常回滚事务completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {cleanupTransactionInfo(txInfo);}if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {// Set rollback-only in case of Vavr failure matching our rollback rules...TransactionStatus status = txInfo.getTransactionStatus();if (status != null && txAttr != null) {retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);}}// 提交事务commitTransactionAfterReturning(txInfo);return retVal;}}
当执行invocation.proceedWithInvocation()的时候将会执行新建的CoroutinesInvocationCallback() 的proceedWithInvocation方法。invocation.proceed();就会开始调用下一个。也就是我们自定义的Around。
其实看到这里就很清楚了,我们应该让Around先执行,或者让Around不吃掉异常才能让事务生效。
TransactionAspect.around
开始执行我们自定义的around方法,方法如下:
@Around("transactionLogInfo()")public Object around(ProceedingJoinPoint pjp){Object proceed = null;System.out.println("TransactionAspect调用目标方法前:@Around");try {// aop拦截器proceed = pjp.proceed();} catch (Throwable e) {e.printStackTrace();}System.out.println("TransactionAspect调用目标方法后:@Around");return proceed;}
所以会先打印出第一行日志,然后执行pjp.proceed()去调用目的方法doUpdate(),但是目的方法会被吃掉异常,此时执行完成之后再打印Around的第二行日志,最后又回到invokeWithinTransaction,因为异常被吃掉了,所以就直接提交事务了。
排序问题
advice排序这一章我们分析了,order可以改变其排序,具体代码如下:
AbstractAdvisorAutoProxyCreator#findEligibleAdvisors
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {List<Advisor> candidateAdvisors = findCandidateAdvisors();// Around before after afterReturing afterThrowingList<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);extendAdvisors(eligibleAdvisors);if (!eligibleAdvisors.isEmpty()) {// 这里是通过order进行排序的eligibleAdvisors = sortAdvisors(eligibleAdvisors);}return eligibleAdvisors;}
查看两个Interceptors的Order,都是2147483647,所以我们可以调整下顺序,让Around在前面执行 ,然后TransactionInterceptor后执行,然后抛出异常后进入到回滚逻辑,最后走Around的后续逻辑。
解决方案
- 修改我们Advice的Order
@Aspect
@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE+1)
public class TransactionAspect {
-
修改@EnableTransactionManagement(order=)
-
调整后结果符合预期
TransactionAspect调用目标方法前:@Around
{id=0008cce0-3c92-45ea-957f-4f6dd568a3e2, is_first_login=1, lastlanddingtime=null, update_time=2023-10-07 09:24:45}
TransactionAspect调用目标方法后:@Around
java.lang.ArithmeticException: / by zeroat 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)TransactionAspect调用目标方法后:@Around