@Transactional 为什么会失效(一):从注解到 Advisor 的装配
引子:注解挂上去了,容器里到底多出了什么
导读把四个阶段铺在了一张图上,这一篇只走第一步:装配。在任何业务 Bean 被创建之前,容器必须先准备好三件事——由谁来判断”这个 Bean 要不要包代理”、包上之后执行什么、以及怎么判断某个方法要不要被拦。
日常卡住的地方,恰恰都在这三件事上:老文章说要加
@EnableTransactionManagement,可从没加过它,事务照样能用;@Aspect
切面和事务写在同一个项目里,说不清它们是不是各有一套代理创建器;读到”protected
方法上的 @Transactional
不生效”,自己试了一下居然回滚了。这些疑问的答案都不在运行期,而在启动时多出来的那几个
Bean 里。
本篇接系列导读那张四阶段图,覆盖第一、二部分:事务的三个组件分别对应 AOP 的哪个概念、
@EnableTransactionManagement导入的两个类各做什么、AutoProxyRegistrar与ProxyTransactionManagementConfiguration注册了哪些 Bean,以及 Boot 自动配置为什么默认用 CGLIB。口径同导读:Spring Framework 6.1.8 + Spring Boot 3.3.x(Java 17)。
本篇要回答的问题:
- 事务的 Advisor、Pointcut、Advice 分别是谁?它和你自己写的
@Aspect是同一套机制吗?- Boot 项目里从不手写
@EnableTransactionManagement,事务是谁打开的?AutoProxyRegistrar到底注册了什么?为什么这个 Bean 名字值得记住?- 事务和
@Aspect是”两个代理创建器并存”,还是”抢同一个 Bean 名”?spring.aop.proxy-target-class这一个开关,为什么能同时决定事务和切面的代理方式?- 只配了数据源却没生成
TransactionManager时,@Transactional为什么会全程静默失效?
第一部分:先看骨架——事务就是一条普通的 AOP 通知
1.1 三个组件,一次对照
Spring 的事务没有任何”底层魔法类”。它老老实实按 AOP 的标准分工拆成了三块,用的全是 AOP 的公开接口:
| AOP 概念 | 事务里的实现类 | 职责 |
|---|---|---|
Advisor |
BeanFactoryTransactionAttributeSourceAdvisor |
把”切哪里”和”切了做什么”绑在一起 |
Pointcut |
TransactionAttributeSourcePointcut |
判断某个方法要不要被切 |
Advice(通知) |
TransactionInterceptor |
真正干活的拦截器 |
| 装配者 | ProxyTransactionManagementConfiguration |
把上面三个注册成 Bean |
对比一下你自己写的切面:
1 |
|
@Aspect 走的是”注解 → 解析 → 生成
Advisor“这条线,由
AnnotationAwareAspectJAutoProxyCreator
在启动时完成解析。
@Transactional
不走注解解析。@EnableTransactionManagement
直接把一个写好的 Advisor 注册成
Bean,然后由自动代理创建器把它捞出来。
两条路,出口完全一样:容器里多了一个
Advisor。这是两者”高度相似”的真正原因——它们不是相似,它们是同一套机制的两个客户。
1.2 一次事务调用的完整链路
把整条链路先画出来,后面的每一个小节都在往这张图上挂细节:
1 | 启动阶段 |
自调用(this.saveOrder())、new
出来的对象、final 方法、private
方法,全都断在”运行阶段入口”之前——它们根本没经过代理。
1.3 把”失效”收敛成四种可能
有了这张图,所有”事务不生效”都能归到下面四类之一,排查顺序也就有了:
- 代理没创建:Bean 没被
BeanPostProcessor加工,或者 Advisor 没匹配上这个类。 - 调用没走代理:方法内部调用自己、
new出来的实例、final/private/static方法。 - 走通了代理,但属性是
null:注解所在的方法不是被调用时的方法(接口注解 + CGLIB 就是典型)。 - 事务开了,但结果不符合预期:异常类型不在回滚规则里、异常被自己吞了、传播行为选错、多线程丢了上下文。
下面按启动顺序,一环一环看。
第二部分:装配阶段——@Transactional
是怎么变成一个 Advisor 的
2.1
入口:@EnableTransactionManagement 到底导入了什么
先纠正一个常见误解:Spring Boot 项目里不需要手写
@EnableTransactionManagement,自动配置已经帮你做了(2.5
节)。但无论谁触发,最终都会走到这个选择器:
1 | // org.springframework.transaction.annotation.TransactionManagementConfigurationSelector |
两个类,对应两件事:
AutoProxyRegistrar:注册”给 Bean 包代理”的自动代理创建器。ProxyTransactionManagementConfiguration:注册事务自己的 Advisor、属性源、拦截器。
注意 case ASPECTJ
是另一条完全不同的路(编译期/加载期织入,不需要代理)。这条线在第七部分(第四篇)单独说,默认情况下你走的是
PROXY。
2.2
AutoProxyRegistrar:注册那个”包代理”的
BeanPostProcessor
1 | // org.springframework.context.annotation.AutoProxyRegistrar#registerBeanDefinitions |
registerAutoProxyCreatorIfNecessary 注册的是
InfrastructureAdvisorAutoProxyCreator,Bean
名字是固定常量:
1 | // org.springframework.aop.config.AopConfigUtils |
这个 Bean 就是第三部分的主角。它本质是个
BeanPostProcessor,在每个 Bean
完成初始化后决定”要不要包一层代理”。
请记住这个 Bean 名字。
后面第七部分(第四篇)会看到,事务和 @Aspect
抢的正是这一个名字。
2.3
ProxyTransactionManagementConfiguration:事务的三个核心
Bean
1 | // org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration |
三个关键点:
第一,Advisor 的 Advice 就是
TransactionInterceptor。 也就是说从 AOP
的角度看,事务只是一段 MethodInterceptor。它和你的
@Around 方法在结构上没有区别,区别只在方法体里干什么。
第二,new AnnotationTransactionAttributeSource(false)
里的 false 是 Spring 6.0 的新行为。 这个参数是
publicMethodsOnly:为 true
时,computeTransactionAttribute 遇到非 public 方法直接返回
null。5.3 的调用点是
new AnnotationTransactionAttributeSource(),即无参构造 →
this(true);Spring 6.0 把它改成显式传
false,源码注释写得很清楚:“Accept protected @Transactional
methods on CGLIB proxies, as of 6.0.”
要注意变的是调用点,不是类本身的默认值——AnnotationTransactionAttributeSource
的无参构造至今仍然是 this(true)。
(这段差异后面在第二篇 3.5
节展开,它正好解释了”protected 方法上的
@Transactional
到底生不生效”这个被传了很多年的错误结论。)
第三,advisor.setOrder(...) 的值来自
@EnableTransactionManagement 的 order
属性,默认是 Ordered.LOWEST_PRECEDENCE:
1 | // org.springframework.transaction.annotation.EnableTransactionManagement |
顺序问题在第七部分(第四篇)展开。
2.4 事务和
@Aspect 用的是同一个代理创建器
这是全文最关键的一段装配代码。AopConfigUtils
里维护了一张优先级表:
1 | // org.springframework.aop.config.AopConfigUtils |
而注册逻辑是”同名则取优先级更高者”:
1 | // org.springframework.aop.config.AopConfigUtils#registerOrEscalateApcAsRequired |
把这张表摊开看:
| 注册来源 | 注册的 APC 类 | 优先级 |
|---|---|---|
@EnableTransactionManagement |
InfrastructureAdvisorAutoProxyCreator |
0 |
@AspectJ 相关(一般由上一个升上来) |
AspectJAwareAdvisorAutoProxyCreator |
1 |
@EnableAspectJAutoProxy(Boot 默认有) |
AnnotationAwareAspectJAutoProxyCreator |
2 |
结论只有一句话:它们不是”两个代理创建器并存”,而是”抢同一个 Bean 名,高优先级者胜出”。
所以在一个正常的 Spring Boot 项目里,只要类路径上有
AspectJ(spring-boot-starter-aop 会带进来),最终生效的
internalAutoProxyCreator 就是
AnnotationAwareAspectJAutoProxyCreator——事务用的也是它。
这一点有两个直接后果,都是实战里会踩到的:
- 事务 Advisor 和你的
@AspectAdvisor 会被同一个顾问链收集、排序,所以它们的执行顺序是一个需要一起考虑的问题(第七部分,第四篇)。 - 代理方式(JDK 还是 CGLIB)是全局的,因为整个容器只有这一个自动代理创建器。
2.5 Boot 自动配置:默认 CGLIB,而且这个开关是共用的
Spring Boot 里负责这件事的是
TransactionAutoConfiguration:
1 | // org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration |
三个细节值得停下来看:
@ConditionalOnBean(TransactionManager.class)——只有容器里已经存在事务管理器时,事务的自动代理才生效。反过来说:如果你只加了
spring-boot-starter-jdbc 却自己配了奇怪的数据源、导致
TransactionManager Bean 没生成,那
@EnableTransactionManagement
这一整套都不会装配,@Transactional
全程静默失效——连 TransactionInterceptor
都不会注册,日志里什么都看不到。
matchIfMissing = true——spring.aop.proxy-target-class
不配置时,走上面那个 CglibAutoProxyConfiguration,也就是
proxyTargetClass = true,默认用
CGLIB。
同一个属性开关同时管着切面。看
AopAutoConfiguration:
1 | // org.springframework.boot.autoconfigure.aop.AopAutoConfiguration |
两边读的是同一个配置项,写的是同一个
internalAutoProxyCreator BeanDefinition 的
proxyTargetClass
属性(forceAutoProxyCreatorToUseClassProxying 里就是
definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE))。
这就意味着:代理方式是全局唯一的,事务和切面共用一个开关。
但要把机制说准,这里有两个容易想错的点。
第一,这个属性只会被写成
TRUE,没有任何代码把它写成 FALSE。
forceAutoProxyCreatorToUseClassProxying 的全部内容就是
definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);而
AutoProxyRegistrar 和
AspectJAutoProxyRegistrar 都只在注解属性为
true
时才调用它。所以不存在”后写覆盖先写、取值看注册先后”的竞争——只要任何一处要求
CGLIB,结果就是 CGLIB。
第二,手写
@EnableAspectJAutoProxy(proxyTargetClass = false)
不足以切回 JDK 代理。 AopAutoConfiguration
与事务自动配置彼此独立,只要 spring.aop.auto
没关(默认就是开的),它就仍然按
spring.aop.proxy-target-class 决定行为,而这个属性默认是
true(matchIfMissing = true),照样强制
CGLIB。真正能整套切到 JDK 代理的只有显式配置:
1 | spring.aop.proxy-target-class=false |
配了它,AopAutoConfiguration 和
TransactionAutoConfiguration 才会一起走各自的
JdkDynamicAutoProxyConfiguration
分支,此时不会有任何一方调用
forceAutoProxyCreatorToUseClassProxying。代价也直接:目标类必须实现接口,且注解要能被解析到接口方法上(第二篇
3.5 节)。
没有 AspectJ
依赖时的那条路更能说明问题——AopAutoConfiguration
里还有第三个分支:
1 |
|
没有 AspectJ 依赖时,Boot 会主动帮你注册一个
InfrastructureAdvisorAutoProxyCreator 并强制
CGLIB。 这解释了一个常见现象:项目里明明没有
@Aspect,事务照样能用。
到这里装配阶段就结束了:容器里多了三样东西——一个 APC、一个事务
Advisor、一个
TransactionInterceptor。接下来看它们怎么作用到具体的 Bean
上。
小结
这一篇走完了装配阶段:@EnableTransactionManagement 经由
TransactionManagementConfigurationSelector
导入两个类,AutoProxyRegistrar 注册出那个名为
internalAutoProxyCreator
的自动代理创建器,ProxyTransactionManagementConfiguration
把事务 Advisor、属性源与 TransactionInterceptor 注册成
Bean。到这一步,容器里只多了三样东西——它们都还没有作用到任何业务
Bean 上。
真正把 Advisor 匹配到具体 Bean、并决定”用 JDK 还是 CGLIB
包一层”的,是下一篇:wrapIfNecessary
的三道闸门、TransactionAttributeSourcePointcut
那两行判断,以及”代理在、事务却没生效”是怎么发生的。
下一篇
(二):代理的生成与调用进链——从
wrapIfNecessary一路读到invokeWithinTransaction。系列入口见导读。