← 返回博客
2026-11-05 08:00:01

@Transactional 自调用为什么会静默失效

@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() 直接打到原始对象,注解形同虚设。

修法有三种,按侵入性从小到大:

  1. 拆到另一个 Bean,让调用跨对象,必然走代理。
  2. 注入自身:@Autowired private OrderService self; 然后 self.updateStatus(id)。注意 Spring 对自身注入有循环依赖处理,能成,但代码味道重。
  3. 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.xxx() 的自调用 grep 一遍,逐个确认是否踩坑。

本文关键词:@Transactional 自调用、JDK 动态代理、CGLIB、三级缓存