- Published on
[Spring 筆記] AOP 與代理機制:JDK Dynamic Proxy vs CGLIB
- Authors

- Name
- Vic Chen
前言
在開發 Spring 專案時,幾乎每個人都用過 @Transactional、@Retryable 或 @Async。 但你可能遇過這種狀況:
- 明明加了
@Transactional,卻沒有 rollback。 @Retryable根本沒重試。@Async方法照樣同步執行。
這通常不是 Spring 壞掉,而是因為你 誤用了 AOP 代理機制(Proxying Mechanism)。
Spring AOP 的本質:代理(Proxy)
Spring 的 AOP 並不是修改原始程式碼或 bytecode,而是 透過代理(Proxy) 包裝原本的 Bean。
也就是說,當你注入一個加了 @Transactional 的 Bean 時,你拿到的不是原始類別,而是代理對象。
@Service
public class OrderService {
@Transactional
public void createOrder() {
// 這段程式會在代理的包裝下執行
}
}
Spring 會在啟動的初始化階段時,掃描建立 Bean 定義,用 Proxy 包住 OrderService,在方法執行前後注入攔截邏輯(Advice)。
NOTE
在啟動後 ,實際上得到的是一個 runtime 時生成的 subclass:com.example.OrderService$$EnhancerBySpringCGLIB$$9a0f3a
這個類別在 JVM 執行時,透過 CGLIB bytecode manipulation 動態生成,只存在於記憶體中,不會被編譯成 .class 檔存在硬碟上。 CGLIB 代理是透過 ASM 操作 bytecode,在 JVM runtime 時產生 subclass,載入至 Enhancer 產生的 ClassLoader,不會寫入檔案系統。
Proxy 的兩種機制
Spring 主要有兩種代理實作:
| 類型 | 使用條件 | 實作方式 | 限制 |
|---|---|---|---|
| JDK Dynamic Proxy | Bean 有實作介面 | 實作該介面的代理類別 | 只能代理介面方法 |
| CGLIB Proxy | Bean 沒有介面 | 繼承原類別產生子類別 | 無法代理 final、private 方法 |
預設行為:
- 如果 Bean 有介面 → 使用 JDK Proxy
- 沒有介面 → 使用 CGLIB
你也可以強制使用 CGLIB:
spring:
aop:
proxy-target-class: true
示意圖:JDK vs CGLIB
- JDK Proxy 會生成一個實作相同介面的類別,攔截介面方法呼叫。
- CGLIB 會生成一個繼承原類別的子類別,透過 method interceptor 注入切面邏輯。
常見陷阱 1:Self Invocation(自我呼叫)
@Service
public class UserService {
@Transactional
public void createUser() {
saveUser();
}
@Transactional
public void saveUser() {
// ...
}
}
這樣寫,saveUser() 的 transaction 不會生效。
為什麼?
因為代理只包在最外層呼叫上。
當你在同一個物件內呼叫自己的方法(this.saveUser()),Spring AOP 完全不會攔截。
解法:
將方法拆分成不同 Bean
@Service public class UserService { @Autowired private UserHelper helper; @Transactional public void createUser() { helper.saveUser(); } } @Service public class UserHelper { @Transactional public void saveUser() { ... } }NOTE
這是 Spring 官方最建議的解法,這方法侵入性較小
自我注入呼叫
@Service
public class UserService {
@Lazy
@Autowired
private final UserService userService;
@Transactional
public void createUser() {
userService.saveUser(); // 自我注入呼叫
}
@Transactional
public void saveUser() {
// ...
}
}
IMPORTANT
自我注入記得要加上 @Lazy 去避免循環依賴(Circular Dependencies)
從 Proxy 取得自己再呼叫
((UserService) AopContext.currentProxy()).saveUser();⚠️ 需在設定檔中啟用:
@EnableAspectJAutoProxy(exposeProxy = true)
WARNING
這是 Spring 官方最不推薦的解法,這方法會使程式碼跟 Spring AOP 過於耦合
常見陷阱 2:Private / Final 方法
CGLIB 是靠「繼承」來實現代理,而 JDK Proxy 是依靠介面,只能攔截介面方法來實現代理
所以 priate, final 都無法被代理
@Service
public class OrderService {
@Transactional
private void saveOrder() {} // ❌ 無效
@Transactional
public final void commit() {} // ❌ 無效
}
@Transactional、@Retryable、@Async 都不會作用。
解法:
- 方法必須是 public 或 protected
- 類別不可是 final class
- 避免使用 private/final 方法作為切點
常見陷阱 3:呼叫時機錯誤
AOP 代理只會在 Spring 完成 Bean 初始化後生效。
因此在 @PostConstruct、建構子或 @Bean 方法內呼叫代理方法,都不會觸發 AOP。
@PostConstruct
public void init() {
doSomething(); // ❌ AOP 尚未初始化
}
小結:JDK vs CGLIB 比較
| 項目 | JDK Proxy | CGLIB |
|---|---|---|
| 實作方式 | 實作介面 | 繼承類別 |
| 是否需要介面 | ✅ 是 | ❌ 否 |
| 是否能代理 private/final 方法 | ❌ 否 | ❌ 否 |
| 效能 | 稍快 | 稍慢(但差距不大) |
| Spring 預設策略 | 有介面用 JDK | 無介面用 CGLIB |
| 可否強制使用 | ✅ proxy-target-class: true | ✅ 自動 fallback |
總結
Spring AOP 是透過「代理模式」包裝原始 Bean。
這帶來了靈活的切面機制,也導致不少「方法沒生效」的誤會。
重點整理:
@Transactional、@Retryable、@Async都是靠 AOP 實現的。- 代理機制分為 JDK Dynamic Proxy 和 CGLIB。
- self-invocation、private、final、建構子時機 都會讓 AOP 失效。