深入理解 Java 動態代理

參考鏈接:http://blog.javaforge.net/post/55630869369/creating-class-proxies-with-javassist
參考鏈接:http://stackoverflow.com/questions/442747/getting-the-name-of-the-current-executing-method

最近在讀 mybatis 源碼的時候想研究一下 mybatis 的懶加載是怎麼工作的。
基本的原理我是知道的,用了代理對象。但是代理對象又是怎麼工作的,就不太清楚,我有自己的兩種想法但不知道具體哪一種是對的。

javassist 動態代理 demo

mybatis 框架用 javassist 爲懶加載對象創建了代理對象。
下面是代碼,完整工程可以在我的 github 下載:demo

public class ProxyFactoryExample {
      public void foo() throws Exception {
          System.out.println("Foo method executed.");
      }

    public static void main(String[] args) throws Throwable {
        ProxyFactory factory = new ProxyFactory(); // 代理工廠
        factory.setSuperclass(ProxyFactoryExample.class); // 設置父類
        MethodHandler tracingMethodHandler = new MethodHandler() {
            public Object invoke(Object self, Method thisMethod,
                                 Method proceed, Object[] args) throws Throwable {
                return proceed.invoke(self, args);
            }
        };
        // 創建代理對象 第一個參數代表的是調用構造方法創建代理對象時所需的參數類型,在這裏我們的構造方法不需要參數,所以傳一個空的 class 類型數組,即: new Class[0] 。
        // 第二個參數代表的是具體的參數值,在這裏我們需要參數,所以傳一個空的 Object 類型數組,即: new Object[0]。第三個參數是 MethodHandler 對象,需要把代理對象和 MethodHandler 對象關聯在一起。
        ProxyFactoryExample proxyObj = (ProxyFactoryExample) factory.create(
                new Class[0], new Object[0], tracingMethodHandler);

        proxyObj.foo();
    }
  }  

javassist 動態代理簡單分析

代碼很少,我簡單的講一下這段代碼是幹什麼的。
ProxyFactory 是個工廠類,工廠類自然是創建對象的。ProxyFactory 創建的代理對象的所屬類需要繼承一個類,我們創建的這個代理對象的所屬類繼承自 ProxyFactoryExample 類。

在創建代理對象時需要關聯一個 MethodHandler 對象。在代理對象上調用方法時,所有的方法都會轉發給 MethodHandler 對象,由 MethodHandler 的 invoke 方法來統一處理。

下面,我們運行一下代碼,結果是:

Foo method executed.

利用 IntelliJ 工具剖析 javassist 動態代理

但是,代碼是怎麼工作的,還是有些迷糊。

下面,我們通過 IntelliJ 的調試功能來分析一下(順便安利一下,IntelliJ 真是一款神器):

這裏寫圖片描述

通過截圖我們可以看到 proxyObj 的類型是 ProxyFactoryExample_$$_jvst9eb_0 ,我們也可以通過 IntelliJ 的 Evaluate Code Fragment 工具來動態的執行代碼,通過執行代碼來觀察 proxyObj 代理對象。

這裏寫圖片描述

通過執行下面這行代碼可以獲取到代理類的父類:

proxyObj.getClass().getSuperclass();// 獲取代理類的父類

這裏寫圖片描述

從截圖中可以看出代理類繼承了 ProxyFactoryExample 類。

通過執行下面這行代碼可以獲取到代理類所實現的接口:

proxyObj.getClass().getInterfaces();

這裏寫圖片描述

由上面的截圖可以看出:代理類實現了 ProxyObject 接口。

執行下面這行代碼可以獲取到代理類的所有屬性:

// 獲取到 proxyObj 對象所屬的類所定義的所有的屬性(包括公有、私有以及任何訪問權限的),但不包括其父類/接口定義的任何屬性。詳情見 Class.getDeclaredFields(); 方法的說明。
proxyObj.getClass().getDeclaredFields(); 

這裏寫圖片描述

可以從上面的截圖中看出該代理類共定義了 4 個成員變量:

  • handler
  • filter_signature
  • serialVersionUID
  • methods

下面這行代碼可以獲取到代理類所定義的所有方法:

// 獲取到 proxyObj 對象所屬的類所定義的所有的方法(包括公有、私有以及任何訪問權限的),但不包括其父類/接口定義的任何方法。詳情見 Class.getDeclaredMethods(); 方法的說明。
proxyObj.getClass().getDeclaredMethods();

這裏寫圖片描述

從上面的截圖中我們可以看出該代理類共定義了 15 個方法:

  • getHandler
  • finalize
  • equals
  • toString
  • hashCode
  • clone
  • writeReplace
  • foo
  • setHandler
  • _d0clone
  • _d1equals
  • _d2finalize
  • _d3foo
  • _d5hashCode
  • _d9toString

其中 getHandler 和 setHandler 這兩個方法是 ProxyObject 接口的兩個方法,代理類又重寫了這兩個方法。
其中 finalize、equals、toString、hashCode、clone 這 5 個方法是 Object 類的 5 個方法,代理類重寫了這 5 個方法。
其中 foo 方法是代理類的父類 ProxyFactoryExample 的方法,代理類也重寫了這個方法。
writeReplace 方法不知道是幹什麼用的,好像 ProxyFactory 創建的每個代理類都有這個方法。
還剩下 6 個方法,這 6 個方法的面孔看起來很熟悉:

  • _d0clone 像 clone
  • _d1equals 像 equals
  • _d2finalize 像 finalize
  • _d3foo 像 foo
  • _d5hashCode 像 hashCode
  • _d9toString 像 toString

通過上面對代理對象的反射分析,我們大致的可以看出代理類是個什麼樣子了。

代理類繼承了被代理的類(ProxyFactoryExample)並且重寫了被代理類的所有方法,重寫的方法的實現都是類似的,這些方法都調用了代理類所關聯的 handler 對象的 invoke() 方法。所有的邏輯都由 invoke() 方法來控制,invoke() 方法像一個控制中心一樣,根據自己的需求定製實現自己的邏輯。
在這裏,我們是調用重寫方法相對應的那個方法。重寫方法相對應的那個方法又調用了父對象的簽名完全相同的方法。

用靜態代理來模擬 javassist 動態代理

上面的話有些繞,下面我用靜態代理來模擬動態代理的調用行爲:

public class ProxyFactoryExample_$$_jvst9eb_0 extends ProxyFactoryExample implements ProxyObject {
        private MethodHandler handler;
        public ProxyFactoryExample_$$_jvst9eb_0(MethodHandler handler) {
        this.handler = handler;
    }

    public static void main(String[] args) throws Throwable {
        MethodHandler tracingMethodHandler = new MethodHandler() {
            public Object invoke(Object self, Method thisMethod,
                                 Method proceed, Object[] args) throws Throwable {
                return proceed.invoke(self, args);
            }
        };

        ProxyFactoryExample proxyObj = new ProxyFactoryExample_$$_jvst9eb_0(tracingMethodHandler);
        proxyObj.foo();
    }

    @Override
    public final void foo() throws Exception {
        try {
            // 獲取 ProxyFactoryExample 類的 foo() 方法
            Method superMethod = this.getClass().getMethod("foo");
            // 獲取 ProxyFactoryExample_$$_jvst9eb_0 類的 _d3foo() 方法
            Method proceedMethod = this.getClass().getDeclaredMethod("_d3foo");
            handler.invoke(this, superMethod, proceedMethod, new Object[]{});
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
    public final void _d3foo()  throws Exception  {
        super.foo();
    }


    public void setHandler(MethodHandler methodHandler) {
        this.handler = methodHandler;

    }

    public MethodHandler getHandler() {
        return handler;
    }
    // 省略了其他方法,其他方法都是類似的
}

用 IntelliJ 工具來驗證 javassist 代理的工作原理

難道動態代理真的就像我上面說的那樣?我最先開始也有這樣的疑問,我原來對重寫方法(foo)相關聯的那個方法(_d3foo)是怎麼實現的有疑問。我原來認爲有兩種情況都可以有相同的行爲:

  • 一種是上面的這種方法,直接調用父方法:

    public final void _d3foo()  throws Exception  {
         super.foo();
    }
    
  • 另一種是把父類的 foo() 方法的實現拷貝過來:

    public final void _d3foo()  throws Exception  {
         System.out.println("Foo method executed.");
    }
    

上面的這兩種方法執行後的行爲都是一樣的,我們可以想辦法來驗證 javassist 到底採用的是哪一種方法,至少有 3 種方法可以驗證,在這裏我只講其中的一種。

Thread.currentThread().getStackTrace(); // 可以獲取到堆棧信息,通過堆棧信息可以知道方法的執行過程

在這裏,我在 foo() 方法上打個斷點,利用 IntelliJ 強大的調試功能來查看方法的執行過程:

這裏寫圖片描述

從圖上可以看出共有 9 次方法調用,中間的幾次調用是 Java 反射調用,我們不用管它。我們關心的調用過程是:

ProxyFactoryExample.main()->
ProxyFactoryExample_$$_jvst9eb_0.foo()->
ProxyFactoryExample $ 1.invoke()->
反射調用->
ProxyFactoryExample jvst9eb_0._d3foo()->
ProxyFactoryExample.foo()

其實就是 main()-> foo()-> invoke() ->反射調用 -> _d3foo() -> foo()

注意,上面的第一個 foo() 方法是代理類的(子類),第二個 foo() 方法是被代理類的(父類)。

通過上面的代碼、截圖和分析大家應該對 Java 動態代理有了個清晰的認識。

注意:我上面的分析都是原理的分析,實現肯定不是這樣的,javassist 最終操作的是字節碼,而我上面是利用 java 代碼來模擬分析的,但原理應該是類似的。

JDK 動態代理和 javassist 動態代理

上面是對 javassist 創建的動態代理的分析,JDK 自帶的動態代理的執行過程和這個是很類似的。
之前我已經寫過一篇關於 Java 動態代理的文章:代理模式和 Java 動態代理 ,對 JDK 自帶的動態代理不熟悉的同學可以先看一下我前面的博客。

我總結下二者的異同點:

  • JDK 動態代理需要實現 0 個或多個接口,實現 0 個接口時傳一個空的數組,不會報錯,但在實際中應該不會出現這種場景。
  • javassist 動態代理可以繼承一個類的同時還可以實現多個接口,當然一個類也不繼承,一個接口也不實現,也不會報錯,但應該不會存在這種使用場景。
  • JDK 沒法爲一個類創建動態代理,只能爲一個接口創建動態代理,而 javassist 動態代理則可以。
  • JDK 動態代理類實現了接口中的所有方法並把這些方法的調用都轉發到了 InvocationHandler 的 invoke 方法,這點和 javassist 創建的動態代理類是相似的,javassist 創建的動態代理類也實現了父類/接口的所有的方法,並把對這些方法的調用轉發到 MethodHandler 的 invoke() 方法,由 invoke() 方法來統一處理。

  • javassist 創建的動態代理類還新增了一些父類/接口方法的關聯方法,例如上例中 _d3foo() 方法就是 foo() 方法的關聯方法,但在 JDK 創建的動態代理類中是沒有這些關聯方法的。因爲前者的關聯方法的目的是調用父類的相應的方法,而後者 JDK 創建的動態代理實現的是接口,接口中只是聲明瞭方法簽名,而沒有方法的實現,所以自然也就不會有關聯方法。

  • JDK 動態代理中的 InvocationHandler 接口和 javassist 動態代理中的 MethodHandler 是相似的,兩者的作用相似,兩者還都有 invoke() 方法。不過這兩者的 invoke() 方法的參數不一樣。

    // InvocationHandler 的 invoke 方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    
    // MethodHandler 的 invoke 方法
    public bject invoke(Object proxy, Method method, Method proceedMethod, Object[] args) throws Throwable;
    

    可以看出 MethodHandler 的 invoke 方法要比 InvocationHandler 的 invoke 方法多一個參數,多了的這個參數是 proceedMethod ,這個參數是 javassist 中的關聯方法,在 JDK 動態代理類中是沒有關聯方法的。其餘幾個參數都是一樣的,作用也是相同的。proxy 是代理對象,InvocationHandler 中的 method 是父接口的 method ,MethodHandler 中的 method 是父類/接口的方法, args 是調用其他方法所需要的參數。

大家可以在 github 上下載我的代碼:https://github.com/fengsmith/javassist-demo用我上面分析 javassist 代理的方法來分析 JDK 動態代理,我就不在這兒再次分析了,寫一篇博客花費的時間太多了。分析的方法是一樣的:利用 IntelliJ 斷點調試、Evaluate Code Fragment(動態執行代碼的工具)、Thread.currentThread().getStackTrace();、Java 反射等方法在程序運行時來剖析動態代理類。

希望我的博客對大家理解動態代理有幫助,如果在閱讀博客的過程中有什麼疑問可以留言,如果我的博客幫助到了大家,大家也可以留言告訴我。

最後我再安利一下:IntelliJ 是一款 Java 開發神器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章