Spring 事务失效场景深度解析
Spring 事务基于 AOP 动态代理实现,事务失效的本质是:事务相关的 AOP 增强逻辑未被触发。本文从 “代理机制” 切入,逐一拆解 6 种常见失效场景的底层原因,并给出可落地的解决方案,尤其重点讲解 “this 调用失效” 这一难点。
事务失效的核心前提:理解 Spring 事务的 AOP 原理
在分析失效场景前,必须先明确 Spring 事务的执行流程(基于 CGLIB 代理,最常见场景):
- 代理类生成:Spring 为标注
@Transactional
的目标 Bean(如UserService
)生成 CGLIB 代理类(继承自目标 Bean); - 外部调用触发增强:当外部代码调用代理类的方法时,代理类会先执行 事务增强逻辑(开启事务),再调用目标 Bean 的原始方法;
- 目标方法执行:目标 Bean 执行业务逻辑,若正常完成则代理类触发事务提交,若抛出异常则触发回滚;
- 内部调用绕过增强:若目标 Bean 内部通过
this
调用自身方法,this
指向目标 Bean 本身(非代理类),会直接执行原始方法,跳过代理类的增强逻辑 → 事务失效。
6 种事务失效场景与底层解析
1. 场景 1:事务方法非 public 修饰
底层原因
Spring AOP 动态代理(JDK/CGLIB)仅对 public 方法生效:
- JDK 代理:基于接口实现,仅代理接口中的 public 方法;
- CGLIB 代理:基于继承生成子类,非 public 方法(private/protected/default)无法被重写,代理类无法插入增强逻辑。
Spring 源码佐证(AbstractFallbackTransactionAttributeSource
):
1 | protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass){ |
若方法非 public,Spring 会忽略其 @Transactional
注解,不生成事务增强。
示例(失效代码)
1 |
|
解决方案
将事务方法改为 public 修饰:
1 |
|
2. 场景 2:事务类未被 Spring 容器管理
底层原因
Spring 事务依赖 IOC 容器:只有被 Spring 管理的 Bean(标注 @Component
/@Service
/@Repository
等,或通过 @Bean
注册),才会被扫描并生成事务代理类。若类未被容器管理,@Transactional
注解仅为普通注解,无任何作用。
示例(失效代码)
1 | // 未标注 @Service,未被 Spring 管理 |
解决方案
给类添加 Spring 组件注解,确保被容器扫描管理:
1 | // 标注为容器 Bean |
3. 场景 3:同类内 this 调用事务方法(最易踩坑)
这是最常见且最难理解的失效场景,下面结合代理机制彻底讲透。
底层原因
this
指向 目标 Bean 本身,而非 Spring 生成的代理类,导致事务增强逻辑未被触发。具体流程如下:
- 外部调用代理类方法:若外部调用
proxy.updateUser()
(代理类方法),会先执行事务增强(开启事务),再调用目标 Bean 的userService.updateUser()
; - 内部 this 调用:若目标 Bean 内部用
this.updateOrder()
调用自身方法,this
是目标 Bean 实例(如userService
),直接执行updateOrder()
原始方法,跳过代理类的事务增强 → 事务失效。
源码分析佐证
以 DynamicAdvisedInterceptor
(CGLIB 代理的核心拦截器)为例:
1 | public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { |
- 外部调用时:触发
chain
(事务增强链),执行proceed()
走增强逻辑; - 内部
this
调用时:直接执行methodProxy.invoke(target, args)
,调用目标 Bean 方法,无增强。
示例(失效代码)
1 |
|
解决方案:暴露代理类,内部调用代理对象
通过配置暴露代理类,让内部调用也能获取到代理对象,从而触发事务增强。步骤如下:
- 开启代理暴露(二选一):
- XML 配置:
<aop:config expose-proxy="true"/>
- 注解配置(Spring Boot):
@EnableAspectJAutoProxy(exposeProxy = true)
- XML 配置:
- 内部调用代理对象:
通过AopContext.currentProxy()
获取代理类,再调用目标方法(而非this
)。
修复后代码
1 |
|
关键原理
expose-proxy="true"
会将代理类存入 ThreadLocal
(AopContext
),内部调用时通过 AopContext.currentProxy()
取出代理类,此时调用的是代理类的方法,会执行事务增强逻辑。
4. 场景 4:未捕获 RuntimeException 或自定义异常未指定回滚规则
底层原因
Spring 事务默认仅对 RuntimeException(运行时异常)和 Error 触发回滚,对 Checked Exception(编译时异常,如 IOException
) 不回滚。若业务抛出 Checked Exception,或捕获异常后未重新抛出,Spring 无法感知异常,会直接提交事务。
示例 1:抛出 Checked Exception(失效)
1 |
|
示例 2:捕获异常未抛出(失效)
1 |
|
解决方案
- 自定义异常回滚:通过
rollbackFor
指定需要回滚的异常类型; - 捕获异常需重新抛出:若必须捕获异常,需在
catch
中重新抛出(或抛出RuntimeException
)。
修复后代码
1 |
|
5. 场景 5:事务传播行为配置错误
底层原因
选择了不支持事务的传播行为,导致事务无法生效或被挂起。常见错误传播行为:
PROPAGATION_NOT_SUPPORTED
:以非事务方式执行,若当前有事务则挂起;PROPAGATION_NEVER
:以非事务方式执行,若当前有事务则抛出异常;PROPAGATION_SUPPORTS
:若当前无事务,则以非事务方式执行(仅依赖外部事务)。
示例(失效代码)
1 |
|
解决方案
选择支持事务的传播行为(根据业务场景):
- 默认值
PROPAGATION_REQUIRED
:最常用,支持当前事务,无则新建; PROPAGATION_REQUIRES_NEW
:新建独立事务,与外部事务隔离;PROPAGATION_NESTED
:嵌套事务,可独立回滚。
修复后代码
1 |
|
6. 场景 6:事务方法所在类是 final 或方法是 final
底层原因
Spring 事务依赖 CGLIB 代理(继承目标类),若目标类或方法是 final
:
- final 类:CGLIB 无法生成子类(继承被禁止),无法创建代理类;
- final 方法:无法被 CGLIB 重写,代理类无法插入事务增强逻辑。
示例(失效代码)
1 | // final 类:CGLIB 无法生成代理,事务失效 |
解决方案
- 移除类或方法的
final
修饰符; - 若必须用
final
,改用 JDK 代理(目标类实现接口),但 JDK 代理仍不支持final
方法(接口方法无法是final
),因此根本解决方案是取消 final 修饰。
事务失效场景总结与排查思路
1. 失效场景汇总表
失效场景 | 底层原因 | 解决方案 |
---|---|---|
非 public 方法 | 代理仅对 public 方法生效 | 改为 public 修饰 |
类未被 Spring 管理 | 无代理类生成 | 加 @Component /@Service 等注解 |
同类内 this 调用 | this 指向目标 Bean,跳过代理增强 | 开启 expose-proxy,用 AopContext 取代理 |
异常未抛出或类型不匹配 | Spring 无法感知异常,不触发回滚 | 抛出异常,配置 rollbackFor |
传播行为错误 | 选择了不支持事务的传播行为 | 改用 REQUIRED/REQUIRES_NEW 等 |
类 / 方法是 final | CGLIB 无法生成代理或重写方法 | 移除 final 修饰符 |
2. 排查思路(三步法)
- 检查代理是否生成:通过
context.getBean(UserService.class).getClass()
查看返回类型,若含 $$$EnhancerByCGLIB$$$ 则代理生成成功; - 检查事务增强是否触发:在事务方法前后加日志,或 debug 查看是否进入
TransactionInterceptor
(事务拦截器); - 检查异常是否被捕获:确保异常未被
try-catch
消化,且异常类型符合rollbackFor
配置。
关键注意点
- JDK 代理 vs CGLIB 代理:两种代理在 “this 调用”“final 方法” 等场景下失效规则一致,核心是 “增强逻辑是否被触发”;
rollbackFor
建议显式配置:默认仅回滚 RuntimeException,实际开发中建议显式配置rollbackFor = Exception.class
,避免因异常类型遗漏导致失效;- 避免过度依赖内部调用:尽量将事务方法拆分到不同类中,通过外部调用触发事务(减少
this
调用场景)
v1.3.10