Spring事务之AOP导致事务失效问题

情况说明

  • 首先开启了AOP,并且同时开启了事务。
  • 下面这个TransactionAspect就是一个简单的AOP切面,有一个Around通知。
java 复制代码
@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:这个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,并且需要事务。
java 复制代码
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("更新用户表信息"+i+"条");
	}
}

@Component
public class TransactionDao {

	@Autowired
	private JdbcTemplate jdbcTemplate;


	// id例子: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;
	}

}

可以看到TransactionServiceImpl的doUpdate方法是被事务管理的。万事具备,只欠东风。

  • 启动
java 复制代码
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方法,代码如下:

java 复制代码
  @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 {
				// 执行被拦截的方法,也就是加了@transaction注解的方法
				return invocation.proceed();
			}
			@Override
			public Object getTarget() {
				return invocation.getThis();
			}
			@Override
			public Object[] getArguments() {
				return invocation.getArguments();
			}
		});
	}

这个方法非常简单,就是执行并返回方法invokeWithinTransaction的内容。

下面是核心代码,这边进行了精简,为了方便观看。

java 复制代码
@Nullable
	protected 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方法,方法如下:

java 复制代码
@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

java 复制代码
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
	
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		// Around before after afterReturing afterThrowing
		List<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
java 复制代码
@Aspect
@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE+1)
public class TransactionAspect {
  • 修改@EnableTransactionManagement(order=)

  • 调整后结果符合预期

properties 复制代码
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 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)

TransactionAspect调用目标方法后:@Around
相关推荐
西猫雷婶5 分钟前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv
kiiila5 分钟前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
初晴~6 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱5813611 分钟前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳31 分钟前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾34 分钟前
scala借阅图书保存记录(三)
开发语言·后端·scala
黑胡子大叔的小屋1 小时前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
ThisIsClark1 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
唐 城1 小时前
curl 放弃对 Hyper Rust HTTP 后端的支持
开发语言·http·rust
星就前端叭1 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc