StackTraces
Spring Boot已验证

Spring Boot @Transactional 在 private 方法上失效

@debug_master14 天前更新阅读 6 min00

代理模式下只有“穿过代理的外部方法调用”会被拦截。private 方法既不在接口里,也无法被 CGLIB 子类覆盖,代理根本拦不到它——@Transactional 于是变成一段没人消费的元数据:编译通过、启动正常、运行无事务,全程零报错。

Issue & Env(案发现场与环境)

运行环境

  • Spring Boot 2.x / 3.x,Spring Framework 5.x / 6.x
  • 默认代理模式:spring.aop.proxy-target-class=true(CGLIB 目标类代理)
  • 事务管理:@Transactional 声明式事务

业务场景

为了让 public 方法保持“只做参数校验和编排”的整洁结构,开发者把真正的写库逻辑抽成 private 方法,顺手把 @Transactional 标在它上面,以为事务边界跟着方法走。实际结果是:public 入口照常执行,private 方法里的插入也照常成功,但没有任何事务被开启。

为什么容易踩

  • 注解本身合法:@Transactional 可以标在任意方法上,编译器不拦。
  • 启动不报错:代理照样建得起来,不会像 final 类那样抛 AopConfigException。
  • 现象和“自调用失效”完全一致:都是 DEBUG 日志里没有 Creating new transaction,容易被归到同一个原因上。

StackTrace(堆栈日志)

本故障没有异常堆栈,唯一的证据是“事务日志缺失”。打开事务拦截器的 DEBUG 级别:

logging.level.org.springframework.transaction.interceptor=DEBUG

正常生效的事务方法,无论成功还是失败,都会留下进出痕迹:

2026-09-27 10:12:03 DEBUG o.s.t.i.TransactionInterceptor : Getting transaction for [OrderService.createOrder]
2026-09-27 10:12:03 DEBUG o.s.t.i.TransactionInterceptor : Completing transaction for [OrderService.createOrder]

而 private 方法上标注解时,整条调用链一句都没有:

2026-09-27 10:12:03 INFO  c.e.OrderService : 开始创建订单
2026-09-27 10:12:03 DEBUG c.e.OrderMapper  : ==>  Preparing: INSERT INTO orders ...
2026-09-27 10:12:03 INFO  c.e.OrderService : 订单创建完成
// 没有 Getting transaction,也没有 Completing transaction

关键判据:有 SQL 执行、有正常返回,就是没有事务拦截器的进出日志。这说明方法被调用了,但没经过代理。

Reproduce(复现代码)

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public void createOrder(Order order) {
        // 外部调用进来的是代理,但这一步是普通 Java 方法调用,没有二次穿越代理
        doCreate(order);
    }

    // 复现点:private + @Transactional,注解不会被任何代理消费
    @Transactional
    private void doCreate(Order order) {
        orderMapper.insert(order);
        throw new RuntimeException("模拟后续失败");
    }
}

调用 createOrder 后:异常如期抛出,但 orders 表里依然留下了已提交的记录——因为从头到尾就没有事务。

Root Cause(根因分析)

1. 代理模式只认“穿过代理的外部调用”

Spring 官方文档在代理模式一节里的表述是:只有外部方法调用经过代理时才会被拦截。这句话有两个约束:

拦截发生的位置:proxy.method() → MethodInterceptor → target.method()
不被拦截的情况:
  1. 同类内部调用(this.method())—— 绕过了代理,见另一个分支
  2. 代理无法覆盖的方法 —— 可见性不满足,就是本文的情况

2. private 方法为什么一定拦不到

两种代理机制的覆盖范围决定了这条硬边界:

CGLIB(目标类代理,Boot 默认):
  原理是继承目标类、override 方法插入拦截器。
  private 方法不是虚方法,子类无法 override —— 没有插入点。

JDK 动态代理(接口代理):
  原理是实现接口、在 InvocationHandler 里转发。
  private 方法既不在接口里,也无法被转发 —— 没有入口。

两者都失效,因此 @Transactional 标在 private 方法上是纯粹的无操作。

3. 一个必须澄清的版本差异

网上大量资料说“非 public 方法一律不生效”,这个说法对 Spring 6.0 以前的版本成立,现在已经不准确:

方法可见性 CGLIB 目标类代理 JDK 接口代理
public 生效 生效(且必须在接口中声明)
protected / 包级 Spring 6.0 起生效 不生效(接口代理要求 public)
private 不生效 不生效

也就是说,Spring 6.0 起 protected 和包级方法在 CGLIB 下已经可以织入事务;真正“一定拦不到”的只有 private。若团队希望代理行为在不同代理类型下保持一致(回到 5.3 之前的语义),可以显式开启 publicMethodsOnly:

@Bean
TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(true);
}

4. 快速定位手段

# 列出所有标注了 @Transactional 但可见性非 public 的方法
grep -rn -B3 "@Transactional" src/main/java --include="*Service*.java" | grep -E "private|protected"

The Fix(解决方案)

方案 A(推荐):把事务边界上移到 public 方法

让 public 方法承担事务边界,private 方法退回纯逻辑。这也符合“事务边界应当是明确的业务入口”的建模习惯:

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        validate(order);
        doCreate(order);      // private,纯逻辑,不承载事务
    }

    private void doCreate(Order order) {
        orderMapper.insert(order);
    }
}

方案 B:private 逻辑确实需要独立事务时,抽到独立 Bean

如果这段逻辑需要独立事务(例如失败也不回滚、或走独立提交),把它挪到另一个 Bean 的 public 方法上,用传播行为表达语义,而不是靠可见性硬凑:

@Service
public class OrderTxService {

    @Autowired
    private OrderMapper orderMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createInNewTransaction(Order order) {
        orderMapper.insert(order);
    }
}

注意 REQUIRES_NEW 的语义是独立提交,外层回滚救不了它,用之前先确认业务上确实允许单独留存。

方案 C:确实需要织入非 public 方法时,切 AspectJ 模式

代理模式下可见性边界无法突破,但 AspectJ 编织模式不依赖代理——它直接改写字节码,可以覆盖任意可见性的方法,也能顺带解决自调用问题。代价是要引入 aspectjweaver 并配置编译期或加载期织入,复杂度明显上升:

@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class TransactionConfig {
}

绝大多数业务项目不必走到这一步,方案 A 已经足够。

总结与避坑要点

  1. private 方法上的 @Transactional 不报错也不生效,是代理机制决定的硬边界,不是配置问题。
  2. protected / 包级方法在 Spring 6.0 起的 CGLIB 代理下可以生效,别再照搬“非 public 一律失效”的旧结论。
  3. 事务边界优先放在 public 的业务入口方法上,private 方法只保留纯逻辑。
  4. 排查时认准一个信号:SQL 执行了、方法返回了、但事务拦截器没有进出日志,就该去查可见性与自调用这两条代理边界。做得更细一点,把 grep 非 public 注解方法的检查加进 Code Review 清单,比出事后再查便宜得多。

常见问题

@Transactional 标在 private 方法上会报错吗?
不会。注解本身合法,编译器不拦;代理也能照常创建,启动阶段不抛异常。失效是静默的——事务边界没有建立,但应用正常启动、方法正常执行。
Spring 6 / Spring Boot 3 之后,private 方法上的事务有变化吗?
没有变化。代理模式下的拦截规则始终是「调用必须穿过代理」,private 方法既不在接口里,也无法被 CGLIB 子类覆盖,因此无论 Spring 5 还是 6 都不会被拦截。
怎么区分是 private 方法失效,还是同类自调用失效?
两者现象一致,都是调试日志里看不到 Creating new transaction,需要从调用入口倒推:外部调用穿过代理进来、内部再调 private 方法,问题在可见性;如果只是同一个类里直接调另一个方法,则属于自调用。

References(参考来源)

最后更新于 2026-09-27

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法