轉:Java 動態代理機制分析及擴充

來源:互聯網
上載者:User

引言

Java 動態代理機制的出現,使得 Java
開發人員不用手工編寫代理類,只要簡單地指定一組介面及委託類對象,便能動態地獲得代理類。代理類會負責將所有的方法調用指派到委派物件上反射執行,在分
派執行的過程中,開發人員還可以按需調整委託類對象及其功能,這是一套非常靈活有彈性的代理架構。通過閱讀本文,讀者將會對 Java
動態代理機制有更加深入的理解。本文首先從 Java 動態代理的運行機制和特點出發,對其代碼進行了分析,推演了動態產生類的內部實現。

回頁首

代理:設計模式

代理是一種常用的設計模式,其目的就是為其他對象提供一個代理以控制對某個對象的訪問。代理類負責為委託類預先處理訊息,過濾訊息並轉寄訊息,以及進行訊息被委託類執行後的後續處理。

圖 1. 代理模式

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

回頁首

相關的類和介面

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

  • java.lang.reflect.Proxy:這是 Java 動態代理機制的主類,它提供了一組靜態方法來為一組介面動態地組建代理程式類及其對象。

    清單 1. 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)
  • java.lang.reflect.InvocationHandler:這是調用處理器介面,它自訂了一個 invoke 方法,用於集中處理在動態代理類對象上的方法調用,通常在該方法中實現對委託類的代理訪問。

    清單 2. InvocationHandler 的核心方法


    // 該方法負責集中處理動態代理類上的所有方法調用。第一個參數既是代理類執行個體,第二個參數是被調用的方法對象
    // 第三個方法是調用參數。調用處理器根據這三個參數進行預先處理或指派到委託類執行個體上發射執行
    Object invoke(Object proxy, Method method, Object[] args)

    每次產生動態代理類對象時都需要指定一個實現了該介面的調用處理器對象(參見 Proxy 靜態方法 4 的第三個參數)。

  • java.lang.ClassLoader:這是類裝載器類,負責將類的位元組碼裝載到 Java
    虛擬機器(JVM)中並為其定義類對象,然後該類才能被使用。Proxy
    靜態方法產生動態代理類同樣需要通過類裝載器來進行裝載才能使用,它與普通類的唯一區別就是其位元組碼是由 JVM
    在運行時動態產生的而非預存在於任何一個 .class 檔案中。

    每次產生動態代理類對象時都需要指定一個類裝載器對象(參見 Proxy 靜態方法 4 的第一個參數)

回頁首

代理機制及其特點

首先讓我們來瞭解一下如何使用 Java 動態代理。具體有如下四步驟:

  1. 通過實現 InvocationHandler 介面建立自己的調用處理器;
  2. 通過為 Proxy 類指定 ClassLoader 對象和一組 interface 來建立動態代理類;
  3. 通過反射機制獲得動態代理類的建構函式,其唯一參數類型是調用處理器介面類型;
  4. 通過建構函式建立動態代理類執行個體,構造時調用處理器對象作為參數被傳入。

清單 3. 動態代理對象建立過程


// 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 的過程,所以簡化後的過程如下

清單 4. 簡化的動態代理對象建立過程


// InvocationHandlerImpl 實現了 InvocationHandler 介面,並能實現方法調用從代理類到委託類的指派轉寄
InvocationHandler handler = new InvocationHandlerImpl(..);

// 通過 Proxy 直接建立動態代理類執行個體
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,
new Class[] { Interface.class },
handler );

接下來讓我們來瞭解一下 Java 動態代理機制的一些特點。

首先是動態產生的代理類本身的一些特點。1)包:如果所代理的介面都是 public
的,那麼它將被定義在頂層包(即包路徑為空白),如果所代理的介面中有非 public 的介面(因為介面不能被定義為 protect 或
private,所以除 public 之外就是預設的 package 存取層級),那麼它將被定義在該介面所在包(假設代理了
com.ibm.developerworks 包中的某非 public 介面 A,那麼新產生的代理類所在的包就是
com.ibm.developerworks),這樣設計的目的是為了最大程度的保證動態代理類不會因為包管理的問題而無法被成功定義並訪問;2)類修
飾符:該代理類具有 final 和 public
修飾符,意味著它可以被所有的類訪問,但是不能被再度繼承;3)類名:格式是“$ProxyN”,其中 N 是一個逐一遞增的阿拉伯數字,代表
Proxy 類第 N 次產生的動態代理類,值得注意的一點是,並不是每次調用 Proxy 的靜態方法建立動態代理類都會使得 N
值增加,原因是如果對同一組介面(包括介面排列的順序相同)試圖重複建立動態代理類,它會很聰明地返回先前已經建立好的代理類的類對象,而不會再嘗試去創
建一個全新的代理類,這樣可以節省不必要的代碼重複產生,提高了代理類的建立效率。4)類繼承關係:該類的繼承關係

圖 2. 動態代理類的繼承圖

由圖可見,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 的幾個重要的靜態變數:

清單 5. 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 的構造方法:

清單 6. Proxy 構造方法


// 由於 Proxy 內部從不直接調用建構函式,所以 private 類型意味著禁止任何調用
private Proxy() {}

// 由於 Proxy 內部從不直接調用建構函式,所以 protected 意味著只有子類可以調用
protected Proxy(InvocationHandler h) {this.h = h;}

接著,可以快速探索一下 newProxyInstance 方法,因為其相當簡單:

清單 7. Proxy 靜態方法 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
內的各路英雄(靜態變數)悉數登場。有點迫不及待了嗎?那就讓我們一起走進 Proxy 最最神秘的殿堂去欣賞一番吧。該方法總共可以分為四個步驟:

  1. 對這組介面進行一定程度的安全檢查,包括檢查介面類對象是否對類裝載器可見並且與類裝載器所能識別的介面類對象是完全相同的,還會檢查確保是
    interface 類型而不是 class 類型。這個步驟通過一個迴圈來完成,檢查通過後將會得到一個包含所有介面名稱的字串數組,記為 String[] interfaceNames。總體上這部分實現比較直觀,所以略去大部分代碼,僅保留留如何判斷某類或介面是否對特定類裝載器可見的相關代碼。

    清單 8. 通過 Class.forName 方法判介面的可見度


    try {
    // 指定介面名字、類裝載器對象,同時制定 initializeBoolean 為 false 表示無須初始化類
    // 如果方法返回正常這表示可見,否則會拋出 ClassNotFoundException 異常表示不可見
    interfaceClass = Class.forName(interfaceName, false, loader);
    } catch (ClassNotFoundException e) {
    }
  2. 從 loaderToCache 映射表中擷取以類裝載器對象為關鍵字所對應的緩衝表,如果不存在就建立一個新的緩衝表並更新到
    loaderToCache。緩衝表是一個 HashMap
    執行個體,正常情況下它將存放索引值對(介面名字列表,動態產生的代理類的類對象引用)。當代理類正在被建立時它會臨時儲存(介面名字列
    表,pendingGenerationMarker)。標記 pendingGenerationMarke
    的作用是通知後續的同類請求(介面數組相同且組內介面排列順序也相同)代理類正在被建立,請保持等待直至建立完成。

    清單 9. 緩衝表的使用


    do {
    // 以介面名字列表作為關鍵字獲得對應 cache 值
    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 跳出迴圈已進入建立過程
    break;
    } while (true);
  3. 動態建立代理類的類對象。首先是確定代理類所在的包,其原則如前所述,如果都為 public
    介面,則包名為空白字串表示頂層包;如果所有非 public 介面都在同一個包,則包名與這些介面的包名相同;如果有多個非 public
    介面且不同包,則拋異常終止代理類的產生。確定了包後,就開始組建代理程式類的類名,同樣如前所述按格式“$ProxyN”產生。類名也確定了,接下來就是見
    證奇蹟的發生 —— 動態組建代理程式類:

    清單 10. 動態組建代理程式類


    // 動態地組建代理程式類的位元組碼數組
    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 類處嘎然而止,所有的神秘都匯聚於此時,相信不少讀者也會對這個 ProxyGenerator
類產生有類似的疑惑:它到底做了什麼呢?它是如何產生動態代理類的代碼的呢?誠然,這裡也無法給出確切的答案。還是讓我們帶著這些疑惑,一起開始探索之旅
吧。

事物往往不像其看起來的複雜,需要的是我們能夠化繁為簡,這樣也許就能有更多撥雲見日的機會。拋開所有想象中的未知而複雜的神秘因素,如果讓
我們用最簡單的方法去實現一個代理類,唯一的要求是同樣結合調用處理器實施方法的指派轉寄,您的第一反應將是什麼呢?“聽起來似乎並不是很複雜”。的確,
掐指算算所涉及的工作無非包括幾個反射調用,以及對原始類型資料的裝箱或拆箱過程,其他的似乎都已經水到渠成。非常地好,讓我們整理一下思緒,一起來完成
一次完整的推演過程吧。

清單 11. 代理類中方法調用的指派轉寄推演實現


// 假設需代理介面 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 的情況:

清單 12. 細化的異常處理 2


Object r = null;

try {
r = handler.invoke(this,
method,
new Object[] {new Integer(arg1), new Long(arg2), arg3});

} catch( ExceptionA e) {

// 介面方法支援 ExceptionA,可以拋出
throw e;

} catch( ExceptionB e ) {
// 介面方法支援 ExceptionB,可以拋出
throw e;

} catch(Throwable e) {
// 其他不支援的異常,一律拋 UndeclaredThrowableException
throw new UndeclaredThrowableException(e);
}

這樣我們就完成了對動態代理類的推演實現。推演實現遵循了一個相對固定的模式,可以適用於任意定義的任何介面,而且代碼產生所需的資訊都是可知的,那麼有理由相信即使是機器自動編寫的代碼也有可能延續這樣的風格,至少可以保證這是可行的。

回頁首

美中不足

誠然,Proxy 已經設計得非常優美,但是還是有一點點小小的遺憾之處,那就是它始終無法擺脫僅支援 interface
代理的桎梏,因為它的設計註定了這個遺憾。回想一下那些動態產生的代理類的繼承關係圖,它們已經註定有一個共同的父類叫 Proxy。Java
的繼承機制註定了這些動態代理類們無法實現對 class 的動態代理,原因是多繼承在 Java 中本質上就行不通。

有很多條理由,人們可以否定對 class 代理的必要性,但是同樣有一些理由,相信支援 class
動態代理會更美好。介面和類的劃分,本就不是很明顯,只是到了 Java
中才變得如此的細化。如果只從方法的聲明及是否被定義來考量,有一種兩者的混合體,它的名字叫抽象類別。實現對抽象類別的動態代理,相信也有其內在的價值。此
外,還有一些曆史遺留的類,它們將因為沒有實現任何介面而從此與動態代理永世無緣。如此種種,不得不說是一個小小的遺憾。

但是,不完美並不等於不偉大,偉大是一種本質,Java 動態代理就是佐例。

參考資料

  • “Dynamic Proxy Classes”:查看 Java 動態代理的相關文檔。

  • “Introduction to Java Exception Handling”:介紹了如何處理 Java 異常。
  • “Java 理論與實踐: 用動態代理進行修飾”(developerWorks,2005 年 9 月):動態代理工具 是 java.lang.reflect 包的一部分,在 JDK 1.3 版本中添加到 JDK,它允許程式建立 代理對象。本文中,作者介紹了幾個用於動態代理的應用程式。
  • “利用動態代理的 Java 驗證”(developerWorks,2004 年 9 月):本文向您展示動態代理如何讓核心應用程式代碼獨立於驗證常式,而只關注商務邏輯。
  • developerWorks Java 技術專區:數百篇關於 Java 編程各個方面的文章。

作者簡介

王忠平,軟體工程師,目前在 IBM 上海中國系統技術實驗室任職。

何平,軟體工程師,目前在 IBM 上海中國系統技術實驗室任職。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.