代理悖论:为什么 Spring @Transactional 会消失

发布: (2025年12月24日 GMT+8 14:30)
4 min read
原文: Dev.to

Source: Dev.to

问题

你在关键方法上标注 @Transactional,运行测试,一切看起来都正常。
但当你查看事务日志时,却 没有事务:没有连接加入、没有超时、错误时也没有回滚。方法被执行了,但它是在 事务之外 运行的。

这就是 代理悖论 —— Spring 的 AOP 代理在同一个 bean 内部方法相互调用时会隐藏事务。

Spring AOP 的工作原理

Spring 使用面向切面编程(AOP)来应用日志、安全或事务等横切关注点。
容器启动时,会扫描 bean 中的切面相关注解(@Transactional@Async@Cacheable 等)。
如果发现这些注解,Spring 会 用代理包装 bean(JDK 动态代理或 CGLIB 子类)。

代理拦截 外部 方法调用,执行拦截器链(例如 TransactionInterceptor),随后委派给原始 bean。

自调用问题

当一个方法调用同一个 bean 中的另一个方法时,拦截器链会被绕过。

@Service
class WalletService {

    // 入口方法
    public void pay(BigDecimal amount) {
        // 内部调用 – 绕过代理
        withdrawMoney(amount);
    }

    @Transactional
    public void withdrawMoney(BigDecimal amount) {
        // ... 数据库写操作 ...
    }
}
  • 从外部调用 withdrawMoney → 经过代理 → 启动事务。
  • pay 中调用 withdrawMoney → 直接 this 调用 → 代理被跳过 → 没有事务

为什么 AspectJ 表现不同

如果启用 AspectJ 的加载时织入:

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

AspectJ 会直接把事务逻辑织入字节码,因此自调用也能生效。

  • 优点: 自调用开箱即用。
  • 缺点: 需要 Java agent,增加构建复杂度,启动时间更长,通常对普通 Web 应用来说是大材小用。

解决方案

1. 将事务方法移到另一个 bean

@Service
class PaymentService {
    private final WalletService walletService; // 注入

    public void pay(BigDecimal amount) {
        walletService.withdrawMoney(amount); // 外部调用
    }
}

简洁、符合 SOLID、易于测试。

2. 自己注入代理(字段注入)

@Service
class WalletService {
    @Lazy
    @Autowired
    private WalletService self; // 注入代理

    public void pay(BigDecimal amount) {
        self.withdrawMoney(amount); // 走代理
    }
}

可行,但依赖字段注入,且在构造器注入时可能出现循环依赖问题。

3. 显式使用 TransactionTemplate

@Autowired
private TransactionTemplate transactionTemplate;

public void pay(BigDecimal amount) {
    transactionTemplate.execute(status -> {
        withdrawMoney(amount);
        return null;
    });
}

增加了一点样板代码,但事务边界变得明确。

4. 手动通过 AOP 代理调用

import org.springframework.aop.framework.AopContext;

public void pay(BigDecimal amount) {
    ((WalletService) AopContext.currentProxy()).withdrawMoney(amount);
}

泄漏抽象——业务代码依赖了 Spring AOP 的内部实现,需谨慎使用。

5. 切换到 AspectJ 织入

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

对多数 bean 有效,但会引入显著的复杂度;对少数情况通常不值得。

小结

  • 外部调用 → 代理 → 切面执行 → 业务逻辑。
  • 内部调用 → 原始 this 对象 → 业务逻辑(无切面)。

为避免悖论,重新组织代码、必要时注入代理,或使用 TransactionTemplate。切勿依赖 @Transactional 来处理自调用方法。

Back to Blog

相关文章

阅读更多 »