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

hyuan發表於2019-01-19

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

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

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

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例項來代理。

所以我們應該能大概刻畫出這個代理類的模樣:

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,它完成方法的攔截與代理。

代理模式一般要求代理類中有一個真正類(被代理類)的例項,在這裡也就是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,也純粹是實驗而已,感興趣的童鞋也可以自己試一下。寫位元組碼還是挺酸爽的,它類似彙編但其實比彙編容易的多。它不像彙編那樣一會兒暫存器一會兒記憶體地址,一會兒堆一會兒棧,各種變數和地址繞來繞去。位元組碼的執行方式是很清晰的,變數都儲存在本地變數表裡,棧只是用來做函式呼叫,所以非常直觀。

相關文章