@Transactional 自调用为什么会静默失效
一个订单服务,pay() 里调 this.updateStatus(),两个方法都标了 @Transactional。压测时发现状态更新没回滚,日志里一条异常都没有。这类问题面试官最爱追:不是问注解怎么写,是问代理对象什么时候生成、调用走没走代理。
手把手实操
先复现。建一个 Spring Boot 工程,写一个自调用场景:
@Service
public class OrderService {
@Autowired private OrderMapper mapper;
@Transactional
public void pay(Long id) {
mapper.updateStatus(id, "PAID");
this.updateStatus(id); // 自调用
}
@Transactional(rollbackFor = Exception.class)
public void updateStatus(Long id) {
mapper.updateStatus(id, "DONE");
throw new RuntimeException("boom");
}
}
跑一次,DONE 留在库里,PAID 也没回滚。原因:pay() 里的 this 是原始对象,不是代理。外部调用 orderService.pay() 进的是代理,代理只对 pay 这一层开了事务;this.updateStatus() 直接打到原始对象,注解形同虚设。
修法有三种,按侵入性从小到大:
- 拆到另一个 Bean,让调用跨对象,必然走代理。
- 注入自身:
@Autowired private OrderService self;然后self.updateStatus(id)。注意 Spring 对自身注入有循环依赖处理,能成,但代码味道重。 AopContext.currentProxy(),需要开@EnableAspectJAutoProxy(exposeProxy = true)。
关键点在第二、三种都依赖“拿到代理引用”,第一种最干净。踩坑点:exposeProxy 默认 false,不开直接抛 IllegalStateException,很多人卡在这。
看源码及解析
代理怎么来的?AbstractAutoProxyCreator.wrapIfNecessary() 在 Bean 初始化后判断是否需要代理,需要就调 createProxy(),最终落到 DefaultAopProxyFactory.createAopProxy():
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
// 走 CGLIB
} else {
// 走 JDK 动态代理
}
proxyTargetClass 默认 false。所以:目标类实现了接口 → JDK 动态代理,代理类实现同一接口;没接口 → CGLIB,生成子类。Spring Boot 从 2.x 起把 spring.aop.proxy-target-class 默认设为 true,也就是默认 CGLIB,这点和裸 Spring 不一样,面试常挖。
为什么自调用必然失效?JDK 代理的拦截逻辑在 JdkDynamicAopProxy.invoke(),只有调用经过代理实例才触发;CGLIB 的 DynamicAdvisedInterceptor.intercept() 同理,它拦的是子类方法调用,this 指向原始实例,压根不进拦截器。这是代理模式的固有边界,不是 Spring 的 bug。
顺带说循环依赖。单例 Bean 的 setter/字段注入靠三级缓存:singletonObjects、earlySingletonObjects、singletonFactories。getSingleton() 里先查一级,没有再查二级,再没有才从三级拿 ObjectFactory 提前暴露引用。构造器注入的循环依赖救不了,因为对象还没造出来就没法提前暴露。JDK 动态代理 vs CGLIB 在这里也有差别:如果循环依赖的 Bean 需要 AOP,提前暴露的是工厂产出的代理,这也是三级缓存存在的核心理由——延迟决定用哪种代理。
验证方法
打开 logging.level.org.springframework.aop=DEBUG,启动日志能看到 Creating JDK dynamic proxy 或 Creating CGLIB proxy 字样,确认代理类型。再在 pay() 里打印 this.getClass(),看到的是原始类而不是 $Proxy 或 $$EnhancerBySpringCGLIB,就坐实了自调用。
事务是否真生效,看 DataSourceTransactionManager 的 DEBUG 日志有没有 Creating new transaction。自调用那次不会有。
面试速答
- 自调用失效根因:
this是原始对象,绕过了代理,注解拦截器不触发。 - 代理选择:有接口默认 JDK,
proxyTargetClass=true或 Spring Boot 默认走 CGLIB。 - 循环依赖靠三级缓存提前暴露引用,构造器注入无解。
- 修法优先拆 Bean,其次
AopContext.currentProxy()并开exposeProxy。
核心收获:注解不是魔法,一切取决于调用有没有经过代理对象。下一步,把项目里所有 this.xxx() 的自调用 grep 一遍,逐个确认是否踩坑。
本文关键词:@Transactional 自调用、JDK 动态代理、CGLIB、三级缓存