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

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法