1. 程式人生 > >深入理解 Java 動態代理

深入理解 Java 動態代理

最近在讀 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 開發神器。