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

情况说明

  • 首先开启了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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/171443.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Android】画面卡顿优化列表流畅度四之Glide几个常用参数设置

好像是一年前快两年了&#xff0c;笔者解析过glide的源码&#xff0c;也是因为觉得自己熟悉一些&#xff0c;也就没太关注过项目里glide的具体使用对当前业务的影响&#xff1b;主要是自负&#xff0c;还有就是真没有碰到过这样的数据加载情况。暴露了经验还是不太足够 有兴趣的…

Vue知识点总结

路由 使用 参数传递的两种方式 路由的params传参 路由的query传参 组件 概念 局部功能代码&#xff08;html、css js&#xff09;和资源(mp3 mp4 ttf .zip)的集合 非单文件组件 一个文件对应多个组件&#xff0c;以html结尾 使用 <xuexiao>即可使用 注意&#xf…

nacos适配达梦数据库

一、下载源码 源码我直接下载gitee上nacos2.2.3的&#xff0c;具体链接&#xff1a;https://gitee.com/mirrors/Nacos/tree/2.2.3&#xff0c;具体如下图&#xff1a; 二、集成达梦数据库驱动 解压源码包&#xff0c;用idea打开源码&#xff0c;等idea和maven编译完成&#xff…

STM32--时钟树

一、什么是时钟&#xff1f; 时钟是单片机的脉搏&#xff0c;是系统工作的同步节拍。单片机上至CPU&#xff0c;下至总线外设&#xff0c;它们工作时序的配合&#xff0c;都需要一个同步的时钟信号来统一指挥。时钟信号是周期性的脉冲信号。 二、什么是时钟树&#xff1f; S…

【分享】Excel“只读方式”的两种模式

查阅Excel表格的时候&#xff0c;担心不小心修改了内容&#xff0c;可以给Excel设置以“只读方式”打开&#xff0c;这样就算修改了内容也不能直接保存表格。Excel表格可以设置两种“只读方式”&#xff0c;一起来看看吧&#xff01; “只读方式” 1&#xff1a; 打开Excel表…

图论15-有向图-环检测+度数+欧拉回路

文章目录 1. 有向图设计1.1 私有变量标记是否有向1.2 添加边的处理&#xff0c;双向变单向1.3 删除边的处理&#xff0c;双向变单向1.4 有向图的出度和入度 2 有向图的环检测2.1 普通的算法实现换检测2.2 拓扑排序中的环检测 3 欧拉回路 1. 有向图设计 1.1 私有变量标记是否有…

Direct3D拾取

假设在屏幕上单击,击中的位置为点s=(x,y)。由图可以看出,用户选中了茶壶。但是仅给出点s,应用程序还无法立即判断出茶壶是否被选中。所以针对这类问题,我们需要采用一项称为“拾 取(Picking)”的技术。 茶壶和屏幕点s之间的一种联系是茶壶被投影到了一个包含了s的区域中。…

【NLP】理解 Llama2:KV 缓存、分组查询注意力、旋转嵌入等

LLaMA 2.0是 Meta AI 的开创性作品&#xff0c;作为首批高性能开源预训练语言模型之一闯入了 AI 场景。值得注意的是&#xff0c;LLaMA-13B 的性能优于巨大的 GPT-3(175B)&#xff0c;尽管其尺寸只是其一小部分。您无疑听说过 LLaMA 令人印象深刻的性能&#xff0c;但您是否想知…

ChatGPT重磅升级 奢侈品VERTU推出双模型AI手机

2023年11月7日,OpenAI举办了首届开发者大会,CEO Sam Altman(山姆奥尔特曼)展示了号称“史上最强”AI的GPT-4 Turbo。它支持长达约10万汉字的输入,具备前所未有的长文本处理能力,使更复杂的互动成为可能。此外,GPT-4 Turbo还引入了跨模态API支持,可以同时处理图片、视频和声音,从…

uniapp h5发行

前端使用uniapp开发项目完成后&#xff0c;需要将页面打包&#xff0c;生成H5的静态文件&#xff0c;部署在服务器上。 这样通过服务器链接地址&#xff0c;直接可以在手机上点开来访问。 打包全步骤如下&#xff1a; 首先在manifest.json文件中进行基础配置&#xff0c;获取…

Python | 机器学习之数据清洗

​ &#x1f308;个人主页&#xff1a;Sarapines Programmer&#x1f525; 系列专栏&#xff1a;《人工智能奇遇记》&#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 目录结构 1. 机器学习之数据清洗概念 1.1 机器学习 1.2 数据清洗 2. 数据清洗 2.1 实验目的…

XSS 跨站点脚本漏洞详解

文章目录 漏洞概述XSS漏洞原理xss漏洞危害xss漏洞验证XSS漏洞分类反射型存储型DOM型 固定会话攻击原理简单xss注入复现 XSS 攻防xss构造方法利用标签符号<>事件响应javascript伪协议其他标签 XSS 变形方式xss防御黑白名单策略输入过滤 防御DOM 型XSS过滤代码测试代码 案例…