CGLib 簡析

buttercup發表於2021-09-11

背景

 JDK 動態代理存在的一些問題:

呼叫效率低

 JDK 通過反射實現動態代理呼叫,這意味著低下的呼叫效率:

  1. 每次呼叫 Method.invoke() 都會檢查方法的可見性、校驗引數是否匹配,過程涉及到很多 native 呼叫,具體參見 JNI 呼叫開銷

  2. 反射呼叫涉及動態類解析,這種不可預測性,導致被反射呼叫的程式碼無法被 JIT 內聯優化,具體參見 反射呼叫方法

 可以通過java.lang.invoke.MethodHandle來規避以上問題,但是這不在本文討論的範圍。

只能代理介面

 java.lang.reflect.Proxy只支援通過介面生成代理類,這意味著 JDK 動態代理只能代理介面,無法代理具體的類。

 對於一些外部依賴或者現有模組來說,無法通過該方式實現動態代理。

應用場景

 CGLib 是一款用於實現高效動態代理的位元組碼增強庫,通過位元組碼生成技術,動態編譯生成代理類,從而將反射呼叫轉換為普通的方法呼叫。

 下面通過兩個案例體驗一下 CGLib 的使用方式。

案例一:Weaving

 現有一個輸出問候語句的類 Greet,現在有個新需求:在輸出內容前後加上姓名,實現個性化輸出。下面通過 CGLib 實現該功能:


class Greet { // 需要被增強目標類
    public String hello() { return "hello";  }
    public String hi() { return "hi"; }
    public String toString() { return "@Greet"; }
}

public class Weaving { // 模擬切面織入過程

    // 增加 before: 字首(模擬前置通知)
    static MethodInterceptor adviceBefore = (target, method, args, methodProxy) -> "before:" + methodProxy.invokeSuper(target, args);

    // 增加 :after 字尾(模擬後置通知)
    static MethodInterceptor adviceAfter = (target, method, args, methodProxy) -> methodProxy.invokeSuper(target, args) + ":after";

    // 通知
    static Callback[] advices = new Callback[] { NoOp.INSTANCE/*預設*/, adviceBefore, adviceAfter };

    // 切入點
    static CallbackFilter pointCut = method -> {
        switch (method.getName()) {
            case "hello" : return 1; // hello() 方法植入前置通知
            case "hi" : return 2;  // hi() 方法植入後置通知
            default: return 0; // 其他方法不新增通知
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Greet.class);     // 設定目標類
        enhancer.setCallbacks(advices);          // 設定通知 Advice
        enhancer.setCallbackFilter(pointCut);    // 設定切點 PointCut
        Greet greet = (Greet) enhancer.create(); // 建立 Proxy 物件
        System.out.println(greet.hello());
        System.out.println(greet.hi());
        System.out.println(greet.toString());
        TimeUnit.HOURS.sleep(1);
    }
}

案例二:Introduction

 隨著業務發展,系統需要支援法語的問候 FranceGreet,在不修改現有業務程式碼的前提下,可以通過 CGLib 實現該功能:

interface FranceGreet { // 支援新功能的介面
    String bonjour();
}

class FranceGreeting implements Dispatcher { // 新介面的實現
    private final FranceGreet delegate = () -> "bonjour";
    @Override
    public Object loadObject() throws Exception {
        return delegate;
    }
}

class FranceGreetingMatcher implements CallbackFilter { // 將新介面呼叫委託給 Dispatcher
    @Override
    public int accept(Method method) {
        return method.getDeclaringClass().equals(FranceGreet.class) ? 1 : 0;
    }
}

public class Introduction { // 模擬引入新介面

    public static void main(String[] args) throws InterruptedException {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Greet.class);
        enhancer.setInterfaces(new Class[]{FranceGreet.class}); // 擴充套件新介面
        enhancer.setCallbacks(new Callback[]{ NoOp.INSTANCE, new FranceGreeting()}); // 實現新介面
        enhancer.setCallbackFilter(new FranceGreetingMatcher()); // 關聯介面與實現
        Greet greet = (Greet) enhancer.create();
        System.out.println(greet.hello()); // 原方法不受影響
        System.out.println(greet.hi());
        FranceGreet franceGreet = (FranceGreet) greet;
        System.out.println(franceGreet.bonjour()); // 新介面方法正常呼叫
        TimeUnit.HOURS.sleep(1);
    }
}

原理簡析

 從前面的案例可以看到,CGLib 使用的方式很簡單,大致可以分為兩步:

  1. 配置 Enhancer
  • 設定需要代理的目標類與介面
  • 通過 Callback 設定需要增強的功能
  • 通過 CallbackFilter 將方法匹配到具體的 Callback

  1. 建立代理物件
  • 通過 CallbackFilter 獲取方法與 Callback 的關聯關係
  • 繼承目標類並重寫override方法,在呼叫程式碼中嵌入 Callback
  • 編譯動態生成的位元組碼生成代理類
  • 通過反射呼叫建構函式生成代理物件

Callback 分類

 此外,CGLib 支援多種 Callback,這裡簡單介紹幾種:

  • NoOp 不使用動態代理,匹配到的方法不會被重寫
  • FixedValue 返回固定值,被代理方法的返回值被忽略
  • Dispatcher 指定上下文,將代理方法呼叫委託給特定物件
  • MethodInterceptor 呼叫攔截器,用於實現環繞通知around advice

 其中 MethodInterceptor 最為常用,可以實現多種豐富的代理特性。
 但這類 Callback 也是其中最重的,會導致生成更多的動態類,具體原因後續介紹。

位元組碼生成過程

 底層通過 Enhancer.generateClass() 生成代理類,其具體過程不作深究,可以簡單概括為:

  1. 通過ClassVisitor獲取目標類資訊
  2. 通過ClassEmitter呼叫 asm 庫注入增強方法,並生成byte[] 形式的位元組碼
  3. 通過反射呼叫ClassLoader.defineClass()byte[]轉換為Class物件
  4. 將生成完成的代理類快取至LoadingCache,避免重複生成

生成的類結構

 通過 arthas 的 jad 命令可以觀察到,案例 Weaving 中實際生成了以下類:

  • 目標類:buttercup.test.Greet
  • 代理類:buttercup.test.Greet$$EnhancerByCGLIB(省略字尾)
  • 目標類 FastClass:buttercup.test.Greet$$FastClassByCGLIB(省略字尾)
  • 代理類 FastClass:buttercup.test.Greet$$EnhancerByCGLIB$$FastClassByCGLIB(省略字尾)

代理類

 代理類就是 Ehancer.create() 中為了建立代理物件動態生成的類,該類不但繼承了目標類,並且還重寫了需要被代理的方法。其命名規則為:目標類 + $$EnhancerByCGLIB
 在案例一中,我們分別給 Greet.hello()Greet.hi() 分別新增了攔截器Weaving.adviceBeforeWeaving.adviceAfter,下面我們分析代理類是如何完成這一功能的:

public class Greet$$EnhancerByCGLIB extends Greet implements Factory {

  private static final Object[] CGLIB$emptyArgs = new Object[0]; // 預設空引數
  private static final Callback[] CGLIB$STATIC_CALLBACKS; // 靜態 Callback(忽略)
  private static final ThreadLocal CGLIB$THREAD_CALLBACKS; // 用於給建構函式傳遞 Callback

  // 通過 MethodProxy 代理 Greet.hell() 方法
  private static final Method CGLIB$hello$0$Method;
  private static final MethodProxy CGLIB$hello$0$Proxy;

  // 通過 MethodProxy 代理 Greet.hi() 方法
  private static final Method CGLIB$hi$1$Method;
  private static final MethodProxy CGLIB$hi$1$Proxy;

  private boolean CGLIB$BOUND; // 判斷 Callback 是否已經初始化
  private NoOp CGLIB$CALLBACK_0; // 預設不攔截,直接呼叫目標類方法
  private MethodInterceptor CGLIB$CALLBACK_1; // Weaving.adviceBefore(增加 before: 字首)
  private MethodInterceptor CGLIB$CALLBACK_2; // Weaving.adviceAfter(增加 :after 字尾)

  static {
    Greet$$EnhancerByCGLIB.CGLIB$STATICHOOK1();
  }

  static void CGLIB$STATICHOOK1() { // 靜態初始化
    CGLIB$THREAD_CALLBACKS = new ThreadLocal();
    Class<?> clazz = Class.forName("buttercup.test.Greet");
    Class<?> clazz2 = Class.forName("buttercup.test.Greet$$EnhancerByCGLIB");
    Method[] methodArray = ReflectUtils.findMethods(new String[]{"hello", "()Ljava/lang/String;", "hi", "()Ljava/lang/String;"}, clazz.getDeclaredMethods());
    CGLIB$hello$0$Method = methodArray[0];
    CGLIB$hello$0$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hello", "CGLIB$hello$0");
    CGLIB$hi$1$Method = methodArray[1];
    CGLIB$hi$1$Proxy = MethodProxy.create(clazz, clazz2, "()Ljava/lang/String;", "hi", "CGLIB$hi$1");
  }

  // 通過 ThreadLocal 傳參
  public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] callbackArray) {
    CGLIB$THREAD_CALLBACKS.set(callbackArray);
  }

  // 工廠方法,建立增強後的物件
  public Object newInstance(Callback[] callbackArray) {
    Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(callbackArray);
    Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB = new Greet$$EnhancerByCGLIB();
    Greet$$EnhancerByCGLIB.CGLIB$SET_THREAD_CALLBACKS(null);
    return Greet$$EnhancerByCGLIB;
  }

  // 重寫 hello() 方法通知 CALLBACK_1
  public final String hello() {
    MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_1;
    if (methodInterceptor == null) { // 初始化 CALLBACK_1
      Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
      methodInterceptor = this.CGLIB$CALLBACK_1;
    }
    if (methodInterceptor != null) { // 呼叫攔截器 Weaving.adviceBefore
      return (String)methodInterceptor.intercept(this, CGLIB$hello$0$Method, CGLIB$emptyArgs, CGLIB$hello$0$Proxy);
    }
    return super.hello();
  }

  // 重寫 hi() 方法通知 CALLBACK_2
  public final String hi() {
    MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_2;
    if (methodInterceptor == null) { // 初始化 CALLBACK_2
      Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
      methodInterceptor = this.CGLIB$CALLBACK_2;
    }
    if (methodInterceptor != null) { // 呼叫攔截器 Weaving.adviceAfter
      return (String)methodInterceptor.intercept(this, CGLIB$hi$1$Method, CGLIB$emptyArgs, CGLIB$hi$1$Proxy);
    }
    return super.hi();
  }

  // 直接呼叫目標類的 hello()
  final String CGLIB$hello$0() {
    return super.hello();
  }

  // 直接呼叫目標類的 hi()
  final String CGLIB$hi$1() {
    return super.hi();
  }

  public Greet$$EnhancerByCGLIB() {
    Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
  }

  private static final void CGLIB$BIND_CALLBACKS(Object object) {
    block2: {
      Object object2;
      Greet$$EnhancerByCGLIB Greet$$EnhancerByCGLIB;
      block3: {
        Greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB) object;
        // 如果已經初始化過,則直接返回
        if (Greet$$EnhancerByCGLIB.CGLIB$BOUND) break block2;
            Greet$$EnhancerByCGLIB.CGLIB$BOUND = true;
        if (object2 = CGLIB$THREAD_CALLBACKS.get()) != null) break block3; // 從 ThreadLocal 獲取 Callback 引數
        if ((object2 = CGLIB$STATIC_CALLBACKS) == null) break block2;
      }
      Callback[] callbackArray = (Callback[])object2; // 初始化 Callback 引數
      Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = Greet$$EnhancerByCGLIB;
      greet$$EnhancerByCGLIB.CGLIB$CALLBACK_2 = (MethodInterceptor)callbackArray[2];
      greet$$EnhancerByCGLIB.CGLIB$CALLBACK_1 = (MethodInterceptor)callbackArray[1];
      greet$$EnhancerByCGLIB.CGLIB$CALLBACK_0 = (NoOp)callbackArray[0];
    }
  }
}

 在動態生成的類中,可以看到 CGLib 為每個被代理的方法建立了 MethodProxy 物件。
 該物件替代了 Method.invoke() 功能,是實現高效方法呼叫的的關鍵。下面我們以 Greet.hello() 為例對該類進行分析:

public class MethodProxy {

  private Signature sig1; // 目標類方法簽名:hello()Ljava/lang/String;
  private Signature sig2; // 代理類方法簽名:CGLIB$hello$0()Ljava/lang/String;

  private MethodProxy.CreateInfo createInfo; /* 省略初始化過程 */

    private static class CreateInfo {
      Class c1; // 目標類 buttercup.test.Greet
      Class c2; // 代理類 buttercup.test.Greet$$EnhancerByCGLIB
    }

  private final Object initLock = new Object();
  private volatile MethodProxy.FastClassInfo fastClassInfo;

    private static class FastClassInfo {
        FastClass f1; // 目標類 FastClass : 
        FastClass f2; // 代理類 FastClass : 
        int i1;       // 方法在 f1 中對應的索引
        int i2;       // 方法在 f2 中對應的索引
    }

    // 只在 MethodProxy 被呼叫時載入 FastClass,減少不必要的類生成(lazy-init)
    private void init() {
        if (fastClassInfo == null)  {
            synchronized (initLock) {
                if (fastClassInfo == null) {
                    MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
                    fci.f1 = helper(ci, ci.c1); // 
                    fci.f2 = helper(ci, ci.c2); // 
                    fci.i1 = fci.f1.getIndex(this.sig1);
                    fci.i2 = fci.f2.getIndex(this.sig2);
                    fastClassInfo = new FastClassInfo();

                }
            }
        }
    }

    // 根據 Class 物件生成 FastClass 
    private static FastClass helper(MethodProxy.CreateInfo ci, Class type) {
      Generator g = new Generator();
      g.setType(type);
      return g.create();
    }

    // 呼叫 buttercup.test.Greet.hello()
    // 但實際上會呼叫代理類的 EnhancerByCGLIB.hello() 實現
    public Object invoke(Object obj, Object[] args) throws Throwable {
        init();
        return fastClassInfo.f1.invoke(fci.i1, obj, args);
    }

    // 呼叫 buttercup.test.Greet$$EnhancerByCGLIB.CGLIB$hello$0()
    // 通過 super.hello() 呼叫目標類的 Greet.hello() 實現
    public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        init();
        return fastClassInfo.f2.invoke(fci.i2, obj, args);
    }
}

 可以看到 MethodProxy 的呼叫實際是通過 FastClass 完成的,這是 CGLib 實現高效能反射呼叫的祕訣,下面來解析這個類的細節。

目標類 FastClass

 為了規避反射帶來的效能消耗,cglib 定義了 FastClass 來實現高效的方法呼叫,其主要職責有兩個

  1. 方法對映:解析 Class 物件併為每個 Constructor 與 Method 指定一個整數索引值 index
  2. 方法呼叫:通過 switch(index) 的方式,將反射呼叫轉化為硬編碼呼叫
abstract public class FastClass {
    
    // 對映:根據方法名稱與引數型別,獲取其對應的 index
    public abstract int getIndex(String methodName, Class[] argClass);

    // 呼叫:根據 index 找到指定的方法,並進行呼叫
    public abstract Object invoke(int index, Object obj, Object[] args) throws InvocationTargetException;

}

其命名規則為:目標類 + $$FastClassByCGLIB。下面具體分析一下對目標類 Greet 對應的 FastClass

public class Greet$$FastClassByCGLIB extends FastClass {

    public Greet$$FastClassByCGLIB(Class clazz) {
        super(clazz);
    }

    // 獲取 index 的最大值
    public int getMaxIndex() {
        return 4; // 當前 FastClass 總共支援 5 個方法
        //索引值分別為 0:hello(), 1:hi(), 2:equals(), 3:hasCode(), 4:toString()
    }

    // 根據方法名稱以及引數型別,獲取到指定方法對應的 index
    public int getIndex(String methodName, Class[] argClass) {
        switch (methodName.hashCode()) {
            case 3329: {
                if (!methodName.equals("hi")) break;
                switch (argClass.length) {
                    case 0: { return 1; } // hi() 對應 index 為 1
                }
                break;
            }
            case 99162322: {
                if (!methodName.equals("hello")) break;
                switch (argClass.length) {
                    case 0: { return 0; } // hello() 對應 index 為 0
                }
                break;
            }
            /* 忽略 Object 方法 */
        }
        return -1;
    }

    // 根據方法簽名,獲取到指定方法對應的 index
    public int getIndex(Signature signature) {
        String sig = ((Object)signature).toString();
        switch (sig.hashCode()) {
            case 397774237: {
                if (!sig.equals("hello()Ljava/lang/String;")) break;
                return 0; // hello() 對應 index 為 0
            }
            case 1155503180: {
                if (!sig.equals("hi()Ljava/lang/String;")) break;
                return 1;  // hi() 對應 index 為 1
            }
            /* 忽略 Object 方法 */
        }
        return -1;
    }

    // 方法呼叫(硬編碼呼叫)
    public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
        Greet greet = (Greet) obj;
        switch (n) { // 通過 index 指定目標函式
            case 0: { return greet.hello(); } // 通過索引 0 呼叫 hello()
            case 1: { return greet.hi(); }    // 通過索引 1 呼叫 hi()
            /* 忽略 Object 方法 */
        }
        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }

    // 建構函式(硬編碼呼叫)
    public Object newInstance(int n, Object[] argClass) throws InvocationTargetException {
        switch (n) {
            case 0: { return new Greet(); }
        }
        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }
}

代理類 FastClass

 之前提及過:使用 MethodInterceptor 會比其他 Callback 生成更多的動態類,這是因為需要支援 MethodProxy.invokeSuper() 呼叫:

public interface MethodInterceptor extends Callback {

    // 所有生成的代理方法都呼叫此方法
    // 大多數情況需要通過 MethodProxy.invokeSuper() 來實現目標類的呼叫
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

 MethodProxy.invokeSuper() 通過呼叫代理類中帶 $CGLIB$ 字首的方法,繞過被重寫的代理方法,避免出現無限遞迴。

 為了保證呼叫效率,需要對代理類也生成 FastClass

public class Greet$$EnhancerByCGLIB$$FastClassByCGLIB extends FastClass {

    public Object invoke(int n, Object obj, Object[] args) throws InvocationTargetException {
        Greet$$EnhancerByCGLIB greet$$EnhancerByCGLIB = (Greet$$EnhancerByCGLIB)obj;
        switch (n) {
            case 9: { // 呼叫目標類的原始 Greet.hello() 方法
                return greet$$EnhancerByCGLIB.CGLIB$hello$0();
            }
            case 10: { // 呼叫目標類的原始 Greet.hi() 方法
                return greet$$EnhancerByCGLIB.CGLIB$hi$1();
            }
            /* 忽略其他 Enhancer 方法 */
        }
        throw new IllegalArgumentException("Cannot find matching method/constructor");
    }
    
    public int getIndex(String methodName, Class[] argClass) {
        switch (methodName.hashCode()) {
            case 1837078673: {
                if (!methodName.equals("CGLIB$hi$1")) break;
                switch (argClass.length) {
                    case 0: { return 10;  }
                }
                break;
            }
            case 1891304123: {
                if (!methodName.equals("CGLIB$hello$0")) break;
                switch (argClass.length) {
                    case 0: { return 9; }
                }
                break;
            }
            /* 忽略其他 Enhancer 方法 */
        }
        return -1;
    }

    public int getIndex(Signature signature) {
        String sig = ((Object)signature).toString();
        switch (sig.hashCode()) {
            case -1632605946: {
                if (!sig.equals("CGLIB$hello$0()Ljava/lang/String;")) break;
                return 9;
            }
            case 540391388: {
                if (!sig.equals("CGLIB$hi$1()Ljava/lang/String;")) break;
                return 10;
            }
            /* 忽略其他 Enhancer 方法 */
        }
        return -1;
    }

}

補充

 案例 Introduction 中僅使用了 Dispatcher,因此只生成了代理類,未使用到 FastClass

public class Greet$$EnhancerByCGLIB extends Greet implements FranceGreet, Factory {

    private NoOp CGLIB$CALLBACK_0;
    private Dispatcher CGLIB$CALLBACK_1;

    public final String bonjour() {
        Dispatcher dispatcher = this.CGLIB$CALLBACK_1;
        if (dispatcher == null) {
            Greet$$EnhancerByCGLIB.CGLIB$BIND_CALLBACKS(this);
            dispatcher = this.CGLIB$CALLBACK_1;
        }
        return ((FranceGreet)dispatcher.loadObject()).bonjour();
    }

    /* 忽略多餘的屬性與方法 */
}

 本文案例僅涉及 MethodInterceptorDispatcher,這兩個 Callback 也是 Spring AOP 實現的關鍵,後續將繼續分析相關的原始碼實現。



附錄

JIT 編譯優化