首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring 進階之路:Advice 五大通知類型詳解與 CGLIB 動態代理實現 AOP 原理

     編輯:Java 綜合教學

前言:Spring AOP 核心在於「通知」與「代理」,搞懂這兩點就懂了 AOP 本質

Spring AOP 通過動態代理將橫切邏輯織入目標方法。本文系統講解 五大 Advice 類型的執行時機、參數訪問能力、返回值控制權,並從零手寫 基於 CGLIB 的 AOP 代理生成過程,揭示 Spring 如何在目標類無接口時依然實現 AOP。

一、 五大 Advice 類型全景對比

Advice 類型 注解 執行時機 可訪問目標方法返回值 可修改返回值 可阻止目標方法執行 典型用途
Before @Before 目標方法前 否 否 是 (拋異常) 參數校驗、權限檢查、日志記錄
AfterReturning @AfterReturning 目標方法正常返回後 是 (returning 屬性) 否 否 返回值日志、緩存回寫、監控埋點
AfterThrowing @AfterThrowing 目標方法拋異常後 否 (可獲取異常) 否 否 異常日志、錯誤上報、補償邏輯
After (Finally) @After 目標方法最終 (try-finally) 否 否 否 資源釋放、清理工作、計時結束
Around @Around 目標方法前後 完全控制 是 是 (唯一能改返回值) 是 (不調用 proceed()) 事務管理、緩存攔截、重試熔斷、性能剖析

關鍵差異總結

  • Around 最強:擁有 ProceedingJoinPoint,可決定是否執行、何時執行、執行幾次、修改參數/返回值,但編碼最複雜,需手動 proceed() 且注意線程安全。
  • Before 唯一能阻止執行:拋出異常即可攔截,但無法獲取返回值。
  • AfterReturning 僅能讀返回值:不可修改,若需修改必須用 Around。
  • After 類似 finally:無論正常/異常都執行,不可影響主流程。

二、 JDK 動態代理 vs CGLIB 代理:Spring 的雙引擎機制

維度 JDK 動態代理 CGLIB 代理
核心原理實現目標接口,反射生成匿名類繼承目標類,字節碼增強生成子類
前提條件目標類必須實現接口目標類無需接口,但不能是 final 類/方法
代理對象類型實現接口類型,非目標類子類目標類子類 (instanceof 目標類為 true)
性能 (歷史)早期較慢 (反射調用)早期較快 (FastClass 索引)
性能 (現代 JDK 8+)invoke 指令優化,差異可忽略同樣優化,差異可忽略
Spring 默認策略有接口 → JDK無接口 → CGLIB
強制 CGLIB@EnableAspectJAutoProxy(proxyTargetClass=true) 或 <aop:config proxy-target-class="true">

三、 手寫 CGLIB AOP 代理:從零實現 MethodInterceptor

3.1 目標類:無接口的 POJO


public class User {
    private String userName, password;
    // getter/setter 省略
    public void saySth() { System.out.println("hello!"); }
}

3.2 代理工廠類:實現 MethodInterceptor


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;
    }
}

3.3 測試驗證


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!
    }
}

3.4 核心原理解析

  1. Enhancer:CGLIB 核心字節碼生成器,創建目標類的子類。
  2. setSuperclass:指定父類,生成的代理類 extends User。
  3. setCallback:註冊 MethodInterceptor,所有非 final 方法調用將轉發到 intercept()。
  4. MethodProxy.invokeSuper:直接調用父類方法,避免反射開銷,比 Method.invoke 快 10 倍+。
  5. FastClass 機制:為代理類和目標類生成索引類,方法調用轉為整數索引查找,極致優化。

四、 Spring 整合 CGLIB 的關鍵點

  • Objenesis 庫:繞過構造函數實例化代理對象,支持無無參構造函數的類。
  • ProxyFactory:統一抽象 JDK/CGLIB,根據 proxyTargetClass 決策。
  • 最終代理鏈:ExposableInvocationInterceptor → Around → Before → Target → AfterReturning → After。
  • final 方法不攔截:CGLIB 無法覆寫 final 方法,Spring 會跳過並輸出警告日志。

五、 避坑指南

  1. 目標類/方法不可為 final:CGLIB 通過繼承實現,final 無法覆寫。
  2. 構造函數執行兩次:Objenesis 實例化 + 父類構造函數調用,構造函數邏輯慎重。
  3. 循環引用風險:AOP 代理早期暴露 (EarlyProxyEvent) 解決循環依賴。
  4. 序列化問題:代理類動態生成無 serialVersionUID,跨 JVM 傳輸需注意。

結語

Advice 定義了「做什麼、何時做」,動態代理決定了「如何織入」。掌握五大通知的能力邊界與 CGLIB 字節碼增強原理,不僅能精準選型,更能在遇到「代理失效、事務不生效、final 方法報錯」等疑難雜症時,從原理層面快速定位根因。Spring AOP 的藝術,就在於用最簡潔的配置,驅動最強大的字節碼魔法。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved