Spring 同方法自调用导致 @Transactional 失效

一、现场日志(还原案发)

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(),伪代码:

    java 复制代码
    public 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 条


五、防御体系(防止再犯)

  1. 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

  1. IDE 插件

    IntelliJ "Spring AOP" 检查已能实时警告自调用

  2. Code Review 清单

    • 本类方法是否含 @Transactional / @Cacheable / @Async

    • 若有,必须拆 Service 或注入代理

六、延伸阅读 & 工具
  • 调试:-Dorg.springframework.transaction.interceptor=TRACE 看事务边界

  • Arthas:watch com.example.OrderService saveOrder '{params,throwExp}' -e -x 3 确认是否进入 TransactionInterceptor

七、一句话总结
  • "只要还在同一个类里 `this.xxx()` 调用带 `@Transactional` 的方法,事务就 100% 失效;
  • 把方法搬到另一个 Bean,或者用代理/编程式事务,才能让 Spring 真正帮你管事务。"
相关推荐
打工仔折腾 AI1 分钟前
鸿蒙端ToDesk远控电脑实测:蓝牙键鼠、双模式鼠标与跨端剪贴板实践
后端·python·性能优化·计算机外设·电脑·harmonyos·ai agent 实战
豆角焖肉12 分钟前
SpringBoot+JWT 实现前后端分离登录鉴权
java·开发语言·spring boot·jwt·前后端分离·登录认证
Wang's Blog21 分钟前
Java 项目部署之 Docker工具快速入门: 自定义镜像的分层结构与写时复制
java·开发语言·docker
别动我齐刘海10 小时前
C++ 工程化学习
java·linux·前端·c++·学习·机器学习·机器人
code2cat11 小时前
Java进阶篇之AtomicLongFieldUpdater:把累计额度留在对象字段中
java·cas·并发编程
WeiXin_DZbishe11 小时前
基于springboot城市公交实时运营与调度系统-计算机毕设 附源码【课程设计】66173
java·vue.js·spring boot·后端·python·php·课程设计
泡茶喝茶写代码11 小时前
A股量化数据工程:从 REST 接口到策略信号(第 18 篇):放量缩量异动扫描
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
wewowilling11 小时前
认识微服务
java·运维·微服务·springcloud
用户114818678948412 小时前
面试官:大屏项目里十几个模块同时渲染上万条数据,怎么做到不卡?
前端·性能优化