前言

上个月,同事出于好奇在群里问AOP的环绕通知与事务注解混合用会不会导致出现异常不回滚的情况。这个问题我一下子回答不上来,因为平时没这样用过,在好奇心的驱使下,我调试了半天终于得到结果,今天我就展开讲讲。(源码解读在最后面,感兴趣的可以看看。)

结论

首先告诉大家的是,同时使用AOP环绕通知和事务注解之后,最终生成的拦截器链的相对顺序是事务的拦截器在前面,AOP环绕通知的拦截器在后面。 在事务的实现中将拦截器的执行过程包裹在了try-catch块中,发生异常后根据配置来决定是否回滚事务。(详见org.springframework.transaction.interceptor.TransactionInterceptor#invoke),因此事务后面的拦截器都会影响事务的执行结果。如果在AOP环绕通知里面将拦截器链执行结果中的异常给吞掉,那么事务就会正常提交而不会回滚。

示例

业务代码

业务代码中直接抛出异常,代码如下所示。

package com.example.demo.aspect;import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;/*** @Author Paul* @Date 2022/7/3 15:52*/
@Service
public class CustomService {@Transactional(rollbackFor = Exception.class)public void echo(){boolean s = true;if (s){throw new RuntimeException("test");}System.out.println("Hello------");}}

环绕通知

环绕通知中捕捉异常并打印日志,代码如下所示。

package com.example.demo.aspect;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;/*** @Author Paul* @Date 2022/7/3 15:49*/
@Component
@Aspect
public class CustomAspect {@Pointcut("execution(* com.example.demo.aspect..*(..))")public void pointcut(){}@Around("pointcut()")public void around(ProceedingJoinPoint joinPoint){try {joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}}}

测试类

这里是用get请求来测试(本来应该用 unit test 来测试的,但是懒得写代码了,手动测试和 UT 的效果一样),代码如下所示。

package com.example.demo.controller;import com.example.demo.aspect.CustomService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Date;@RestController
@RequestMapping("/home")
public class HomeController {private CustomService customService;@Autowiredpublic void setCustomService(CustomService customService) {this.customService = customService;}@GetMapping("/echo")public String echo(){customService.echo();return new Date().toString();}
}

打断点

直接debug更加清晰,给出几个关键的位置,方便大家定位(记得下载spring源码再debug,不然会跟我给出的行数对不上)。

  • org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction line:388和407 (分别对应事务往后执行和最后提交事务的代码)
  • com.example.demo.aspect.CustomAspect#around line:24和26 (自定义环绕通知的代码中的 joinPoint.proceed(),以及异常处理的地方)
  • com.example.demo.aspect.CustomService#echo line:18 (业务代码中抛出异常的地方)

效果描述

启动项目后直接用GET请求访问本地http://localhost:8080/home/echo ,你会发现断点的执行顺序是 org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction line:388com.example.demo.aspect.CustomAspect#around line:24com.example.demo.aspect.CustomService#echo line:18com.example.demo.aspect.CustomAspect#around line:26org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction line:407

现象:同时有环绕通知和事务时,在业务代码中抛出的异常会先被环绕通知处理,所以后面事务会正常提交而不会回滚。

现实情况

现实中,我们很多人喜欢用环绕通知处理一些通用逻辑,那为什么没有出现bug呢?这里我列举下,环绕通知的常见使用场景如下。

  • 打日志
  • 流控

这些场景的共同特点是它们都是在入口层(也就是Web层)做的环绕通知,而我们通常不会把所有逻辑都写在入口层(如果你真的这么做了,我相信Code Review不会通过),而是有一个逻辑处理层(也就是service层)对入口层的数据进行专门处理。因此,我们的事务注解也大都标注在逻辑处理层的方法上。这样看来,确实很少有上面提到的场景。(这里我们可以看到,遵守开发规范在一定程度上对开发效率是有提升的,它可以规避一些bug)

源码解读

说明: 在上面我们介绍了拦截器链的顺序是事务在前,环绕通知在后。这里解读源码的目的是为了搞清楚为什么事务的拦截器在前,环绕通知的拦截器在后。

我们知道事务和环绕通知的最终实现都是通过 AOP,而 spring 默认的 AOP构造类就是 org.springframework.aop.framework.CglibAopProxy,通过getProxy() 完成构造,而getCallbacks()就是构造的关键,可以看到关键代码在DynamicAdvisedInterceptor中,在这个类中完成了拦截器链的构造。(代码就不展示AOP代码了,这里涉及到AOP的太多知识,有兴趣的可以联系我,我可以单独讲讲)。


/*** General purpose AOP callback. Used when the target is dynamic or when the* proxy is not frozen.*/
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {private final AdvisedSupport advised;public DynamicAdvisedInterceptor(AdvisedSupport advised) {this.advised = advised;}@Override@Nullablepublic Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {Object oldProxy = null;boolean setProxyContext = false;Object target = null;TargetSource targetSource = this.advised.getTargetSource();try {if (this.advised.exposeProxy) {// Make invocation available if necessary.oldProxy = AopContext.setCurrentProxy(proxy);setProxyContext = true;}// Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...target = targetSource.getTarget();Class<?> targetClass = (target != null ? target.getClass() : null);List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);Object retVal;// Check whether we only have one InvokerInterceptor: that is,// no real advice, but just reflective invocation of the target.if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {// We can skip creating a MethodInvocation: just invoke the target directly.// Note that the final invoker must be an InvokerInterceptor, so we know// it does nothing but a reflective operation on the target, and no hot// swapping or fancy proxying.Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);retVal = methodProxy.invoke(target, argsToUse);}else {// We need to create a method invocation...retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();}retVal = processReturnType(proxy, target, method, retVal);return retVal;}finally {if (target != null && !targetSource.isStatic()) {targetSource.releaseTarget(target);}if (setProxyContext) {// Restore old proxy.AopContext.setCurrentProxy(oldProxy);}}}@Overridepublic boolean equals(@Nullable Object other) {return (this == other ||(other instanceof DynamicAdvisedInterceptor &&this.advised.equals(((DynamicAdvisedInterceptor) other).advised)));}/*** CGLIB uses this to drive proxy creation.*/@Overridepublic int hashCode() {return this.advised.hashCode();}
}

上面可以看到拦截器链是通过this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);来构造的,最终其实走到org.springframework.aop.framework.DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice,简化后的代码如下。

public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable {@Overridepublic List<Object> getInterceptorsAndDynamicInterceptionAdvice(Advised config, Method method, @Nullable Class<?> targetClass) {// This is somewhat tricky... We have to process introductions first,// but we need to preserve order in the ultimate list.AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();Advisor[] advisors = config.getAdvisors();List<Object> interceptorList = new ArrayList<>(advisors.length);Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());Boolean hasIntroductions = null;for (Advisor advisor : advisors) {if (advisor instanceof PointcutAdvisor) {// Add it conditionally.PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();boolean match;if (mm instanceof IntroductionAwareMethodMatcher) {if (hasIntroductions == null) {hasIntroductions = hasMatchingIntroductions(advisors, actualClass);}match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);}else {match = mm.matches(method, actualClass);}if (match) {MethodInterceptor[] interceptors = registry.getInterceptors(advisor);if (mm.isRuntime()) {// Creating a new object instance in the getInterceptors() method// isn't a problem as we normally cache created chains.for (MethodInterceptor interceptor : interceptors) {interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));}}else {interceptorList.addAll(Arrays.asList(interceptors));}}}}else {Interceptor[] interceptors = registry.getInterceptors(advisor);interceptorList.addAll(Arrays.asList(interceptors));}}return interceptorList;}}

可以看到,最终还是通过Ioc中的org.springframework.aop.Advisor来得到最终的拦截器链。代码里面是遍历 Advisor,判断是否符合条件,把符合条件的拦截器放入最终结果。因此 Advisor 的相对顺序和拦截器链的相对顺序是一致的。

而在SpringBoot启动的时候,会通过spring.factories中配置的相对顺序来自动装配模块。Aop先于事务装载,在装载Aspectj相关模块时会将org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator注册到IOC容器中。当装配事务时,也向IOC容器中注入了org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor。在通过Aop生成的Advisor时,会通过org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#findCandidateAdvisors来找所有的 Advisor,而此时还在IOC刷新阶段,只有事务注册了Advisor,因此会先加载事务相关的Advisor。(详细代码在org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator#findCandidateAdvisors有兴趣的可以自行查阅)。而到DI时再去生成拦截器链时,就会发现事务的拦截器永远在最前面

推荐读物

《Spring 技术内幕》 – 计文柯

这本书我反复读了3遍以上。虽然书是12年出版的,基于Spring Framework 4.X 进行讲解,版本有些旧。但是,当你读完这本书再去看 Spring Framework 5.x 你会发现书上讲的spring核心思想在最新版本中并没有发生太多变化,只是有了些增强。在我们对Spring核心还不太了解的时候如果直接上手最新版本可能会有些复杂,因为有很多优化实现,这样容易让我们陷入细节太深不太能看到系统的全貌。

避坑:@Around与@Transactional混用导致事务无法回滚相关推荐

  1. spring 事务应用误区总结:那些导致事务不回滚的坑

    基于JDBC的 Spring事务在项目中常用来保证数据的一致性, 想要正确的使用,绝不是加一个@Transactional那么简单.最近团队内在排查事务不生效的问题时,就遇到了一个很典型的错误应用的场 ...

  2. java事务中使用try catch 导致事务不回滚的问题

    @Transactional注解的触发,只回滚RuntimeException和Error异常,默认不回滚非RuntimeException异常 解决方法: 1.方法前添加注解(基础的  @Trans ...

  3. 异常被 ”吃“ 掉导致事务无法回滚

    我们在处理异常时,有两种方式,要么抛出去,让上一层来捕获处理:要么把异常 try catch 掉,在异常出现的地方给处理掉.就因为有这中 try-catch,所以导致异常被 "吃" ...

  4. 为什么加了@Transactional注解,事务没有回滚?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 在前天的<事务管理入门>一文发布之后,有读者 ...

  5. 为什么catch了异常,但事务还是回滚了?

    前几天我发了这篇文章<我来出个题:这个事务会不会回滚?>(https://blog.didispace.com/will-this-transcation-rollback/) 得到了很多 ...

  6. spring事务——try{...}catch{...}中事务不回滚的几种处理方式

    当希望在某个方法中添加事务时,我们常常在方法头上添加@Transactional注解 @ResponseBody@RequestMapping(value = "/payment" ...

  7. spring@Transactional注解事务不回滚不起作用无效的问题处理

    这几天在项目里面发现我使用@Transactional注解事务之后,抛了异常居然不回滚.后来终于找到了原因. 如果你也出现了这种情况,可以从下面开始排查. 一.特性 先来了解一下@Transactio ...

  8. @Transactional注解事务不回滚不起作用无效

    @Transactional注解事务不回滚不起作用无效 以下内容均来自http://blog.csdn.net/u011410529/article/details/54287307 转载 这几天在项 ...

  9. SpringBoot 异常回滚 事务的使用___Springboot @Transactional 事务不回滚

    Springboot中事务的使用: 1.启动类加上@EnableTransactionManagement注解,开启事务支持(其实默认是开启的). 2.在使用事务的public(只有public支持事 ...

最新文章

  1. mysql约束建表规范_MySQL 建库建表规范
  2. Java 中 Comparable 接口的意义和用法.
  3. js实现日历框上一日下一日_一日三项令人兴奋的Lucene功能
  4. php文件上传到虚拟主机,php源码上传到虚拟主机(php源码上传到服务器)
  5. C语言全局变量和局部变量深入
  6. ubuntu下 ls: error initializing month strings
  7. win11鼠标怎么在轮滑时只滚动一个屏幕 Windows11鼠标设置轮滑只滚动一个屏幕的步骤方法
  8. 一级造价工程师(安装)- 案例笔记
  9. 800*480bmp图片显示
  10. python-ip端口扫描器
  11. 赴日研修的发展怎么样
  12. 寒假每日一题题解(1.29)摘花生(DP水题)
  13. 读 联想:去尖刀上跳舞!有感
  14. excel职称计算机,2017职称计算机Excel考点:启动与退出
  15. QNAP(威联通)联合创始人郭博达先生一行到访
  16. Choosing Teams
  17. 【MySQL】连接查询
  18. 在北京工作了两年,如今跳槽到了广州,社保公积金该怎样办理?
  19. bst latex 最大作者数_请教!如何修改bst文件,使作者姓名改为“Peng H”这样的格式...
  20. oracle wd2go 转_WD2go的作用是()。

热门文章

  1. 数据仓库数据挖掘——商业智能概述
  2. 计算机与网络 文献,计算机与网络专著参考文献 计算机与网络参考文献怎么写...
  3. android CMWAP CMNET
  4. (转)资管的四个未来?——寻找中国非银金融的Alpha
  5. 简易磁盘写入速度测试工具(GO)
  6. 有用的文章集锦(干货和有效知识)
  7. JSON.parse() 和 JSON.stringify()
  8. python生成简单名片二维码(不带图片)
  9. 喜欢的一首歌(小曲儿唱的别有一番趣味)
  10. 卡牌系统psv游戏推荐_PSV精品游戏推荐之一,让你的小V再次发挥余热吧!