Java 原生基於接口的動態代理

參考文章

java dynamic proxies

前言

代理可以看做是一種封裝, 最終會把功能調用傳遞給背後真正的實現方。 使用代理的原因往往是因爲背後的真實實現方不便於直接被調用(可能是一個遠程對象) 或者需要爲被代理的功能添加一些輔助功能。

注: 根據 GoF 設計模式原書的描述, 代理模式和裝飾器模式的實現方式是完全一樣的, 其區別主要在於使用場景

  • 代理模式的目的在於進行 訪問控制, 其意圖在於爲被代理對象提供一個替身, 而之所以提供替身而不直接訪問的目的是, 原有的被代理對象可能不便於被直接訪問(是一個遠程對象), 或者是一個訪問權限需要被限制的對象。
  • 裝飾器模式的目的在於進行功能添加 / 功能增強 , 其意圖在於爲被裝飾對象動態地增加功能或屬性

筆者認爲這種說法是比較合理的, 畢竟一個代理,從業務邏輯角度來講, 對被代理方應該是透明的。 就好比你委託一箇中介替你完成一些事情, 他所達成的結果,應當和你的期待一致, 不應該有差別。所以代理如果要爲被代理添加一些功能的話, 也應該是業務邏輯無關的功能,例如日誌統計等功能。

動態代理和靜態代理的區別:

  • 動態代理 中的代理關係是程序運行時產生的, 支持動態變化。 靜態代理 的對象間代理關係是編譯前就已經確定的, 在運行時刻, 是固定不變的。
  • 動態代理可以通過單一的一個類中的一個方法爲多個類的多個方法提供代理服務。 動態代理允許你編寫代碼爲任意的一種接口提供代理服務。
  • 其背後的工作原理,是針對特定接口, 創建代理類,使用者可以在這個代理類 Proxy 身上直接調用各種接口方法, 而代理類會把所有的方法調用都轉發給一個單一的 handlerinvoke 方法, 完成真正的邏輯。

雖然動態代理在日常開發中並不經常使用,但是對於框架編寫者而言, 動態代理非常實用。
動態代理也是 Spring AOP 的實現基礎, 本文旨在梳理一下 Java 內置的動態代理實現方式, 供初學者學習參考

JAVA 原生動態代理

java 自帶的動態代理方式是構建在 JDK 中的一個特性, 所以不需要引入任何額外的依賴即可使用。

但是有一個限制: java 內置的動態代理必須依託於接口

jdk 動態代理主要涉及到以下3個概念

  • 接口(Interface )
    • 這裏以一個 FooInterface 進行舉例, FooInterface 中有一個 foo() 方法必須實現
  • 代理類實例 ( Proxy Instance)
    • proxyInstance 是我們通過動態代理創建的代理類實例, 對外表現爲實現了 FooInterface 接口
  • 調用處理器(Invocation Handler)
    • 對於proxyInstance 的方法調用, 會實際觸發 invocationHandler.invoke() 函數

上述三個角色的邏輯工作關係是這樣的 在這裏插入圖片描述

  • Tips: invocationHandler.invoke() 方法, 常常會把針對 proxyInstance 的調用進一步轉發給其他類,這其實是引入了多層代理關係, 即 interface --> invocationHandler–> interfaceImpl 這有時會讓部分讀者產生困惑, 搞不清動態代理的核心工作機制。
  • 實際上, 讀者僅需要記住, 動態代理本身最基礎的邏輯就是完成一層代理關係, 即將針對某一接口的調用, 全部轉發給一個 InvocationHandler 接口實現類的 invoke 方法。
  • 下面的例子一例子二, 分別展示了單層代理關係與多層代理關係的實現方式。

例子一:一個只會打印被調用方法的代理

下面通過一個例子進一步展現使用流程。 我們的目標是創建一個 Map 接口的代理類實例 fakedHashMap, 調用代代理類所實現的 Map 接口中的 get(), put() 方法時, 什麼都不執行, 僅僅打印被執行的方法名稱

  • 接口(Interface ) --> Map
  • 被代理類(Target Class) -->
  • 代理類實例 ( Proxy Instance) --> fakedMap
  • 調用處理器(Invocation Handler) --> MapInvocationHandler

步驟一: 創建 Invocation Handler

InvocationHandlerinvoke

public class DynamicInvocationHandler implements InvocationHandler {
 
    private static Logger LOGGER = LoggerFactory.getLogger(
      DynamicInvocationHandler.class);
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
      throws Throwable {
        LOGGER.info("Invoked method: {}", method.getName());
 
        return 42;
    }
}

步驟二: 創建 Proxy Instance

Map proxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), 
  new Class[] { Map.class }, 
  new DynamicInvocationHandler());

創建好代理類實例 proxyInstance 後, 即可調用被代理的 Map 的接口方法

proxyInstance.put("hello", "world"); // 調用後控制檯輸出 Invoked method: put

例子二:可以代理多個接口的 InvocationHandler

在這個例子中,我們的目標是創建多個類的代理實例, 調用代理實例 proxyInstance 的對應方法後, 除了可以調用到真正的實現類方法, 還可以統計到實現類方法實際執行的耗時。

步驟一: 創建 TimingInvocationHandler

public class TimingDynamicInvocationHandler implements InvocationHandler {
 
    private static Logger LOGGER = LoggerFactory.getLogger(
      TimingDynamicInvocationHandler.class);
     
    private final Map<String, Method> methods = new HashMap<>();
 
    private Object target;
 
 	// 把接口實現類中所實現的 methood 全部記錄到 map 中, 後續方便轉發
    public TimingDynamicInvocationHandler(Object target) {
        this.target = target;
 
        for(Method method: target.getClass().getDeclaredMethods()) {
            this.methods.put(method.getName(), method);
        }
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) 
      throws Throwable {
        long start = System.nanoTime();
        Object result = methods.get(method.getName()).invoke(target, args);
        long elapsed = System.nanoTime() - start;
 
        LOGGER.info("Executing {} finished in {} ns", method.getName(), 
          elapsed);
 
        return result;
    }
}

步驟二: 創建代理實例並調用

Map mapProxyInstance = (Map) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), new Class[] { Map.class }, 
  new TimingDynamicInvocationHandler(new HashMap<>()));
 
mapProxyInstance.put("hello", "world");
 
CharSequence csProxyInstance = (CharSequence) Proxy.newProxyInstance(
  DynamicProxyTest.class.getClassLoader(), 
  new Class[] { CharSequence.class }, 
  new TimingDynamicInvocationHandler("Hello World"));
 
csProxyInstance.length()

注意到, 我們用同一個 InvocationHandler 創建了多個接口(Map, CharSequence)的代理實例, 並將這些接口的調用最終分別轉發到了 HashMap 以及 CharSequence 的實例上。

總結

java 內置實現的動態代理本質是通過一種手段, 創建一個虛擬實現了某 Interface 的代理實例 proxyInstance, 對於該 proxyInstance 直接可以調用 Interface 中的任意方法, 最終會觸發對應的 InvocationHandler 中的 invoke() 函數。 至於 InvocationHandler 最終是否進一步調用真正的 Interface 實現類, 完全取決於自定義的 InvocationHandler 邏輯。

整體工作流程可以簡化爲下圖
在這裏插入圖片描述

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