✅ 本文案例源码,基于最新Spring Boot 版本2.7.5,Spring 版本是5.3.23
⭐ Spring AOP
是 Spring
中除了依赖注入外(DI)最为核心的功能,AOP 即 为面向切面编程。
⭐ Spring AOP
通过 CGlib
和 JDK
动态代理等方式来实现运行期动态方法增强,目的是将与业务无关的代码单独抽离出来,逻辑解耦,降低系统的耦合性,提高程序的可重用性和开发效率。AOP
在日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用。
🏷️JDK动态代理面向接口,通过反射生成目标代理接口的匿名实现类;
🏷️CGLIB动态代理则通过继承,使用字节码增强技术 为目标代理类生成代理子类。
⭐ Spring
默认对接口实现使用JDK动态代理,对具体类使用CGLIB,同时也支持配置全局使用CGLIB来生成代理对象。
⭐Spring AOP
对 Bean
的增强有5种形式:
org.springframework.aop.BeforeAdvice
):在目标方法执行之前进行增强;org.springframework.aop.AfterReturningAdvice
):在目标方法执行之后进行增强;org.aopalliance.intercept.MethodInterceptor
):在目标方法执行前后都执行增强;org.springframework.aop.ThrowsAdvice
):在目标方法抛出异常后执行增强;org.springframework.aop.IntroductionInterceptor
):为目标类添加新的方法和属性。🏷️ 测试案例:向学生表中新增一条数据,走一遍Spring 事务的执行过程。
⭐ Spring
声明式事务使用AOP 的环绕增强方式,在方法执行之前开启事务,在方法执行之后提交或回滚事务。对应的实现为 TransactionInterceptor
,其实现了 MethodInterceptor
,即,通过AOP的环绕增强方式。
⭐TransactionInterceptor.invoke()
主要有2步:
TargetClass
TransactionAspectSupport #invokeWithinTransaction
处理事务⭐TransactionAspectSupport
是Spring
事务管理的基类,其支持声明式事务、编程式事务。
PlatformTransactionManager
或ReactiveTransactionManager
实现将执行实际的事务管理
PlatformTransactionManager
:声明式事务管理器ReactiveTransactionManager
:编程式事务管理器⭐TransactionAspectSupport #invokeWithinTransaction
事务处理方法主流程如下:
⭐主要源码及注释如下:
⭐1、 获取事务属性源 TransactionAttributeSource
,是一个策略接口,主要用来获取事务属性,根据不同类型的源,确定事务属性。
⭐2、根据属性源获取事务属性TransactionAttribute
,是直接从 事务属性缓存
Map
中获取的。
❓思考:事务属性缓存attributeCache是什么时候缓存的?
⭐Spring 容器初始化时,Bean后置处理器 会识别到 @Transactional
注解 的类和方法,将事务属性放入缓存attributeCache
中,并创建 AOP 代理对象。
⭐TransactionAttribute
继承TransactionDefinitio
事务属性,包含:事务的隔离级别、事务的传播类型、事务超时时间、事务名称、事务是否只读等。
⭐3、确定事务管理器 PlatformTransactionManager
。如果没有指定事务管理器,会获取一个默认的事务管理器(本案例中是 DataSourceTransactionManager
并放到缓存中,之后直接从缓存获取。
PlatformTransactionManager
事务管理器,有3个方法:
getTransaction
:获取当前激活的事务或者创建一个事务commit
:提交当前事务rollback
:回滚当前事务⭐PlatformTransactionManager
是Spring
抽象的接口,其定义了事务的3个基本动作。具体实现是由相应的子类进行的。
DataSourceTransactionManager
使用Jdbc 和 ibatis 进行数据持久化的事务管理器JtaTransactionManager
分布式事务的事务管理⭐4、获取 joinpoint
标识,用于确定事务名称,值是方法全路径。
⭐5、创建事务 createTransactionIfNecessary
joinpoint
标识确定事务名称,主要用于对事务进行监控和记录。TransactionStatus # getTransaction
方法内部会调用相应的事务管理器 开启事务。TransactionInfo
:根据事务状态和事务属性封装 TransactionInfo
事务信息对象,并将其加入到ThreadLocal
中,交给事务管理器进行管理。其中,b 步骤 getTransaction
负责开启事务。见下图中的第4步操作 startTransaction
⭐startTransaction
开启事务方法如下:
⭐doBegin()
是一个抽象方法,不同的事务管理器有对应的实现。
⭐DataSourceTransactionManager # doBegin
开启事务代码如下,至此数据库事务已经开启了。
⭐6、回调执行目标方法:invocation.proceedWithInvocation();
这里就是调用被 Spring AOP 增强的事务方法了。
⭐7、目标方法执行成功后,调用 DataSourceTransactionManager # doCommit
提交事务。
🏷️我们向学生表中插入一条数据:“小荣同学”。紧接着我们抛Exception异常,试图将事务回滚。事务是否会回滚?
数据库插入学生方法打印了日志,并且事务方法抛出了 Exception.class
,如果事务回滚了,那数据库中应该没有数据吧?
❓但发现,数据库中数据保存成功了,为什么事务没有回滚❓
⭐让我们回顾一下事务的处理流程。TransactionAspectSupport #invokeWithinTransaction
执行目标方法(事务方法),如果没有抛异常,则提交事务。如果抛异常,则执行回滚流程,问题就出现在回滚流程中。
异常处理流程,对应下图源码中的第6、7步,如果目标方法抛出异常,则捕获执行第7步回滚流程。
⭐Spring事务管理基类 TransactionAspectSupport
事务异常处理方法 completeTransactionAfterThrowing
🤔从Spring官方注释可以看到,这个方法不仅可以将事务回滚、也有可能提交事务,这取决于配置
Handle a throwable, completing the transaction. 处理异常,完成事务。
We may commit or roll back, depending on the configuration. 执行回滚、提交事务,取决于配置
这个方法逻辑很简单,只有2种情况:
🤔 回滚条件,取决于什么配置❓
⭐txInfo.transactionAttribute.rollbackOn(ex)
这是事务的回滚的关键判断条件。当这个条件满足时,会触发 事务回滚。
然后调用RuleBasedTransactionAttribute #rollbackOn(ex)
判断是否满足回滚规则,对事务进行回滚。
rollbackOn(ex)
有2个层级的判断逻辑
层级1:根据回滚规则和异常层级判断。
如果在 @Transactional
中配置了 rollbackFor
,这个方法就会用捕获到的异常和 rollbackFor
中配置的异常做比较。如果捕获到的异常是 rollbackFor
配置的异常或其子类,就会直接 rollback
。
层级2:如果没有配置 rollbackFor 或者 异常类型没有匹配,则使用父类的 super.rollbackOn(ex)
条件进行判断。
如果没有配置 rollbackFor 事务回滚条件是:只有抛出的异常类型是RuntimeException
或者Error
时才回滚
如果配置了 rollbackFor
则只有捕获到 rollbackFor
配置的异常及其子类,才会回滚事务。
而我们的例子中,抛出的是 Excepton类型异常,并且没有指定rollbackFor属性 ,所以不会回滚事务,而是继续提交事务。提交事务的流程在 案例1 中我们已经分析过了。
🤔怎么改正问题?
rollbackFor @Transactional(rollbackFor = Exception.class)
RuntimeException throw new RuntimeException();
RuntimeException 是 Exception 的子类,如果用 rollbackFor=Exception.class,对 RuntimeException 也会生效。
🤔如果我们需要对 Exception 执行回滚操作,但对于 RuntimeException 不执行回滚操作,应该怎么做呢?
@Transactional(rollbackFor = Exception.class , noRollbackFor = RuntimeExcetion.class)
🏷️测试场景:通过
this
调用事务方法。方法向学生表中插入数据,之后抛出异常。事务是否回滚?
从日志看,向学生表插入数据,并且事务方法抛出了异常。
从数据库看,数据保存成功了,没有回滚
🤔通过 this 调用事务方法,为什么事务没有生效❓
通过断点调试,Controller
中 注入的 StudentService
是被Spring AOP 通过 CGLIB动态代理增强过的类。
而 StudentService
中 通过 this
调用事务方法,this
就是一个普通的 对象,不是被Spring 代理过的类。
只有通过Spring 动态代理过的类调用,(AOP)事务才会生效。使用 this 调用事务方法,是一个普通对象,不具有AOP增强功能,事务自然不会生效。
🤔怎么改正问题?
通过代理对象调用事务方法:使用 @Autowired 注入 StudentService
通过断点我们看到 现在是通过代理类调用事务方法了,此时(AOP)事务已经生效。
🏷️测试场景:给 private 方法添加事务。向学生表中插入数据,之后抛出异常。事务是否回滚?
从日志看,向学生表插入数据,并且事务方法抛出了异常。
从数据库看,数据保存成功了,没有回滚
🤔通过 private 修饰的事务方法,为什么事务没有生效❓
这个问题,需要从Spring AOP动态代理的处理寻找答案。
Spring 代理操作的入口是 AbstractAutoProxyCreator # postProcessAfterInitialization
wrapIfNecessary
首先获取满足条件的切面数组,然后为其创建代理。
是否满足代理条件,最终会调用AopUtils
的 canApply
方法,看是否满足代理条件。
事务方法的匹配规则 会 调用事务属性源切点 TransactionAttributeSourcePointcut 的 matches
方法
这个方法的主要逻辑是:根据事务属性源获取事务属性,只要事务属性不为空即匹配成功。
AbstractFallbackTransactionAttributeSource # getTransactionAttribute
,这个方法如果缓存中有值,则返回缓存中的值,否则 computeTransactionAttribute
计算得到事务属性,并且加入到缓存中。
注意这里,我们代理对象还没有创建,此时缓存中是没有值的。
computeTransactionAttribute
其主要功能是根据方法和类的类型确定是否返回事务属性
注意这个判断条件
allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())
,
当这个判断结果为 true
的时候返回 nul
,也就意味着这个方法不会被代理,从而导致事务的注解不会生效。
条件 1:allowPublicMethodsOnly()
,这个方法意思是:是否只允许公共方法具有事务属性,默认是true的。
条件2:Modifier.isPublic(method.getModifiers())
,这个方法是判断当前方法是不是public的。这个方法里通过位运算,只有public 方法才会返回true。
Spring 默认使用动态代理方式对目标方法执行AOP增强,使用private修饰事务方法,会导致类无法被Spring Cglib代理,无法进行AOP增强,也就是事务不会生效。
只有使用 public 修饰的事务方法,事务才会生效。
🤔 思考:使用final、static 修饰的类可以被代理吗❓
🏷️测试场景。新增一个学生,然后记录日志。记录日志异常回滚了,但是不希望影响新增学生的流程,学生会保存成功吗?
保存学生时,我们catch
住了日志的异常,希望它不会影响新增学生的结果。
记录日志方法中,我们模拟日志保存失败了,并抛出异常,让新增日志的事务回滚
注意这是一个嵌套事务场景,saveStudent
方法 使用 @Transactional
注解开启了事务,saveStudent
方法中调用记录日志方法 logsService.saveLog(logs);
这个方法也开启了事务。
运行结果:
通过数据库记录看到,日志表回滚了,学生表也回滚了。为什么?
在这个例子中,我们在一个事务中开启了另一个事务,其实就是事务传播的问题。
Spring 支持7种事务传播类型,具体的传播类型是通过 propagation
属性控制的。
本例中,我们使用Spring 默认的事务传播类型,记录日志事务发现已经存在一个事务了,则加入该事务,也就是2个方法公用一个事务,这就好理解了,内层事务事务回滚,外层事务肯定也回滚。
原理我们知道了,所以我们要解决这个问题,在内存事务中 指定事务传播类型属性为REQUIRES_NEW
或 NESTED
即可。
🤔Spring 是如何处理嵌套事务的呢❓
⭐再回顾一下事务的执行流程。注意我们使用嵌套事务,所以通过AOP增强 会执行2次这个方法。
TransactionAspectSupport # invokeWithinTransaction
事务处理方法主流程如下:
主要源码及注释如下:
首先,内层事务由于抛出异常,会执行上图第7步:异常回滚流程。即 completeTransactionAfterThrowing
方法
completeTransactionAfterThrowing
会检查事务配置是否满足回滚条件,内层事务我们手动指定回滚Exception
类型异常,是满足回滚条件的。@Transactional(rollbackFor = Exception.class)
(至于为什么满足回滚条件,我们在 案例2:事务异常处理 已经分析过了)
紧接着,上图 1.1 步骤 执行回滚。最终会调用
AbstractPlatformTransactionManager # processRollback
事务的回滚流程。
⭐processRollback
回滚流程主要有3种情况:
本案例中,事务传播使用Spring 默认的传播属性REQUIRED
,属于第 3 种情况,执行第 3 种 情况的流程。先进行2个条件判断:
⭐条件1:status.isLocalRollbackOnly()
:默认值是false
,只有 手动调用 TransactionStatus # setRollbackOnly
时,才会设置为true
;
⭐条件2:isGlobalRollbackOnParticipationFailure()
:这个的默认值是true;这个属性的意思是:是否回滚交由外层事务决定。
条件1、条件2 全部满足,则执行 3.2 步骤 回滚标记流程 DataSourceTransactionManager#doSetRollbackOnly
。这个方法最终调用 setRollbackOnly()
。
至此,内层事务的异常回滚流程执行完毕。我们通过源码发现:内存事务没有立即回滚,而是设置了一个回滚标志 setRollbackOnly()
。
⭐继续回到外层事务,在外层事务中,我们catch
住了内层事务抛出的异常,所以外层事务并不会执行异常回滚流程,而是走提交事务流程 即 commitTransactionAfterReturning
。
然后调用事务管理器的提交事务方法:AbstractPlatformTransactionManager#commit
注意,这个方法中如果满足回滚条件,则执行回滚事务流程;否则提交事务。
⭐回滚流程中有2个判断条件:
shouldCommitOnGlobalRollbackOnly()
,含义是回滚事务时,是否提交事务。这个默认值是 false。defStatus.isGlobalRollbackOnly()
,而这个标记正是内层事务设置的回滚标记。至此,外层事务由于内层设置了回滚标记,也满足了回滚条件。调用AbstractPlatformTransactionManager#processRollback
回滚。
至此,外层事务也执行了数据库回滚操作。
Spring事务使用的 注意事项如下:
✅ 通过this
调用事务方法时,this
对象是普通的对象,不是被代理过的类,所以事务不会生效。
✅ 通过private
修饰的事务方法,无法被Spring 动态代理,所以事务不会生效。事务方法必须使用public
修饰。
✅ 通过final、static
修饰的事务方法,由于方法无法被重写,无法被Spring 动态代理,所以事务不会生效。
✅Spring 默认只会对抛出RuntimeException
或 Erro
r 的异常类型进行回滚,如果不指定rollbackFor
并在程序中抛出Exception
,事务是不会回滚的。
✅阿里巴巴的开发者规范中,要求 rollbackFor
显示指定要回滚的异常类型 。一般建议设置成Exception
,只要是指定的异常类型及其子类,才会回滚事务。
✅小心嵌套事务!Spring事务的默认传播类型是 REQUIRED
,如果涉及嵌套事务,请务必确认事务传播类型对业务的影响,以确保业务正确性。
✅避免大事务!大事务在并发情况下有很多问题,包括:连接池打满、事务回滚时间长、锁等待、死锁等;所以我们要避免使用大事务!比如:
🎉 如果这篇文章对你有帮助,点赞👍 收藏⭐ 关注✅ 哦,创作不易,感谢!😀