一、现场日志(还原案发)
Service 层代码:
java
@Service
public class OrderService {
public void create(OrderDTO dto) {
// 参数校验 & 业务计算
Order order = convert(dto);
// ② 调用本类"事务"方法
saveOrder(order);
}
@Transactional(rollbackFor = Exception.class)
public void saveOrder(Order order) {
orderMapper.insert(order); // ①
orderDetailMapper.batchInsert(order.getDetails()); // ③
if (order.getDetails().size() > 10) {
throw new IllegalArgumentException("明细过多");
}
}
}
单元测试
java
@Test
public void shouldRollbackWhenTooManyDetails() {
OrderDTO dto = buildBigOrder(); // 11 条明细
Assertions.assertThrows(IllegalArgumentException.class,
() -> orderService.create(dto));
// 期望:订单表 0 条,明细表 0 条
assertEquals(0, jdbcTemplate.queryForObject(
"select count(*) from t_order", Long.class));
}
结果
-
异常确实抛出 ✅
-
数据库仍出现 1 条订单 + 11 条明细 ❌
-
日志无 rollback 信息 ❌
二、根因剖析
1. Spring 事务 = 动态代理
-
容器启动时为
OrderService生成 TransactionProxy(CgLib) -
代理类重写
saveOrder(),伪代码:javapublic void saveOrder(Order order) { TransactionStatus tx = PlatformTransactionManager.getTransaction(new DefaultTransactionDefinition()); try { // 目标对象真实方法 target.saveOrder(order); PlatformTransactionManager.commit(tx); } catch (Throwable e) { PlatformTransactionManager.rollback(tx); throw e; } }
2. 自调用 bypass 代理
-
create()是 目标对象内部方法 ,this.saveOrder(order)的this= 原生对象,不是代理 -
因此事务切面永远不被触发,等同于普通本地方法调用
https://i.imgur.com/tx-spring-proxy.png
三、复现 Demo(最小可运行)
见 com.example.chapter1.tx.SelfInvokeDemoApplicationTests
关键断点:在 OrderMapper.insert 后查看 TransactionSynchronizationManager.getCurrentTransactionName()
-
自调用场景返回 null(无事务)
-
代理调用返回
saveOrder(存在事务)
四、正确修复方案
|-------------------|-------------------------------------------------------------------|-----------|------------|
| 方案 | 代码示例 | 优点 | 缺点 |
| 拆 Service(推荐) | OrderCommandService.saveOrder() | 完全解耦、事务生效 | 类增多 |
| 注入代理自调用 | @Autowired private OrderService self; + self.saveOrder(order) | 改动小 | 循环依赖风险 |
| AspectJ 编译织入 | <tx:annotation-driven mode="aspectj"/> | 无代理限制 | 构建复杂、需 ajc |
| 编程式事务 | TransactionTemplate.execute(status -> { ... }); | 灵活、可读写分离 | 模板代码多 |
拆 Service 示例
java
@Service
public class OrderCommandService {
@Transactional(rollbackFor = Exception.class)
public void saveOrder(Order order) {
orderMapper.insert(order);
orderDetailMapper.batchInsert(order.getDetails());
if (order.getDetails().size() > 10)
throw new IllegalArgumentException("明细过多");
}
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderCommandService commandService;
public void create(OrderDTO dto) {
Order order = convert(dto);
commandService.saveOrder(order); // 走代理
}
}
单元测试再次执行
✅ 异常抛出后数据回滚,表记录 0 条
五、防御体系(防止再犯)
- ArchUnit 规则固化
java
@ArchTest
static final ArchRule no_self_call_with_tx =
noMethods().that().areAnnotatedWith(Transactional.class)
.should().beCalledFromMethods().that().areDeclaredIn(
new DescribedPredicate<>("同一类") {
public boolean apply(JavaMethod input) {
return input.getOwner().equals(
input.getCallsOfSelf().get(0).getOwner());
}
});
测试失败即阻断 CI
-
IDE 插件
IntelliJ "Spring AOP" 检查已能实时警告自调用
-
Code Review 清单
-
本类方法是否含
@Transactional/@Cacheable/@Async -
若有,必须拆 Service 或注入代理
-
六、延伸阅读 & 工具
- Spring 官方文档 Declarative Transaction Management
-
调试:
-Dorg.springframework.transaction.interceptor=TRACE看事务边界 -
Arthas:
watch com.example.OrderService saveOrder '{params,throwExp}' -e -x 3确认是否进入 TransactionInterceptor
七、一句话总结
- "只要还在同一个类里 `this.xxx()` 调用带 `@Transactional` 的方法,事务就 100% 失效;
- 把方法搬到另一个 Bean,或者用代理/编程式事务,才能让 Spring 真正帮你管事务。"