Java動態代理 jdk和cglib的實現比較

發現Java面試很喜歡問Spring AOP怎麼實現的之類的問題,因此寫一篇文章來整理一下。關於AOP和代理模式的概念這裏並不作贅述,而是直奔主題,即AOP的實現方式:動態代理。與靜態代理對比,動態代理是在runtime動態生成Java代理類,由代理類完成對具體方法的封裝,實現AOP的功能。java

本文將分析Java中兩種動態代理的實現方式,jdk proxycglib,比較它們的異同。本文並不會過多地分析jdk和cglib的源碼去探究底層的實現細節,而只關注最後生成的代理類應該是什麼樣的,如何實現代理。只是我我的的整理和思考,和真正的jdk,cglib的產生的結果可能不盡相同,但從原理上來說是一致的。git

文章的最後也會探討如何本身實現一個簡單的動態代理,並提供我本身實現的簡單版本,固然僅供參考。github

JDK Proxy

這是Java反射包java.lang.reflect提供的動態代理的方式,這種代理方式是徹底基於接口的。這裏先給出一個簡單的例子。面試

定義接口:安全

interface ifc {
  int add(int, int);
}

而後是接口ifc的實現類Real多線程

class Real implements ifc {
  @Override
  public int add(int x, int y) {
    return x + y;
  }

Real就是咱們須要代理的類,好比咱們但願在調用add的先後打印一些log,這實際上就是AOP了。咱們須要最終產生一個代理類,實現一樣的接口ifc,執行Real.add的功能,但須要增長一行新的打印語句。這一切對用戶是透明的,用戶只須要關心接口的調用。爲了能在Real.add的周圍添加額外代碼,動態代理都是經過一種相似方法攔截器的東西來實現的,在Java Proxy裏這就是InvocationHandler.併發

class Handler implements InvocationHandler {
  private final Real real;

  public Handler(Real real) {
    this.real = real;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args)
      throws IllegalAccessException, IllegalArgumentException,
             InvocationTargetException {
    System.out.println("=== BEFORE ===");
    Object re = method.invoke(real, args);
    System.out.println("=== AFTER ===");
    return re;
  }
}

這裏最關鍵的就是invoke方法,實際上代理類的add方法,以及其它方法(若是接口還定義了其它方法),最終都只是調用這個Handlerinvoke方法,由你來具體定義在invoke裏須要作什麼,一般就是調用真正實體類Real的方法,這裏就是add,以及額外的AOP行爲(打印 BEFORE 和 AFTER)。因此可想而知,代理類裏必然是有一個InvocationHandler的實例的,全部的接口方法調用都會由這個handler實例來代理。ide

因此咱們應該能大概刻畫出這個代理類的模樣:函數

public ProxyClass implements ifc {
  private static Method mAdd;

  private InvocationHandler handler;

  static {
    Class clazz = Class.forName("ifc");
    mAdd = clazz.getMethod("add", int.class, int.class);
  }
  
  @Override
  public int add(int x, int y) {
    return (Integer)handler.invoke(this, mAdd, new Object[] {x, y});
  }
}

這個版本很是簡單,但已足夠實現咱們的要求。咱們來觀察這個類,首先毋庸置疑它實現了ifc接口,這是代理模式的根本。它的add方法直接調用InvocationHandler實例的invoke方法,傳入三個參數,第一個是代理類自己this指針,第二個是add方法的反射類,第三個是參數列表。因此在invoke方法裏,用戶就能自由定義它的行爲實現AOP,全部這一切的橋樑就是InvocationHandler,它完成方法的攔截與代理。this

代理模式通常要求代理類中有一個真正類(被代理類)的實例,在這裏也就是Real的實例,這樣代理類才能去調用Real中本來的add方法。那Real在哪裏呢?答案也是在InvocationHandler裏。這與標準的代理模式相比,彷佛多了一層嵌套,不過這並無關係,只要這個代理的鏈條可以搭建起來,它就符合代理模式的要求。

注意到這裏add方法的反射實例mAdd的初始化方式,咱們使用靜態塊static {...}來完成,只會被設置一次,而且不會有多線程問題。固然你也能夠用懶加載等方式,不過就得考慮併發的安全性。

最後看一下JDK Proxy的具體使用:

Handler handler = new Handler(new Real());
ifc p = (ifc)Proxy.newProxyInstance(ifc.class.getClassLoader(),
                                    new Class[] {ifc},
                                    handler);
p.add(1, 2);

方法newProxyInstance就會動態產生代理類,而且返回給咱們一個實例,實現了ifc接口。這個方法須要三個參數,第一個ClassLoader並不重要;第二個是接口列表,即這個代理類須要實現那些接口,由於JDK的Proxy是徹底基於接口的,它封裝的是接口的方法而不是實體類;第三個參數就是InvocationHandler的實例,它會被放置在最終的代理類中,做爲方法攔截和代理的橋樑。注意到這裏的handler包含了一個Real實例,這在上面已經說過是代理模式的必然要求。

總結一下JDK Proxy的原理,首先它是徹底面向接口的,其實這纔是符合代理模式的標準定義的。咱們有兩個類,被代理類Real和須要動態生成的代理類ProxyClass,都實現了接口ifc。類ProxyClass須要攔截接口ifc上全部方法的調用,而且最終轉發到實體類Real上,這二者之間的橋樑就是方法攔截器InvocatioHandlerinvoke方法。

上面的例子裏我給出類ProxyClass的源代碼,固然實際上JDK Proxy是不會去產生源代碼的,而是直接生成類的原始數據,它具體是怎麼實現咱們暫時不討論,咱們目前只須要關心這個類是什麼樣的,以及它實現代理的原理。

cglib實現動態代理

這是Spring使用的方式,與JDK Proxy不一樣之處在於它不是面向接口的,而是基於類的繼承。這彷佛是有點違背代理模式的標準格式,不過這沒有關係,所謂的代理模式只是一種思想而不是嚴格的規範。咱們直接看它是如何使用的。

如今沒有接口,咱們直接有實體類:

class Real {
  public int add(int x, int y) {
    return x + y;
  }
}

相似於InvocationHandler,這裏cglib直接使用一個叫MethodInterceptor的類,顧名思義。

public class Interceptor implements MethodInterceptor {
  @Override
  public Object intercept(Object obj,
                          Method method,
                          Object[] args,
                          MethodProxy proxy) throws Throwable {
      System.out.println("=== BEFORE ===");
      Object re = proxy.invokeSuper(obj, args);
      System.out.println("=== AFTER ===");
      return re;
  }
}

使用方法:

public static void main(String[] args) {
  Enhancer eh = new Enhancer();
  eh.setSuperclass(Real.class);
  eh.setCallback(new Interceptor());

  Real r = (Real)eh.create();
  int result = r.add(1, 2);
}

若是你仔細和JDK Proxy比較,會發現它們實際上是相似的:

  1. 首先JDK Proxy提供interface列表,而cglib提供superclass供代理類繼承,本質上都是同樣的,就是提供這個代理類的簽名,也就是對外表現爲何類型。
  2. 而後是一個方法攔截器,JDK Proxy裏是InvocationHandler,而cglib裏通常就是MethodInterceptor,全部被代理的方法的調用是經過它們的invokeintercept方法進行轉接的,AOP的邏輯也是在這一層實現。

它們不一樣之處上面已經說了,就在於cglib生成的動態代理類是直接繼承原始類的,因此咱們這裏也能夠大概刻畫出這個代理類長什麼樣子:

public ProxyClass extends Real {
  private static Method mAdd;
  private static MethodProxy mAddProxy;

  private MethodInterceptor interceptor;

  static {
    Class clazz = Class.forName("ifc");
    mAdd = clazz.getMethod("add", int.class, int.class);
    // Some logic to generate mAddProxy.
    // ...
  }
  
  @Override
  public int add(int x, int y) {
    return (Integer)interceptor.invoke(
        this, mAdd, new Object[] {x, y}, mAddProxy);
  }
}

由於直接繼承了Real,那天然就包含了Real的全部public方法,都經過interceptor.invoke進行攔截代理。這其實和上面JDK Proxy的原理是相似的,連invokeintercept方法的簽名都差很少,第一個參數是this指針代理類自己,第二個參數是方法的反射,第三個參數是方法調用的參數列表。惟一不一樣的是,這裏多出一個MethodProxy,它是作什麼用的?

若是你仔細看這裏invoke方法內部的寫法,當用戶想調用原始類(這裏是Real)定義的方法時,它必須使用:

Object re = proxy.invokeSuper(obj, args);

這裏就用到了那個MethodProxy,那咱們爲何不直接寫:

Object re = method.invoke(obj, args);

答案固然是不能夠,你不妨試一下,程序會進入一個無限遞歸調用。這裏的緣由偏偏就是由於代理類是繼承了原始類的,obj指向的就是代理類對象的實例,因此若是你對它使用method.invoke,因爲多態性,就會又去調用代理類的add方法,繼而又進入invoke方法,進入一個無限遞歸:

obj.add() {
  interceptor.invoke() {
    obj.add() {
      interceptor.invoke() {
        ...
      }
    }
  }
}

那我如何才能在interceptor.invoke()裏去調用基類Realadd方法呢?固然一般作法是super.add(),然而這是在MethodInterceptor的方法裏,並且這裏的method調用必須經過反射完成,你並不能在語法層面上作到這一點。因此cglib封裝了一個類叫MethodProxy幫助你,這也是爲何那個方法的名字叫invokeSuper,代表它調用的是原始基類的真正方法。它到底是怎麼辦到的呢?你能夠簡單理解爲,動態代理類裏會生成這樣一個方法:

int super_add(int x, int y) {
  return super.add(x, y);
}

固然你並不知道有這麼一個方法,但invokeSuper會最終找到這個方法並調用,這都是在生成代理類時經過一系列反射的機制實現的,這裏就不細展開了。

小結

以上我對比了JDK Proxycglib動態代理的使用方法和實現上的區別,它們本質上是相似的,都是提供兩個最重要的東西:

  1. 接口列表或者基類,定義了代理類(固然也包括原始類)的簽名。
  2. 一個方法攔截器,完成方法的攔截和代理,是全部調用鏈的橋樑。

須要說明的一點是,以上我給出的代理類ProxyClass的源代碼,僅是參考性的最精簡版本,只是爲了說明原理,而不是JDK Proxycglib真正生成的代理類的樣子,真正的代理類的邏輯要複雜的多,可是原理上基本是一致的。另外以前也說到過,事實上它們也不會生成源碼,而是直接產生類的字節碼,例如cglib是封裝了ASM來直接生成Class數據的。

如何生成代理類

接下來的部分純粹是實驗性質的。既然知道了代理類長什麼樣,可能仍是有人會關心底層究竟如何在runtime動態生成這個類,這裏我我的想了兩種方案。

第一種方法是動態生成ProxyClass源碼,而後動態編譯,就能獲得Class了。這裏就須要利用反射,加上一系列字符串拼接,生成源碼。若是你充分理解代理類應該長什麼樣,其實並非很難作到。那如何動態編譯呢?你可使用JOOR,這是一個封裝了javax.tools.JavaCompiler的庫,幫助你方便地實現動態編譯Java源代碼。我試着寫了一個Demo,純粹是實驗性質的。並且它有個重大問題,我不知道如何修改它編譯使用的classpath,在默認狀況下它沒法引用到你本身定義的任何類,由於它們不在編譯的classpath裏,編譯就不會經過,這實際上就使得這個代碼生成器沒有任何卵用。。。我強行經過修改System.setPropertyclasspath來添加個人class路徑繞開了這個問題,然而這顯然不是個解決根本問題的方法。

第二種方法更直接,就是生成類的字節碼。這也是cglib使用的方法,它封裝了ASM,這是一個能夠用來直接操縱Class數據的庫,經過它你就能夠任意生成或修改你想要的Class,固然這須要你對虛擬機的字節碼比較瞭解,才能玩得通這種比較黑科技的套路。這裏我也寫了一個Demo,也純粹是實驗而已,感興趣的童鞋也能夠本身試一下。寫字節碼仍是挺酸爽的,它相似彙編但其實比彙編容易的多。它不像彙編那樣一下子寄存器一下子內存地址,一下子堆一下子棧,各類變量和地址繞來繞去。字節碼的執行方式是很清晰的,變量都存儲在本地變量表裏,棧只是用來作函數調用,因此很是直觀。

相關文章
相關標籤/搜索