1. 程式人生 > >動態代理(二)—— CGLIB代理原理

動態代理(二)—— CGLIB代理原理

構建 ted err 包裝 appears 動態 調用棧 hook ces

前篇文章動態代理(一)——JDK中的動態代理中詳細介紹了JDK動態代理的Demo實現,api介紹,原理詳解。這篇文章繼續討論Java中的動態代理,並提及了Java中動態代理的幾種實現方式。這裏繼續介紹CGLIB代理方式。

CGLIB動態代理在AOP、RPC中都有所使用,是Java體系中至關重要的一塊內容。本篇文章的主要目標:

  • 掌握使用CGLIB生成代理類
  • 深入理解CGLIB的代理原理

從以上目標出發,本篇文章主要從以下幾個方面逐步深入探索CGLIB:

  • CGLIB的使用Demo
  • CGLIB重要API介紹
  • CGLIB代理原理
  • 總結

一.CGLIB的使用Demo

使用CGLIB的大致分為四步驟:

  1. 創建被代理對象
  2. 創建方法攔截器
  3. 創建代理對象
  4. 調用代理對象

1.創建被代理對象

public class EchoServiceImpl implements EchoService {

    public void echo(String message) {
        System.out.println(message);
    }

    public void print(String message) {
        System.out.println(message);
    }

    public int test() {
        return 1;
    }

    public final void finalTest() {
        System.out.println("I am final method.");
    }
}

2.創建方法攔截器

public class EchoServiceInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("before invoking!");
        Object object = methodProxy.invokeSuper(o, objects);
        System.out.println("after invoking!");
        return object;
    }
}

接口MethodInterceptor是CGLIB庫提供,用於應用開發者根據自己的業務邏輯進行擴展實現。

3.創建代理對象

//Enhancer是生成代理類的工廠
Enhancer enhancer = new Enhancer();
//設置代理的超類,即被代理對象
enhancer.setSuperclass(EchoServiceImpl.class);
//設置攔截方法
enhancer.setCallback(new EchoServiceInterceptor());
//生成代理對象
EchoService echoService = (EchoService) enhancer.create();

4.調用代理對象

echoService.echo("test");

執行結果:
before invoking!
test
after invoking!

二.CGLIB重要API介紹

CGLIB庫的體積很小,但是學習難度確非常高,畢竟涉及到bytecode。所以該篇文章後續關於原理介紹,只涉及代理原理方面,關於如何生成代理對象的方面,由於個人能力所及,不敢妄加說明。

下面是CGLIB的包結構,每個包都是負責一個模塊功能,定義非常明確,負責單一的功能職責:

  • net.sf.cglib.core
    低級別的字節碼操作的類,它們直接與ASM相關

  • net.sf.cglib.transform
    在運行或者構建時轉換類文件的一些類

  • net.sf.cglib.proxy
    創建代理和方法攔截器定義的類

  • net.sf.cglib.reflect
    實現快速反射的一些基礎類

  • net.sf.cglib.util
    用於集合排序的一些工具類

  • net.sf.cglib.beans
    與JavaBean相關的類

雖然cglib包含了如此多的功能模塊,但是對於使用者,我們並不需要關註如此多的細節,只需要掌握幾個重要的接口:

技術分享圖片

在看完上面的Demo,應該對Enhancer有一定了解。Enhancer字面義即增強,也正如其表述,Enhancer就是用來創建代理對象的接口。其中create方法可以生成代理對象,實際就是工廠模式。

在生成對象前,需要做關於代理方面的配置:

  • 配置被代理對象(目標),即setSuperClass設置超類型,該superClass即Enhancer中持有的Class對象;
  • 配置統一攔截方法(中間人),即setCallBack設置回調接口,對應上圖的CallBack。AOP的實現使用methodInterpretor型CallBack;
  • 可選性的配置攔截過濾器(核驗流程),即setCallBackFilter,對應上圖的CallBackFiler;

Enhancer的create api提供了生成代理對象的。以上即在編寫cglib動態代理過程中使用的幾個重要api。雖然字節碼技術是非常晦澀深奧,但是cglib以簡單易用的api使字節碼增強技術變得非常容易上手。

通過以上的demo示例和幾個重要api的介紹應該都能掌握使用cglib庫生成代理類。下面依然通過反編譯的方式繼續深入cglib的動態代理的調用原理。

三.CGLIB代理原理

1.整體架構與調用過程概覽

在詳細查看被代理對象的原理之前,先了解下cglib的整體架構圖:

技術分享圖片

從圖中可以看出,cglib在字節碼層面的操作技術主要依賴ASM提供的能力。在上節中提到的net.sf.cglib.core包,正是與ASM相關。CGLIB上層直接面向應用層,將深奧晦澀的字節碼技術包裝成應用易用能理解的api,為aop,dynaminc proxy等技術提供了實現基礎。

在反編譯看代理對象的源代碼之前,先看下代理調用的過程圖:

技術分享圖片

從圖中可以看出:

  1. 客戶端調用代理對象的被代理方法
  2. 代理對象將調用委派給方法攔截器統一接口intercept
  3. 方法攔截器中執行前置操作,然後調用方法代理的統一接口invokeSuper
  4. 方法代理的invokeSuper初始化代理對象的和被代理對象的fastClass
  5. 初始化後,再調用代理對象的fastClass
  6. 代理對象的fastClass能夠fast的調用代理的代理對象
  7. 代理對象再調用被代理對象的被代理方法
  8. 調用棧彈出,到intercept中再執行後置操作,方法調用結束

通過以上的過程再來看下它們之間的UML:

技術分享圖片

在Enhancer中的配置的被代理對象、統一回調的最終都被聚合到生成的代理對象中。(工廠模式,零件組裝成產品)
代理對象聚合同一方法回調、繼承被代理對象,聚合方法代理的。

2.反編譯代理類字節碼

CGLIB提供生成代理類的class文件的配置項。在CGLIB中提供了DebuggingClassWriter類用於將字節碼的byte字節寫入class文件中。

public byte[] toByteArray() {
    
  return (byte[]) java.security.AccessController.doPrivileged(
    new java.security.PrivilegedAction() {
        public Object run() {
            
            // 獲取代理類的字節內容
            byte[] b = ((ClassWriter) DebuggingClassWriter.super.cv).toByteArray();
            if (debugLocation != null) {
                    // 轉換生成的class文件路徑分隔符
                String dirs = className.replace(‘.‘, File.separatorChar);
                try {
                     // 創建class文件
                    new File(debugLocation + File.separatorChar + dirs).getParentFile().mkdirs();
                    
                    File file = new File(new File(debugLocation), dirs + ".class");
                    OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
                    try {
                         // 將字節內容寫入class文件
                        out.write(b);
                    } finally {
                        out.close();
                    }
                    
                    if (traceCtor != null) {
                        file = new File(new File(debugLocation), dirs + ".asm");
                        out = new BufferedOutputStream(new FileOutputStream(file));
                        try {
                            ClassReader cr = new ClassReader(b);
                            PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
                            ClassVisitor tcv = (ClassVisitor)traceCtor.newInstance(new Object[]{null, pw});
                            cr.accept(tcv, 0);
                            pw.flush();
                        } finally {
                            out.close();
                        }
                    }
                } catch (Exception e) {
                    throw new CodeGenerationException(e);
                }
            }
            return b;
         }  
        });
        
    }

從以上可以看出只要配置文件的生成路徑變量debugLocation即可,再來看下該變量初始化賦值情況

static {
      // 從System中取出屬性DEBUG_LOCATION_PROPERTY賦值給文件class文件生成路徑變量
    debugLocation = System.getProperty(DEBUG_LOCATION_PROPERTY);
    if (debugLocation != null) {
        System.err.println("CGLIB debugging enabled, writing to ‘" + debugLocation + "‘");
        try {
          Class clazz = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
          traceCtor = clazz.getConstructor(new Class[]{ClassVisitor.class, PrintWriter.class});
        } catch (Throwable ignore) {
        }
    }
}

可以看出只要應用啟動時

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,
    "/Users/lixinyou/Documents/code-space/java/java-base/java-proxy/target/proxy/impl");

設置DebuggingClassWriter.DEBUG_LOCATION_PROPERTY屬性,在運行時CGLIB便會生成代理類的class。

這種方式與JDK的動態代理中的class文件生成方式一致。這種用法,在日常應用開發中也可借鑒,利用應用啟動參數的不同,可以在運行時改變行為,代碼具有強擴展性。

3.fastClass機制

在看代理機制源碼之前,做好一切準備。再來了解下CGLIB中關於實現快速調用的fastClass機制。
在JDK的動態代理中使用反射調用目標對象,在CGLIB中為了更好的提升性能,采用fastClass機制。

FastClass機制:將類的方法信息解析出來,然後為其建立索引。調用的時候,只要傳索引,就能找到相應的方法進行調用。

  1. 為所有的方法建立索引
  2. 調用前先根據方法信息尋找到索引
  3. 調用時根據索匹配相應的方法進行直接調用

CGLIB在字節碼層面將方法和索引的對應關系建立,避免了反射調用:

public int getIndex(Signature var1) {
    String var10000 = var1.toString();
    switch(var10000.hashCode()) {
        case -2055565910:
            if (var10000.equals("CGLIB$SET_THREAD_CALLBACKS([Lnet/sf/cglib/proxy/Callback;)V")) {
                return 11;
            }
            break;
        case -1980342926:
            if (var10000.equals("print(Ljava/lang/String;)V")) {
                return 6;
            }
            break;
        case -1860420502:
            if (var10000.equals("CGLIB$clone$7()Ljava/lang/Object;")) {
            return 24;
            }
            break;
        case -1725733088:
            if (var10000.equals("getClass()Ljava/lang/Class;")) {
                return 29;
            }
            break;
}
return -1;

}

上述獲取方法的索引,下述代碼再根據索引進行調用:

public Object invoke(int index, Object var2, Object[] var3) throws InvocationTargetException {
    e77dd5ce var10000 = (e77dd5ce)var2;
    try {
        switch(index) {
        case 0:
            return new Boolean(var10000.equals(var3[0]));
        case 1:
            return var10000.toString();
   } catch (Throwable var4) {
        throw new InvocationTargetException(var4);
    }
    throw new IllegalArgumentException("Cannot find matching method/constructor");
}

4.運行時的代理類

CGLIB運行時,實際會生成三個class:

  • 代理類
  • 代理類對應的fastClass
  • 被代理類的fastClass

如上述Demo中生成的代理類和相應的代理類:

  1. EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce$$FastClassByCGLIB$$1b37f797.class
  2. EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce.class
  3. EchoServiceImpl$$FastClassByCGLIB$$44f86581.class

下面就看下生成的類的代碼片段,理解下CGLIB的運行時代理原理。

首先看下生成的代理類

public class EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce extends EchoServiceImpl implements Factory {

代理類是繼承自被代理類,這裏與JDK的不同是,JDK是實現了接口。

下面的即是對echo方法的代理方法:

public final void echo(String var1) {
    // 獲取方法攔截器
    MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
    if (this.CGLIB$CALLBACK_0 == null) {
        CGLIB$BIND_CALLBACKS(this);
        var10000 = this.CGLIB$CALLBACK_0;
    }

    if (var10000 != null) { // 如果不為空,則調用其統一攔截
        var10000.intercept(this, CGLIB$echo$2$Method, new Object[]{var1}, CGLIB$echo$2$Proxy);
    } else { // 如果為空,則直接調用父類即被代理類的方法
        super.echo(var1);
    }
}

最為讓人關註的是intercept方法調用時的參數MethodProxy:CGLIB$echo$2$Proxy

static {
    CGLIB$STATICHOOK1();
}

static void CGLIB$STATICHOOK1() {
    CGLIB$echo$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "echo", "CGLIB$echo$2");
}

在代理類被加載時,執行靜態方法CGLIB$STATICHOOK1(),創建了echo方法對應的方法代理。

public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
    MethodProxy proxy = new MethodProxy();
    proxy.sig1 = new Signature(name1, desc);
    proxy.sig2 = new Signature(name2, desc);
    proxy.createInfo = new CreateInfo(c1, c2);
    return proxy;
}

這裏使用了工廠模式,在創建MethodProxy時,為期成員CreateInfo賦值。c1代表被代理類,c2代表代理類。desc代理方法和被代理方法的參數信息,name1是被代理方法名,name2是代理方法名。

下面再看來下intercepte方法中調用的methodProxy的invokeSuper方法:

public Object invokeSuper(Object obj, Object[] args) throws Throwable {
    try {
        // 先進行初始化
        init();
        // 獲取fastClassInfo對象
        FastClassInfo fci = fastClassInfo;
        // 獲取代理類對應的fastClass對象並按索引調用
        return fci.f2.invoke(fci.i2, obj, args);
    } catch (InvocationTargetException e) {
        throw e.getTargetException();
    }
}

先看下初始化中所執行的邏輯:

private void init()
{
    /* 
     * Using a volatile invariant allows us to initialize the FastClass and
     * method index pairs atomically.
     * 
     * Double-checked locking is safe with volatile in Java 5.  Before 1.5 this 
     * code could allow fastClassInfo to be instantiated more than once, which
     * appears to be benign.
     */
    if (fastClassInfo == null)
    {
        synchronized (initLock)
        {
            if (fastClassInfo == null)
            {
                CreateInfo ci = createInfo;

                FastClassInfo fci = new FastClassInfo();
                // 獲取被代理類
                fci.f1 = helper(ci, ci.c1);
                // 獲取代理類
                fci.f2 = helper(ci, ci.c2);
                // 獲取被代理類的被代理方法的索引
                fci.i1 = fci.f1.getIndex(sig1);
                // 獲取代理類的代理方法的索引
                fci.i2 = fci.f2.getIndex(sig2);
                fastClassInfo = fci;
                createInfo = null;
            }
        }
    }
}

這裏使用了單例模式,fastClassInfo對象是單例。所以初始化方法只會在第一次調用代理方法的時候,才響應的進行對其初始化。

初始化後,就將代理類和代理方法的索引獲取到了,然後再按照索引直接對代理方法進行調用:

public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
    e77dd5ce var10000 = (e77dd5ce)var2;
    int var10001 = var1;

    try {
        switch(var10001) {
        case 19:
            var10000.CGLIB$echo$2((String)var3[0]);
            return null;
        }
    } catch (Throwable var4) {
        throw new InvocationTargetException(var4);
    }
    throw new IllegalArgumentException("Cannot find matching method/constructor");
}

上述考慮到篇幅和簡潔的原因,這裏只摘取了case:19的代碼片段。

通過索引直接調用代理類代理方法:CGLIB$echo$2:

final void CGLIB$echo$2(String var1) {
    super.echo(var1);
}

在該方法中再調用被代理類(繼承了被代理類)的被代理方法。

至此,CGLIB的代理調用原理就是以上的內容。

四.總結

CGLIB是一種字節碼增強庫,利用其提供的字節碼技術可以實現動態代理。其底層依賴ASM字節碼技術。

CGLIB的動態代理與JDK動態代理的不同點:

  • JDK動態代理必須需要接口,JDK代理是基於接口進行動態代理。CGLIB中既支持對接口的代理,也支持對對象的代理。
  • CGLIB動態代理使用fastClass機制實現快速調用被代理類,JDK中使用了反射方式調用被代理。所以CGLIB的動態代理的方式性能上更有優勢。
  • CGLIB額外對來源於Object中的finalize和clone方法也做了攔截代理,JDK只為了equals、hashCode、toString進行代理

註:JDK中生成的代理類已經靜態解析了方法對象作為代理類的靜態變量,類似做緩存,從而部分解決反射的性能問題。

CGLIB的動態代理與JDK動態代理的相同點:

  • 都具有統一接口,JDK動態代理中中間統一接口是InvocationHandler,CGLIB中是MethodInteceptor。
  • 生成的代理類和其中的方法都是final
參考

neoremind/dynamic-proxy
Are there alternatives to cglib
Spring AOP 實現原理與 CGLIB 應用
深入淺出CGlib-打造無入侵的類代理
CGLib: The Missing Manual
Create Proxies Dynamically Using CGLIB Library

動態代理(二)—— CGLIB代理原理