Spring AOP 通過動態代理將橫切邏輯織入目標方法。本文系統講解 五大 Advice 類型的執行時機、參數訪問能力、返回值控制權,並從零手寫 基於 CGLIB 的 AOP 代理生成過程,揭示 Spring 如何在目標類無接口時依然實現 AOP。
| Advice 類型 | 注解 | 執行時機 | 可訪問目標方法返回值 | 可修改返回值 | 可阻止目標方法執行 | 典型用途 |
|---|---|---|---|---|---|---|
| Before | @Before |
目標方法前 | 否 | 否 | 是 (拋異常) | 參數校驗、權限檢查、日志記錄 |
| AfterReturning | @AfterReturning |
目標方法正常返回後 | 是 (returning 屬性) | 否 | 否 | 返回值日志、緩存回寫、監控埋點 |
| AfterThrowing | @AfterThrowing |
目標方法拋異常後 | 否 (可獲取異常) | 否 | 否 | 異常日志、錯誤上報、補償邏輯 |
| After (Finally) | @After |
目標方法最終 (try-finally) | 否 | 否 | 否 | 資源釋放、清理工作、計時結束 |
| Around | @Around |
目標方法前後 完全控制 | 是 | 是 (唯一能改返回值) | 是 (不調用 proceed()) | 事務管理、緩存攔截、重試熔斷、性能剖析 |
ProceedingJoinPoint,可決定是否執行、何時執行、執行幾次、修改參數/返回值,但編碼最複雜,需手動 proceed() 且注意線程安全。| 維度 | JDK 動態代理 | CGLIB 代理 |
|---|---|---|
| 核心原理 | 實現目標接口,反射生成匿名類 | 繼承目標類,字節碼增強生成子類 |
| 前提條件 | 目標類必須實現接口 | 目標類無需接口,但不能是 final 類/方法 |
| 代理對象類型 | 實現接口類型,非目標類子類 | 目標類子類 (instanceof 目標類為 true) |
| 性能 (歷史) | 早期較慢 (反射調用) | 早期較快 (FastClass 索引) |
| 性能 (現代 JDK 8+) | invoke 指令優化,差異可忽略 | 同樣優化,差異可忽略 |
| Spring 默認策略 | 有接口 → JDK | 無接口 → CGLIB |
| 強制 CGLIB | @EnableAspectJAutoProxy(proxyTargetClass=true) 或 <aop:config proxy-target-class="true"> |
public class User {
private String userName, password;
// getter/setter 省略
public void saySth() { System.out.println("hello!"); }
}
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MyCglibProxy implements MethodInterceptor {
private Object target;
public Object getProxy(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass()); // 設置父類為目標類
enhancer.setCallback(this); // 設置回調攔截器
return enhancer.create(); // 生成代理對象
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("=== Before: " + method.getName() + " ===");
// 業務邏輯:如權限校驗、日志、事務開啟
// 模擬條件攔截
if (target instanceof User && "saySth".equals(method.getName())) {
User user = (User) target;
if (user.getUserName() == null || user.getUserName().isEmpty()) {
System.out.println("攔截:userName 為空,禁止執行");
return null; // 阻止目標方法執行
}
}
Object result = proxy.invokeSuper(obj, args); // 調用父類(目標)方法
// 也可用 method.invoke(target, args),但 proxy.invokeSuper 更快
System.out.println("=== AfterReturning: " + method.getName() + " ===");
return result;
}
}
public class MainTest {
public static void main(String[] args) {
User user = new User("wy", "wy"); // 有 userName
MyCglibProxy proxyFactory = new MyCglibProxy();
User proxyUser = (User) proxyFactory.getProxy(user);
proxyUser.saySth(); // 輸出:Before → hello! → AfterReturning
System.out.println("----- 分割線 -----");
User user2 = new User("", "wy"); // 空 userName
User proxyUser2 = (User) proxyFactory.getProxy(user2);
proxyUser2.saySth(); // 輸出:Before → 攔截:userName 為空 → 無 hello!
}
}
extends User。MethodInterceptor,所有非 final 方法調用將轉發到 intercept()。Method.invoke 快 10 倍+。proxyTargetClass 決策。ExposableInvocationInterceptor → Around → Before → Target → AfterReturning → After。EarlyProxyEvent) 解決循環依賴。serialVersionUID,跨 JVM 傳輸需注意。Advice 定義了「做什麼、何時做」,動態代理決定了「如何織入」。掌握五大通知的能力邊界與 CGLIB 字節碼增強原理,不僅能精準選型,更能在遇到「代理失效、事務不生效、final 方法報錯」等疑難雜症時,從原理層面快速定位根因。Spring AOP 的藝術,就在於用最簡潔的配置,驅動最強大的字節碼魔法。