首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 動態代理機制分析與擴展(第 1 部分):核心原理與程式碼推演

     編輯:Java 相關知識

引言

Java 動態代理機制的出現,使 Java 開發人員無需手工編寫代理類別,只需簡單指定一組介面與委託類別物件,便能動態獲得代理類別。代理類別負責將所有方法呼叫分派至委託物件上反射執行,分派過程中開發人員還可按需調整委託類別物件及其功能,這是一套非常靈活彈性的代理框架。閱讀本文將對 Java 動態代理機制有更深入理解。本文從執行機制與特點出發,對程式碼進行分析,推演動態生成類別內部實作。

代理:設計模式

代理是常用設計模式,目的為其他物件提供一個代理以控制對某物件存取。代理類別負責為委託類別預處理訊息、過濾訊息並轉發訊息、以及委託類別執行後後續處理。

代理模式圖解

為保持行為一致性,代理類別與委託類別通常實作相同介面,所以在存取者看來兩者無絲毫差別。透過代理類別這中間層,能有效控制對委託類別物件直接存取,也能很好隱藏保護委託類別物件,同時為實施不同控制策略預留空間,從而在設計上獲得更大靈活性。Java 動態代理機制以巧妙方式近乎完美實踐代理模式設計理念。

相關類別與介面

了解 Java 動態代理機制,首先需了解以下相關類別或介面:

  • java.lang.reflect.Proxy:Java 動態代理機制主類別,提供一組靜態方法為一組介面動態生成代理類別及其物件。
  • java.lang.reflect.InvocationHandler:調用處理器介面,自訂 invoke 方法集中處理動態代理類別物件上方法呼叫,通常在該方法實作對委託類別代理存取。
  • java.lang.ClassLoader:類別載入器類別,負責將類別位元組碼載入 Java 虛擬機(JVM)並定義類別物件,然後類別才能被使用。Proxy 靜態方法生成動態代理類別同樣需透過類別載入器載入才能使用,與普通類別唯一差別:其位元組碼由 JVM 執行時動態生成而非預存在 .class 檔中。

Proxy 靜態方法

// 方法 1:取得指定代理物件關聯調用處理器
static InvocationHandler getInvocationHandler(Object proxy)
// 方法 2:取得關聯指定類別載入器與一組介面動態代理類別類別物件
static Class getProxyClass(ClassLoader loader, Class[] interfaces)
// 方法 3:判斷指定類別物件是否為動態代理類別
static boolean isProxyClass(Class cl)
// 方法 4:為指定類別載入器、一組介面及調用處理器生成動態代理類別實例
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)

InvocationHandler 核心方法

// 負責集中處理動態代理類別上所有方法呼叫。第一參數為代理類別實例,第二為被呼叫方法物件,第三為呼叫參數。調用處理器根據三參數預處理或分派至委託類別實例發射執行
Object invoke(Object proxy, Method method, Object[] args)

每次生成動態代理類別物件需指定實作該介面調用處理器物件(參見 Proxy 靜態方法 4 第三參數)。每次生成動態代理類別物件需指定類別載入器物件(參見 Proxy 靜態方法 4 第一參數)。

代理機制及其特點

使用 Java 動態代理具體四步驟:

  1. 實作 InvocationHandler 介面建立調用處理器
  2. 為 Proxy 指定 ClassLoader 與一組 interface 建立動態代理類別
  3. 透過反射取得動態代理類別建構子,唯一參數型別為調用處理器介面型別
  4. 透過建構子建立動態代理類別實例,建構時調用處理器物件作為參數傳入
// InvocationHandlerImpl 實作 InvocationHandler 介面,能實現方法呼叫從代理類別到委託類別分派轉發
// 內部通常包含指向委託類別實例引用,用於真正執行分派轉發過來方法呼叫
InvocationHandler handler = new InvocationHandlerImpl(..);
// 透過 Proxy 為包含 Interface 介面在內一組介面動態建立代理類別類別物件
Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... });
// 透過反射從生成類別物件取得建構子物件
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class });
// 透過建構子物件建立動態代理類別實例
Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });

實際使用更簡單,因 Proxy 靜態方法 newProxyInstance 已封裝步驟 2-4:

InvocationHandler handler = new InvocationHandlerImpl(..);
// 透過 Proxy 直接建立動態代理類別實例
Interface proxy = (Interface)Proxy.newProxyInstance(classLoader,
    new Class[] { Interface.class }, handler);

動態生成代理類別特點

  1. 套件:若代理介面皆為 public,定義於頂層套件(套件路徑為空);若有非 public 介面(介面不可定義為 protected 或 private,除 public 外即預設套件存取層級),定義於該介面所在套件(如代理 com.ibm.developerworks 套件中非 public 介面 A,新代理類別套件即 com.ibm.developerworks)。設計目的:最大程度保證動態代理類別不會因套件管理問題無法成功定義存取。
  2. 類別修飾詞:具 final 與 public 修飾詞,可被所有類別存取但不能再被繼承。
  3. 類別名稱:格式 $ProxyN,N 為逐一遞增阿拉伯數字,代表 Proxy 第 N 次生成動態代理類別。值得注意的是,非每次呼叫 Proxy 靜態方法建立動態代理類別都會使 N 值增加,因若對同一組介面(含介面排列順序相同)嘗試重複建立,會聰明地回傳先前已建立代理類別類別物件,不再嘗試建立全新代理類別,節省不必要程式碼重複生成,提高建立效率。
  4. 類別繼承關係:Proxy 類別為父類別,此規則適用所有由 Proxy 建立動態代理類別。該類別還實作其代理一組介面,這是能安全型別轉換至代理某介面根本原因。 動態代理類別繼承圖

代理類別實例特點

每個實例關聯一個調用處理器物件,可透過 Proxy 提供靜態方法 getInvocationHandler 取得。在代理類別實例上呼叫代理介面宣告方法時,最終都由調用處理器 invoke 方法執行。值得注意的是,代理類別根類別 java.lang.Object 中三個方法也同樣分派至調用處理器 invoke 執行:hashCode、equals、toString。可能原因:一是這些方法為 public 非 final 型別,能被代理類別覆寫;二是這些方法常呈現類別某種特徵屬性,具一定區分度,為保證代理與委託類別對外一致性,這三方法也應分派至委託類別執行。當代理一組介面有重複宣告方法且被呼叫時,代理類別總從排最前面介面取得方法物件分派給調用處理器,無論代理類別實例是否正以該介面(或繼承該介面子介面)形式被外部引用,因代理類別內部無法區分當前被引用型別。

被代理介面特點

  • 不能有重複介面,避免動態代理類別程式碼生成編譯錯誤
  • 介面對類別載入器必須可見,否則載入器無法鏈接,導致類別定義失敗
  • 所有非 public 介面必須在同一套件,否則代理類別生成失敗
  • 介面數目不超過 65535(JVM 限制)

異常處理特點

從調用處理器介面宣告方法可見理論上能拋出任何型別異常(所有異常繼承 Throwable),但事實否。原因必須遵守繼承原則:子類別覆寫父類別或實作父介面方法時,拋出異常必須在原方法支援異常列表內。所以雖調用處理器理論上能,但實際常受限,除非父介面方法支援拋 Throwable 異常。若 invoke 方法確實產生介面方法宣告不支援異常如何?Java 動態代理類別已設計解決方法:拋出 UndeclaredThrowableException 異常。此異常為 RuntimeException 型別,不會引起編譯錯誤。透過該異常 getCause 方法可取得原不支援異常物件,便於錯誤診斷。

程式碼是最好的老師

機制與特點介紹完畢,透過原始碼了解 Proxy 如何實作。

Proxy 重要靜態變數

// 對應表:維護類別載入器物件至其對應代理類別快取
private static Map loaderToCache = new WeakHashMap();
// 標記:標記動態代理類別正被建立中
private static Object pendingGenerationMarker = new Object();
// 同步表:記錄已建立動態代理類別型別,主要供 isProxyClass 判斷
private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap());
// 關聯調用處理器引用
protected InvocationHandler h;

Proxy 建構方法

// 因 Proxy 內部從不直接呼叫建構函數,private 意味禁止任何呼叫
private Proxy() {}
// 因 Proxy 內部從不直接呼叫建構函數,protected 意味只有子類別可呼叫
protected Proxy(InvocationHandler h) { this.h = h; }

newProxyInstance 方法

public static Object newProxyInstance(ClassLoader loader,
    Class<?>[] interfaces, InvocationHandler h)
    throws IllegalArgumentException {
    // 檢查 h 不為空,否則拋異常
    if (h == null) throw new NullPointerException();
    // 取得指定類別載入器與一組介面相關代理類別型別物件
    Class cl = getProxyClass(loader, interfaces);
    // 透過反射取得建構子物件生成代理類別實例
    try {
        Constructor cons = cl.getConstructor(constructorParams);
        return (Object) cons.newInstance(new Object[] { h });
    } catch (NoSuchMethodException e) { throw new InternalError(e.toString()); }
    catch (IllegalAccessException e) { throw new InternalError(e.toString()); }
    catch (InstantiationException e) { throw new InternalError(e.toString()); }
    catch (InvocationTargetException e) { throw new InternalError(e.toString()); }
}

可見動態代理真正關鍵在 getProxyClass 方法,負責為一組介面動態生成代理類別型別物件。該方法內部 Proxy 靜態變數悉數登場。方法共四步驟:

  1. 對介面進行安全檢查:檢查介面類別物件是否對類別載入器可見且與載入器能識別介面類別物件完全相同,確保為 interface 型別非 class 型別。透過迴圈完成,檢查通過得到包含所有介面名稱字串陣列 String[] interfaceNames。僅保留判斷類別或介面是否對特定類別載入器可見相關程式碼。
    try {
        // 指定介面名字、類別載入器物件,同時指定 initializeBoolean 為 false 表示無須初始化類別
        // 方法回傳正常表示可見,否則拋出 ClassNotFoundException 表示不可見
        interfaceClass = Class.forName(interfaceName, false, loader);
    } catch (ClassNotFoundException e) { }
  2. 從 loaderToCache 取得以類別載入器物件為關鍵字對應快取表,不存在則建新快取表更新至 loaderToCache。快取表為 HashMap 實例,正常儲存鍵值對(介面名字列表、動態生成代理類別類別物件引用)。代理類別正被建立時臨時保存(介面名字列表、pendingGenerationMarker)。標記作用:通知後續同類請求(介面陣列相同且順序相同)代理類別正被建立,請等待直至完成。
    do {
        Object value = cache.get(key);
        if (value instanceof Reference) {
            proxyClass = (Class) ((Reference) value).get();
        }
        if (proxyClass != null) {
            return proxyClass;
        } else if (value == pendingGenerationMarker) {
            try { cache.wait(); } catch (InterruptedException e) {}
            continue;
        } else {
            cache.put(key, pendingGenerationMarker);
            break;
        }
    } while (true);
  3. 動態建立代理類別類別物件。首先確定代理類別套件,原則如前所述。確定套件後生成類別名,按 $ProxyN 格式生成。類別名確定,接著見證奇跡——動態生成代理類別:
    // 動態生成代理類別位元組碼陣列
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
    try {
        // 動態定義新生成代理類別
        proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        throw new IllegalArgumentException(e.toString());
    }
    // 將生成代理類別類別物件記錄進 proxyClasses 表
    proxyClasses.put(proxyClass, null);
    所有程式碼生成工作由神秘 ProxyGenerator 完成。嘗試探索此類別時,僅知位於未公開 sun.misc 套件,有若干常數、變數、方法完成神奇程式碼生成過程,但 Sun 未提供原始碼供研讀。動態類別定義由 Proxy native 靜態方法 defineClass0 執行。
  4. 程式碼生成進入尾聲,根據結果更新快取表:成功則更新代理類別類別物件引用,否則清除快取表對應鍵值,最後喚醒所有可能等待執行緒。

走完四步驟,所有代理類別生成細節介紹完畢。剩餘靜態方法 getInvocationHandler、isProxyClass 顯得直觀,只需查詢相關變數完成,省略程式碼分析。

代理類別實作推演

分析 Proxy 原始碼,讀者腦海中對 Java 動態代理機制形成更清晰理解。但探索之旅在 sun.misc.ProxyGenerator 處戛然而止,所有神秘匯聚於此。讓我們帶著疑惑開始探索之旅。

事物往往不像看起來複雜,需化繁為簡。拋開想像中未知複雜神秘因素,若用最簡單方法實作代理類別,唯一要求同樣結合調用處理器實施方法分派轉發,第一反應:「聽起來似乎並不複雜」。確實,涉及工作無非幾個反射呼叫、原始型別資料裝箱拆箱,其他似乎水到渠成。整理思緒完成完整推演。

代理類別方法呼叫分派轉發推演實作

// 假設需代理介面 Simulator
public interface Simulator {
    short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB;
}
// 假設代理類別為 SimulatorProxy
final public class SimulatorProxy implements Simulator {
    // 調用處理器物件引用
    protected InvocationHandler handler;
    // 以調用處理器為參數建構函數
    public SimulatorProxy(InvocationHandler handler) { this.handler = handler; }
    // 實作介面方法 simulate
    public short simulate(int arg1, long arg2, String arg3)
        throws ExceptionA, ExceptionB {
        // 第一步取得 simulate 方法 Method 物件
        java.lang.reflect.Method method = null;
        try {
            method = Simulator.class.getMethod("simulate",
                new Class[] { int.class, long.class, String.class });
        } catch (Exception e) { /* 異常處理 1 略 */ }
        // 第二步呼叫 handler invoke 方法分派轉發方法呼叫
        Object r = null;
        try {
            r = handler.invoke(this, method,
                new Object[] { new Integer(arg1), new Long(arg2), arg3 });
        } catch (Throwable e) { /* 異常處理 2 略 */ }
        // 第三步回傳結果(回傳型別為原始型別需拆箱)
        return ((Short)r).shortValue();
    }
}

模擬推演為突出通用邏輯關注正常流程,淡化錯誤處理,但實際中錯誤處理同樣重要。從推演得出通用結構化流程:第一步從代理介面取得被呼叫方法物件,第二步分派方法至調用處理器執行,第三步回傳結果。其中所有資訊可知:介面名、方法名、參數型別、回傳型別、所需裝箱拆箱操作。既然手工編寫如此,有何理由不相信 ProxyGenerator 不會做類似實作?至少這是種可行實作。

關注先前淡化錯誤處理。異常處理 1 因有理由確保所有資訊準確無誤,發生機率基本為零,可忽略。異常處理 2 需思考更多。回想介面方法可能宣告支援異常列表,調用處理器 invoke 可能拋出介面方法不支援異常。回想 Java 動態代理異常處理特點:對不支援異常必須拋 UndeclaredThrowableException 執行期異常。再次推演得出細化異常處理 2:

Object r = null;
try {
    r = handler.invoke(this, method,
        new Object[] { new Integer(arg1), new Long(arg2), arg3 });
} catch (ExceptionA e) {
    throw e;  // 介面方法支援 ExceptionA,可拋出
} catch (ExceptionB e) {
    throw e;  // 介面方法支援 ExceptionB,可拋出
} catch (Throwable e) {
    throw new UndeclaredThrowableException(e);  // 其他不支援異常,一律拋 UndeclaredThrowableException
}

完成動態代理類別推演實作。推演遵循相對固定模式,適用任意定義介面,程式碼生成所需資訊可知,有理由相信機器自動編寫程式碼也可能延續此風格,至少保證可行。

美中不足

Proxy 設計優美,但有小小遺憾:始終無法擺脫僅支援 interface 代理桎梏,因設計注定此遺憾。回想動態生成代理類別繼承關係圖,它們注定共同父類別叫 Proxy。Java 繼承機制注定這些動態代理類別無法實現對 class 動態代理,原因多重繼承在 Java 本質上行不通。

有許多理由否定對 class 代理必要性,但同樣有理由相信支援 class 動態代理會更美好。介面與類別劃分本不顯著,只到 Java 變如此細化。若只從方法宣告及是否被定義考量,有一種混合體叫抽象類別。實現對抽象類別動態代理,相信也有內在價值。此外,一些歷史遺留類別將因未實作任何介面而與動態代理永世無緣。種種不得不說是小小遺憾。但不完美不等於不偉大,偉大是本質,Java 動態代理就是佐證。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved