代理悖论:为什么 Spring @Transactional 会消失
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 来处理自调用方法。