1. 程式人生 > >Java Proxy 和 CGLIB 動態代理原理

Java Proxy 和 CGLIB 動態代理原理

動態代理在Java中有著廣泛的應用,比如Spring AOP,Hibernate資料查詢、測試框架的後端mock、RPC,Java註解物件獲取等。靜態代理的代理關係在編譯時就確定了,而動態代理的代理關係是在編譯期確定的。靜態代理實現簡單,適合於代理類較少且確定的情況,而動態代理則給我們提供了更大的靈活性。今天我們來探討Java中兩種常見的動態代理方式:JDK原生動態代理和CGLIB動態代理。

JDK原生動態代理

先從直觀的示例說起,假設我們有一個介面Hello和一個簡單實現HelloImp:

// 介面

interface Hello{

    String sayHello(String str);

}

// 實現

class HelloImp implements Hello{

    @Override

    public String sayHello(String str) {

        return "HelloImp: " + str;

    }

}

這是Java種再常見不過的場景,使用介面制定協議,然後用不同的實現來實現具體行為。假設你已經拿到上述類庫,如果我們想通過日誌記錄對sayHello()的呼叫,使用靜態代理可以這樣做:

// 靜態代理方式

class StaticProxiedHello implements Hello{

    ...

    private Hello hello = new HelloImp();

    @Override

    public String sayHello(String str) {

        logger.info("You said: " + str);

        return hello.sayHello(str);

    }

}

上例中靜態代理類StaticProxiedHello作為HelloImp的代理,實現了相同的Hello介面。用Java動態代理可以這樣做:

  1. 首先實現一個InvocationHandler,方法呼叫會被轉發到該類的invoke()方法。

  2. 然後在需要使用Hello的時候,通過JDK動態代理獲取Hello的代理物件。

// Java Proxy

// 1. 首先實現一個InvocationHandler,方法呼叫會被轉發到該類的invoke()方法。

class LogInvocationHandler implements InvocationHandler{

    ...

    private Hello hello;

    public LogInvocationHandler(Hello hello) {

        this.hello = hello;

    }

    @Override

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if("sayHello".equals(method.getName())) {

            logger.info("You said: " + Arrays.toString(args));

        }

        return method.invoke(hello, args);

    }

}

// 2. 然後在需要使用Hello的時候,通過JDK動態代理獲取Hello的代理物件。

Hello hello = (Hello)Proxy.newProxyInstance(

    getClass().getClassLoader(), // 1. 類載入器

    new Class<?>[] {Hello.class}, // 2. 代理需要實現的介面,可以有多個

    new LogInvocationHandler(new HelloImp()));// 3. 方法呼叫的實際處理者

System.out.println(hello.sayHello("I love you!"));

執行上述程式碼輸出結果:

日誌資訊: You said: [I love you!]

HelloImp: I love you!

上述程式碼的關鍵是Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler)方法,該方法會根據指定的引數動態建立代理物件。三個引數的意義如下:

  1. loader,指定代理物件的類載入器;

  2. interfaces,代理物件需要實現的介面,可以同時指定多個介面;

  3. handler,方法呼叫的實際處理者,代理物件的方法呼叫都會轉發到這裡(*注意1)。

newProxyInstance()會返回一個實現了指定介面的代理物件,對該物件的所有方法呼叫都會轉發給InvocationHandler.invoke()方法。理解上述程式碼需要對Java反射機制有一定了解。動態代理神奇的地方就是:

  1. 代理物件是在程式執行時產生的,而不是編譯期;

  2. 對代理物件的所有介面方法呼叫都會轉發到InvocationHandler.invoke()方法,在invoke()方法裡我們可以加入任何邏輯,比如修改方法引數,加入日誌功能、安全檢查功能等;之後我們通過某種方式執行真正的方法體,示例中通過反射呼叫了Hello物件的相應方法,還可以通過RPC呼叫遠端方法。

注意1:對於從Object中繼承的方法,JDK Proxy會把hashCode()、equals()、toString()這三個非介面方法轉發給InvocationHandler,其餘的Object方法則不會轉發。詳見JDK Proxy官方文件。

如果對JDK代理後的物件型別進行深挖,可以看到如下資訊:

# Hello代理物件的型別資訊

class=class jdkproxy.$Proxy0

superClass=class java.lang.reflect.Proxy

interfaces: 

interface jdkproxy.Hello

[email protected]

代理物件的型別是jdkproxy.$Proxy0,這是個動態生成的型別,類名是形如$ProxyN的形式;父類是java.lang.reflect.Proxy,所有的JDK動態代理都會繼承這個類;同時實現了Hello介面,也就是我們介面列表中指定的那些介面。

如果你還對jdkproxy.$Proxy0具體實現感興趣,它大致長這個樣子:

// JDK代理類具體實現

public final class $Proxy0 extends Proxy implements Hello

{

  ...

  public $Proxy0(InvocationHandler invocationhandler)

  {

    super(invocationhandler);

  }

  ...

  @Override

  public final String sayHello(String str){

    ...

    return super.h.invoke(this, m3, new Object[] {str});// 將方法呼叫轉發給invocationhandler

    ...

  }

  ...

}

這些邏輯沒什麼複雜之處,但是他們是在執行時動態產生的,無需我們手動編寫。更多詳情,可參考 BrightLoong的Java靜態代理&動態代理筆記

https://www.jianshu.com/p/e2917b0b9614

Java動態代理為我們提供了非常靈活的代理機制,但Java動態代理是基於介面的,如果物件沒有實現介面我們該如何代理呢?CGLIB登場。

CGLIB動態代理

CGLIB(Code Generation Library)是一個基於ASM的位元組碼生成庫,它允許我們在執行時對位元組碼進行修改和動態生成。CGLIB通過繼承方式實現代理。

來看示例,假設我們有一個沒有實現任何介面的類HelloConcrete:

public class HelloConcrete {

    public String sayHello(String str) {

        return "HelloConcrete: " + str;

    }

}

因為沒有實現介面該類無法使用JDK代理,通過CGLIB代理實現如下:

  1. 首先實現一個MethodInterceptor,方法呼叫會被轉發到該類的intercept()方法。

  2. 然後在需要使用HelloConcrete的時候,通過CGLIB動態代理獲取代理物件。

// CGLIB動態代理

// 1. 首先實現一個MethodInterceptor,方法呼叫會被轉發到該類的intercept()方法。

class MyMethodInterceptor implements MethodInterceptor{

  ...

    @Override

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        logger.info("You said: " + Arrays.toString(args));

        return proxy.invokeSuper(obj, args);

    }

}

// 2. 然後在需要使用HelloConcrete的時候,通過CGLIB動態代理獲取代理物件。

Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(HelloConcrete.class);

enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();

System.out.println(hello.sayHello("I love you!"));

執行上述程式碼輸出結果:

日誌資訊: You said: [I love you!]

HelloConcrete: I love you!

上述程式碼中,我們通過CGLIB的Enhancer來指定要代理的目標物件、實際處理代理邏輯的物件,最終通過呼叫create()方法得到代理物件,對這個物件所有非final方法的呼叫都會轉發給MethodInterceptor.intercept()方法,在intercept()方法裡我們可以加入任何邏輯,比如修改方法引數,加入日誌功能、安全檢查功能等;通過呼叫MethodProxy.invokeSuper()方法,我們將呼叫轉發給原始物件,具體到本例,就是HelloConcrete的具體方法。CGLIG中MethodInterceptor的作用跟JDK代理中的InvocationHandler很類似,都是方法呼叫的中轉站。

注意:對於從Object中繼承的方法,CGLIB代理也會進行代理,如hashCode()、equals()、toString()等,但是getClass()、wait()等方法不會,因為它是final方法,CGLIB無法代理。

如果對CGLIB代理之後的物件型別進行深挖,可以看到如下資訊:

# HelloConcrete代理物件的型別資訊

class=class cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52

superClass=class lh.HelloConcrete

interfaces: 

interface net.sf.cglib.proxy.Factory

invocationHandler=not java proxy class

我們看到使用CGLIB代理之後的物件型別是cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52,這是CGLIB動態生成的型別;父類是HelloConcrete,印證了CGLIB是通過繼承實現代理;同時實現了net.sf.cglib.proxy.Factory介面,這個介面是CGLIB自己加入的,包含一些工具方法。

注意,既然是繼承就不得不考慮final的問題。我們知道final型別不能有子類,所以CGLIB不能代理final型別,遇到這種情況會丟擲類似如下異常:

java.lang.IllegalArgumentException: Cannot subclass final class cglib.HelloConcrete

同樣的,final方法是不能過載的,所以也不能通過CGLIB代理,遇到這種情況不會拋異常,而是會跳過final方法只代理其他方法。

如果你還對代理類cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52具體實現感興趣,它大致長這個樣子:

// CGLIB代理類具體實現

public class HelloConcrete$$EnhancerByCGLIB$$e3734e52

  extends HelloConcrete

  implements Factory

{

  ...

  private MethodInterceptor CGLIB$CALLBACK_0; // ~~

  ...

  public final String sayHello(String paramString)

  {

    ...

    MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;

    if (tmp17_14 != null) {

      // 將請求轉發給MethodInterceptor.intercept()方法。

      return (String)tmp17_14.intercept(this, 

              CGLIB$sayHello$0$Method, 

              new Object[] { paramString }, 

              CGLIB$sayHello$0$Proxy);

    }

    return super.sayHello(paramString);

  }

  ...

}

上述程式碼我們看到,當呼叫代理物件的sayHello()方法時,首先會嘗試轉發給MethodInterceptor.intercept()方法,如果沒有MethodInterceptor就執行父類的sayHello()。這些邏輯沒什麼複雜之處,但是他們是在執行時動態產生的,無需我們手動編寫。如何獲取CGLIB代理類位元組碼可參考Access the generated byte[] array directly。

更多關於CGLIB的介紹可以參考Rafael Winterhalter的cglib: The missing manual,一篇很深入的文章。

https://dzone.com/articles/cglib-missing-manual

結語

本文介紹了Java兩種常見動態代理機制的用法和原理,JDK原生動態代理是Java原生支援的,不需要任何外部依賴,但是它只能基於介面進行代理;CGLIB通過繼承的方式進行代理,無論目標物件有沒有實現介面都可以代理,但是無法處理final的情況。

動態代理是Spring AOP(Aspect Orient Programming, 面向切面程式設計)的實現方式,瞭解動態代理原理,對理解Spring AOP大有幫助。