Spring Boot 同类方法调用时 @Transactional 为什么不生效
同类内部调用绕过 Spring 事务代理。按整个业务入口或独立 Bean 调整边界,区分外层已有事务、REQUIRED 与内层注解失效。
一个 Service 的普通方法调用了同类中带 @Transactional 的方法,写入一部分数据后抛异常,前面的记录却留在数据库里。把调用写成 this.save(),或把代理切换成 CGLIB,通常都不能解决这条调用路径的问题。
Spring 的默认事务模式依靠代理拦截方法调用。对象内部调用自己的方法没有经过代理,内层方法上的事务配置不会被应用。以下讨论适用于 Spring Boot 3.x、Spring Framework 6.x 的代理事务,前提是数据访问使用对应事务管理器管理的连接;AspectJ 织入模式需要另行判断。
先看是谁调用了带注解的方法
下面是有问题的结构片段,jdbc 是注入的 JdbcTemplate,orders 是示例业务表:
public void submit(long orderId) {
this.writeOrder(orderId);
}
@Transactional
public void writeOrder(long orderId) {
jdbc.update("insert into orders(id) values (?)", orderId);
throw new IllegalStateException("Reject this order");
}
如果外部通过 Spring Bean 调用的是没有事务注解的 submit(),进入对象后再执行 writeOrder(),这次内部调用不会启动注解要求的事务。JdbcTemplate 写入是否已经提交,还取决于连接和数据库配置;不要把“没有执行内层事务拦截”直接等同于所有项目都会出现同一种提交结果。
还要区分另一种情况:外部调用的 submit() 本身已有事务,内层代码可以继续使用这个已有事务。同类调用并不会把外层事务删除。失效的是内层注解的独立配置,例如 REQUIRES_NEW 不会因此新开事务,内层单独设置的 rollbackFor 也不会在这个调用点生效。
两次写入必须一起提交,就标注外部入口
如果整个 submit() 就是一个完整业务操作,事务应覆盖这个入口。下面仍是放在现有 Service 中的局部示例:
@Transactional
public void submit(long orderId) {
writeOrder(orderId);
writeAudit(orderId);
}
调用方必须使用 Spring 注入的 Service Bean。自己 new 出来的对象不会因为方法有注解就获得事务。辅助方法可以留在同类,使用外层已经开始的事务;此时不要期待它们各自的事务注解改变传播行为。
这种调整会把 submit() 中的其他工作一起纳入事务。如果里面还有很慢的远程请求,应先确认事务边界是否合适,避免无意延长连接和锁的占用时间。
内层有独立边界,就拆成另一个 Bean
需要让写入方法成为独立的事务入口时,可以把它放到另一个受 Spring 管理的 Service。下面两个类分别保存为自己的 Java 文件,放在组件扫描范围内:
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderWriter {
private final JdbcTemplate jdbc;
public OrderWriter(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Transactional
public void write(long orderId) {
jdbc.update("insert into orders(id) values (?)", orderId);
throw new IllegalStateException("Reject this order");
}
}
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final OrderWriter writer;
public OrderService(OrderWriter writer) {
this.writer = writer;
}
public void submit(long orderId) {
writer.write(orderId);
}
}
这里 writer 来自容器注入,调用可以经过它的事务代理。示例故意抛出运行时异常来说明失败路径,不是正常下单实现。默认 REQUIRED 会在没有事务时创建事务,有外层事务时参与外层事务;拆 Bean 本身不会让它自动变成 REQUIRES_NEW。
不必为了这条路径开启循环依赖或到处获取当前代理。切换 JDK/CGLIB 代理同样不会改变 this 调用绕过事务拦截的事实。
复核时保留真实的调用入口
在隔离业务数据中,从实际调用方经过 Spring Bean 执行成功和异常两条路径,确认属于同一事务的数据库写入按预期一起提交或回滚。不要直接调用内部方法后就认定原来的调用链已修复。还应确认方法可以被代理、事务管理器选对,以及异常没有在事务方法内部被吞掉。
若事务确实已经启动,抛出的却是受检异常,需要另查受检异常的回滚规则。数据库回滚也不会撤销已经发送的消息、邮件或 HTTP 请求;这些副作用不能用本次代理修复来保证一致性。


