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

情况说明

  • 首先开启了AOP,并且同时开启了事务。
  • 下面这个TransactionAspect就是一个简单的AOP切面,有一个Around通知。
@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,并且需要事务。
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方法是被事务管理的。万事具备,只欠东风。

  • 启动
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
	@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的内容。

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

@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方法,方法如下:

@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 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
@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 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于 Letterize.js + Anime.js 实现炫酷文本特效

如上面gif动图所示&#xff0c;这是一个很炫酷的文字动画效果&#xff0c;文字的每个字符呈波浪式的扩散式展开。本次文章将解读如何实现这个炫酷的文字效果。 基于以上的截图效果可以分析出以下是本次要实现的主要几点&#xff1a; 文案呈圆环状扩散开&#xff0c;扩散的同时…

YOLOv8-Seg改进:分割注意力系列篇 | 轻量级上采样CARAFE算子,助力小目标分割

🚀🚀🚀本文改进:轻量级上采样CARAFE算子,引入到YOLOv8,neck处 Upsanple替换为CARAFE 🚀🚀🚀CARAFE在小目标分割领域中应用广泛 🚀🚀🚀YOLOv8-seg创新专栏:http://t.csdnimg.cn/KLSdv 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; 1)手把手教…

LeetCode | 20. 有效的括号

LeetCode | 20. 有效的括号 OJ链接 这道题可以使用栈来解决问题~~ 思路&#xff1a; 首先我们要使用我们之前写的栈的实现来解决此问题~~如果左括号&#xff0c;就入栈如果右括号&#xff0c;出栈顶的左括号跟右括号判断是否匹配 如果匹配&#xff0c;继续如果不匹配&#…

算法笔记-第五章-质因子分解

算法笔记-第五章-质因子分解 小试牛刀质因子2的个数丑数 质因子分解最小最大质因子约数个数 小试牛刀 质因子2的个数 #include<cstdio> int main() {int n; scanf_s("%d", &n); int count 0; while (n % 2 0) {count; n / 2; }printf("%…

【Mysql系列】Mysql基础篇

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

部署 KVM 虚拟化平台

虚拟化技术的演变过程分为软件模拟、虚拟化层翻译、容器虚拟化三个阶段 1 软件模拟的技术方式 软件模拟是通过软件完全模拟CPU、网卡、芯片组、磁盘等计算机硬件&#xff0c;因为是软件模拟&#xff0c;所以理论上可以模拟任何硬件&#xff0c;甚至不存在的硬件。但是由于是软…

Changes to Captions: An Attentive Network forRemote Sensing Change Captioning

字幕的变化&#xff1a;一个用于遥感变化字幕的关注网络 IEEE Transactions on Image Processing Shizhen Chang, Pedram Ghamisi 2023 摘要&#xff1a;近年来&#xff0c;高级研究集中在使用自然语言处理&#xff08;NLP&#xff09;技术对遥感图像进行直接学习和分析。准…

如何应对招聘中的职业性格测评?

很多同学听说要做性格测试&#xff0c;第一反应是如何让自己的性格让HR看起来更好....没办法为了顺利入职&#xff0c;咱不能老实作答&#xff0c;因为性格测评搞不好是真刷人的&#xff0c;刷人的&#xff08;无视你的专业能力和笔试成绩&#xff09;..... 可是....很多性格测…

C++标准模板(STL)- 类型支持 (受支持操作,检查类型是否拥有未被弃置的析构函数)

类型特性 类型特性定义一个编译时基于模板的结构&#xff0c;以查询或修改类型的属性。 试图特化定义于 <type_traits> 头文件的模板导致未定义行为&#xff0c;除了 std::common_type 可依照其所描述特化。 定义于<type_traits>头文件的模板可以用不完整类型实例…

ARPG----C++学习记录04 Section8 角色类,移动

角色类输入 新建一个角色C&#xff0c;继承建立蓝图,和Pawn一样&#xff0c;绑定输入移动和相机. 在构造函数中添加这段代码也能实现。打开UsePawnControlRotation就可以让人物不跟随鼠标旋转 得到旋转后的向前向量 使用旋转矩阵 想要前进方向和旋转的方向对应。获取当前控制…

Linux可以投屏到电视吗?用网页浏览器就能投屏到电视!

Linux系统的电脑如果要投屏到安卓电视屏幕上&#xff0c;可以使用投屏工具AirDroid Cast的网页版和TV版一起实现。 首先&#xff0c;在Linux系统的电脑里用chrome浏览器或edge浏览器打开webcast.airdroid.com。这就是AirDroid Cast的网页版。你可以看到中间白色框框的右上角有个…

简单地聊一聊Spring Boot的构架

本文由葡萄城技术团队发布。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 前言 本文小编将详细解析Spring Boot框架&#xff0c;并通过代码举例说明每个层的作用。我们将深入探讨Spring Boot的…

GraphQL 与 REST 双重赋能:Hasura 帮你给数据库添加接口 | 开源日报 No.75

hasura/graphql-engine Stars: 30.3k License: Apache-2.0 Hasura GraphQL Engine 是一个开源产品&#xff0c;通过为您的数据提供 GraphQL 或 REST API 以及内置授权来加速 API 开发。它具有以下主要功能和核心优势&#xff1a; 内建强大查询&#xff1a;支持过滤、分页、模…

Cnyunwei

运维管理系统&#xff1a;监控系统 Cnyunwei Centos 6 封装 Cacti、Nagios、Centreon&#xff08;中英文自己切换&#xff09;、Check_MK、Nconf英文版本全部采用与国外官方同步的最新版本&#xff0c;会发布32位和64位两个版本。 安装很简单&#xff0c;直接回车即可全自动安…

如何在TS中使用JS库

在 TypeScript 中使用 JavaScript 库&#xff0c;几种常用的方法。 直接使用&#xff1a;如果 JavaScript 库不提供 TypeScript 类型定义文件&#xff08;.d.ts&#xff09;&#xff0c;您可以直接在 TypeScript 代码中使用该库。您可以通过在 TypeScript 代码的开头添加 //ts-…

模拟接口数据之使用Fetch方法实现

文章目录 前言一、package.json配置mock执行脚本二、封装接口&#xff0c;区分走ajax还是fetch三、创建mock目录&#xff0c;及相关接口文件四、定义接口五、使用mock数据使用模拟数据优化fetch返回数据 六、不使用模拟数据七、对比其他需要使用依赖相关配置如有启发&#xff0…

HTML字符实体

从注释汲取知识&#xff0c;由代码熟悉用法&#xff0c;所以直接看代码吧&#xff01;&#x1f447;&#x1f447;&#x1f447; <body><!-- 空格 --><!-- 三个空格&#xff0c;实际只显示一个 --><div>我 嘎嘎嘎</div><!-- 用字符实体代替…

C++引用 引用做函数参数

一.引用的定义和语法 // 给a取别名为b int &b a; // 修改b的值&#xff0c;a的值也会被修改&#xff0c;因为他们都指向同一个内存空间 b 20; 二.引用的注意事项 1.引用必须初始化如 int&b; 是错误的&#xff0c;因为没有初始化。 2.引用在初始化后&#xff0c;不…

聚观早报 |滴滴发布Q3财报;小鹏G9连续销量排行第一

【聚观365】11月14日消息 滴滴发布Q3财报 小鹏G9连续销量排行第一 XREAL双11实现7倍增长 真我GT5 Pro真机图 2024年智能手机AI功能竞争激烈 滴滴发布Q3财报 滴滴在其官网发布2023年三季度业绩报告。报告显示&#xff0c;三季度滴滴实现总收入514亿元&#xff0c;同比增长…

Command Injection

Command Injection "Command Injection"(命令注入)&#xff0c;其目标是通过一个应用程序在主机操作系统上执行任意命令。当一个应用程序将用户提供的数据&#xff08;如表单、cookies、HTTP头等&#xff09;传递给系统shell时&#xff0c;就可能发生命令注入攻击。在…
最新文章