Java 动态代理机制分析及扩展--转
http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/#icomments
http://www.ibm.com/developerworks/cn/java/j-lo-proxy2/
?
引言
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 動態代理。具體有如下四步驟:
清單 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 最最神秘的殿堂去欣賞一番吧。該方法總共可以分為四個步驟:
清單 8. 通過 Class.forName 方法判接口的可見性
try { // 指定接口名字、類裝載器對象,同時制定 initializeBoolean 為 false 表示無須初始化類// 如果方法返回正常這表示可見,否則會拋出 ClassNotFoundException 異常表示不可見interfaceClass = Class.forName(interfaceName, false, loader); } catch (ClassNotFoundException e) { }清單 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);清單 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 執行。
走完了以上四個步驟后,至此,所有的代理類生成細節都已介紹完畢,剩下的靜態方法如 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 動態代理就是佐例。
?
本文希望將 Java 動態代理機制從接口擴展到類,使得類能夠享有與接口類似的動態代理支持。
設計及特點
新擴展的類名為 ProxyEx,將直接繼承于 java.lang.reflect.Proxy,也聲明了與原 Proxy 類中同名的 public 靜態方法,目的是保持與原代理機制在使用方法上的完全一致。
圖 1. ProxyEx 類繼承圖
與原代理機制最大的區別在于,動態生成的代理類將不再從 Proxy 類繼承,改而繼承需被代理的類。由于 Java 的單繼承原則,擴展代理機制所支持的類數目不得多于一個,但它可以聲明實現若干接口。包管理的機制與原來相似,不支持一個以上的類和接口同時為非 public;如果僅有一個非 public 的類或接口,假設其包為 PackageA,則動態生成的代理類將位于包 PackageA;否則將位于被代理的類所在的包。生成的代理類也被賦予 final 和 public 訪問屬性,且其命名規則類似地為“父類名 +ProxyN”(N 也是遞增的阿拉伯數字)。最后,在異常處理方面則與原來保持完全一致。
圖 2. 動態生成的代理類的繼承圖
回頁首
模板
通過對 Java 動態代理機制的推演,我們已經獲得了一個通用的方法模板。可以預期的是,通過模板來定制和引導代理類的代碼生成,是比較可行的方法。我們將主要使用兩個模板:類模板和方法模板。
清單 1. 類模板
package &Package; final public class &Name &Extends &Implements {private java.lang.reflect.InvocationHandler handler = null;&Constructors&Methods }類模板定制了代理類的代碼框架。其中帶“&”前綴的標簽位被用來引導相應的代碼替換。在此預留了包(&Package)、類名(&ClassName)、類繼承(&Extends)、接口實現(&Implements)、構造函數集(&Constructors)及方法集(&Methods)的標簽位。類模板還同時聲明了一個私有型的調用處理器對象作為類成員。
清單 2. 方法模板
&Modifiers &ReturnType &MethodName(&Parameters) &Throwables {java.lang.reflect.Method method = null;try {method = &Class.getMethod( \"& MethodName\", &ParameterTypes );}catch(Exception e){}Object r = null;try{r = handler.invoke( this, method, &ParameterValues );}&Exceptions&Return }方法模板定制了代理類方法集合中各個方法的代碼框架,同樣的帶“&”前綴的標簽位被用來引導相應的代碼替換。在此預留了修飾符(&Modifiers)、返回類型(&ReturnType)、方法名(&MethodName)、參數列表(Parameters)、異常列表(&Throwables)、方法的聲明類(&Class)、參數類型列表(&ParameterTypes)、調用處理器的參數值列表(&ParameterValues),異常處理(&Exceptions)及返回值(&Return)的標簽位。
回頁首
代碼生成
有了類模板和方法模板,代碼生成過程就變得有章可依。基本過程可分為三步:1)生成代理類的方法集合;2)生成代理類的構造函數;3)最后生成整個代理類。
生成代理類的方法集
第一步,通過反射獲得被代理類的所有 public 或 protected 且非 static 的 Method 對象列表,這些方法將被涵蓋的原因是它們是可以被其他類所訪問的。
第二步,遍歷 Method 對象列表,對每個 Method 對象,進行相應的代碼生成工作。
清單 3. 對標簽位進行代碼替換生成方法代碼
String declTemplate = "&Modifiers &ReturnType &MethodName(&Parameters) &Throwables"; String bodyTemplate = "&Declaration &Body"; // 方法聲明 String declare = declTemplate.replaceAll("&Modifiers", getMethodModifiers( method )).replaceAll("&ReturnType", getMethodReturnType( method )).replaceAll("&MethodName", method.getName()).replaceAll("&Parameters", getMethodParameters( method )).replaceAll("&Throwables", getMethodThrowables( method ));// 方法聲明以及實現 String body = bodyTemplate.replaceAll("&Declaration", declare ).replaceAll("&Body", getMethodEntity( method ));這里涉及了一些 ProxyEx 類的私有的輔助函數如 getMethodModifiers 和 getMethodReturnType 等等,它們都是通過反射獲取所需的信息,然后動態地生成各部分代碼。函數 getMethodEntity 是比較重要的輔助函數,它又調用了其他的輔助函數來生成代碼并替換標簽位。
清單 4. ProxyEx 的靜態方法 getMethodEntity()
private static String getMethodEntity( Method method ) {String template = "\n{"+ "\n java.lang.reflect.Method method = null;"+ "\n try{"+ "\n method = &Class.getMethod( \"&MethodName\", &ParameterTypes );"+ "\n }"+ "\n catch(Exception e){"+ "\n }"+ "\n Object r = null;"+ "\n try{" + "\n r = handler.invoke( this, method, &ParameterValues );"+ "\n }&Exceptions"+ "\n &Return"+ "\n}";String result = template.replaceAll("&MethodName", method.getName() ).replaceAll("&Class", method.getDeclaringClass().getName() + ".class").replaceAll("&ParameterTypes", getMethodParameterTypesHelper(method)).replaceAll("&ParameterValues", getMethodParameterValuesHelper(method) ).replaceAll("&Exceptions", getMethodParameterThrowablesHelper(method)).replaceAll("&Return", getMethodReturnHelper( method ) );return result; }當為 Class 類型對象生成該類型對應的字符代碼時,可能涉及數組類型,反推過程會需要按遞歸方法生成代碼,這部分工作由 getTypeHelper 方法提供
清單 5. ProxyEx 的靜態方法 getTypeHelper()
private static String getTypeHelper(Class type) {if( type.isArray() ){Class c = type.getComponentType();return getTypeHelper(c) + "[]";}else{return type.getName();} }第三步,將所生成的方法保存進一個 map 表,該表記錄的是鍵值對(方法聲明,方法實現)。由于類的多態性,父類的方法可能被子類所覆蓋,這時以上通過遍歷所得的方法列表中就會出現重復的方法對象,維護該表可以很自然地達到避免方法重復生成的目的,這就維護該表的原因所在。
生成代理類的構造函數
相信讀者依然清晰記得代理類是通過其構造函數反射生成的,而構造時傳入的唯一參數就是調用處理器對象。為了保持與原代理機制的一致性,新的代理類的構造函數也同樣只有一個調用處理器對象作為參數。模板簡單如下
清單 6. 構造函數模板
public &Constructor(java.lang.reflect.InvocationHandler handler) { super(&Parameters); this.handler = handler; }需要特別提一下的是 super 方法的參數值列表 &Parameters 的生成,我們借鑒了 Mock 思想,側重于追求對象構造的成功,而并未過多地努力分析并尋求最準確最有意義的賦值。對此,相信讀者會多少產生一些疑慮,但稍后我們會提及改進的方法,請先繼續閱讀。
生成整個代理類
通過以上步驟,構造函數和所有需被代理的方法的代碼已經生成,接下來就是生成整個代理類的時候了。這個過程也很直觀,通過獲取相關信息并對類模板中各個標簽位進行替換,便可以輕松的完成整個代理類的代碼生成。
回頁首
被遺忘的角落:類變量
等等,似乎遺忘了什么?從調用者的角度出發,我們希望代理類能夠作為被代理類的如實代表呈現在用戶面前,包括其內部狀態,而這些狀態通常是由類變量所體現出來的,于是就涉及到類變量的代理問題。
要解決這個問題,首先需要思考何時兩者的類變量可能出現不一致?回答了這個問題,也就找到了解決思路。回顧代理類的構造函數,我們以粗糙的方式構造了代理類實例。它們可能一開始就已經不一致了。還有每次方法調用也可能導致被兩者的類變量的不一致。如何解決?直觀的想法是:1)構造時需設法進行同步;2)方法調用之前和之后也需設法進行同步。這樣,我們就能夠有效避免代理類和被代理類的類變量不一致的問題的出現了。
但是,如何獲得被代理類的實例呢?從當前的的設計中已經沒有辦法做到。既然如此,那就繼續我們的擴展之旅。只不過這次擴展的對象是調用處理器接口,我們將在擴展后的接口里加入獲取被代理類對象的方法,且擴展調用處理器接口將以 static 和 public 的形式被定義在 ProxyEx 類中。
清單 7. ProxyEx 類內的靜態接口 InvocationHandlerEx
public static interface InvocationHandlerEx extends InvocationHandler { // 返回指定 stubClass 參數所對應的被代理類實體對象Object getStub(Class stubClass); }新的調用處理器接口具備了獲取被代理類對象的能力,從而為實現類變量的同步打開了通道。接下來還需要的就是執行類變量同步的 sync 方法,每個動態生成的代理類中都會被悄悄地加入這個私有方法以供調用。每次方法被分派轉發到調用處理器執行之前和之后,sync 方法都會被調用,從而保證類變量的雙向實時更新。相應的,方法模板也需要更新以支持該新特性。
清單 8. 更新后的方法模板(部分)
Object r = null; try{// 代理類到被代理類方向的變量同步sync(&Class, true);r = handler.invoke( this, method, &ParameterValues );// 被代理類到代理類方向的變量同步sync(&Class, false); }&Exceptions&Returnsync 方法還會在構造函數尾部被調用,從而將被代理類對象的變量信息同步到代理類對象,實現類似于拷貝構造的等價效果。相應的,構造函數模板也需要更新以支持該新特性。
清單 9. 更新后的構造函數模板
public &Name(java.lang.reflect.InvocationHandler handler) {super(&Parameters);this.handler = handler;// 被代理類到代理類方向的變量同步sync(null, false); }接下來介紹 sync 方法的實現,其思想就是首先獲取被代理類的所有 Field 對象的列表,并通過擴展的調用處理器獲得方法的聲明類說對應的 stub 對象,然后遍歷 Field 對象列表并對各個變量進行拷貝同步。
清單 10. 聲明在動態生成的代理類內部的 snyc 函數
private synchronized void sync(java.lang.Class clazz, boolean toStub) {// 判斷是否為擴展調用處理器if( handler instanceof InvocationHandlerEx ){java.lang.Class superClass = this.getClass().getSuperclass();java.lang.Class stubClass = ( clazz != null ? clazz : superClass );// 通過擴展調用處理器獲得stub對象Object stub = ((InvocationHandlerEx)handler).getStub(stubClass);if( stub != null ){// 獲得所有需同步的類成員列表,遍歷并同步java.lang.reflect.Field[] fields = getFields(superClass);for(int i=0; fields!=null&&i<fields.length; i++){try{fields[i].setAccessible(true);// 執行代理類和被代理類的變量同步if(toStub){fields[i].set(stub, fields[i].get(this));}else{fields[i].set(this, fields[i].get(stub));}}catch(Throwable e){}}}} }這里涉及到一個用于獲取類的所有 Field 對象列表的靜態輔助方法 getFields。為了提高頻繁查詢時的性能,配合該靜態方法的是一個靜態的 fieldsMap 對象,用于記錄已查詢過的類其所包含的 Field 對象列表,使得再次查詢時能迅速返回其對應列表。相應的,類模板也需進行更新。
清單 11. 增加了靜態 fieldsMap 變量后的類模板
package &Package; final public class &Name &Extends &Implements {private static java.util.HashMap fieldsMap = new java.util.HashMap();private java.lang.reflect.InvocationHandler handler = null;&Constructors&Methods }清單 12. 聲明在動態生成的代理類內部的靜態方法 getFields
private static java.lang.reflect.Field[] getFields(java.lang.Class c) {if( fieldsMap.containsKey(c) ){return (java.lang.reflect.Field[])fieldsMap.get(c);}java.lang.reflect.Field[] fields = null;if( c == java.lang.Object.class ){fields = c.getDeclaredFields();}else{java.lang.reflect.Field[] fields0 = getFields(c.getSuperclass());java.lang.reflect.Field[] fields1 = c.getDeclaredFields();fields = new java.lang.reflect.Field[fields0.length + fields1.length];System.arraycopy(fields0, 0, fields, 0, fields0.length);System.arraycopy(fields1, 0, fields, fields0.length, fields1.length);}fieldsMap.put(c, fields);return fields; }回頁首
動態編譯及裝載
代碼生成以后,需要經過編譯生成 JVM 所能識別的字節碼,而字節碼還需要通過類裝載器載入 JVM 才能最終被真正使用,接下來我們將闡述如何動態編譯及裝載。
首先是動態編譯。這部分由 ProxyEx 類的 getProxyClassCodeSource 函數完成。該函數分三步進行:第一步保存源代碼到 .java 文件;第二步編譯該 .java 文件;第三步從輸出的 .class 文件讀取字節碼。
清單 13. ProxyEx 的靜態方法 getProxyClassCodeSource
private static byte[] getProxyClassCodeSource( String pkg, String className, String declare ) throws Exception {// 將類的源代碼保存進一個名為類名加“.java”的本地文件File source = new File(className + ".java");FileOutputStream fos = new FileOutputStream( source );fos.write( declare.getBytes() );fos.close();// 調用com.sun.tools.javac.Main類的靜態方法compile進行動態編譯int status = com.sun.tools.javac.Main.compile( new String[] { "-d", ".", source.getName() } );if( status != 0 ){source.delete();throw new Exception("Compiler exit on " + status);}// 編譯得到的字節碼將被輸出到與包結構相同的一個本地目錄,文件名為類名加”.class”String output = ".";int curIndex = -1;int lastIndex = 0;while( (curIndex=pkg.indexOf('.', lastIndex)) != -1 ){output = output + File.separator + pkg.substring( lastIndex, curIndex );lastIndex = curIndex + 1;}output = output + File.separator + pkg.substring( lastIndex );output = output + File.separator + className + ".class";// 從輸出文件中讀取字節碼,并存入字節數組File target = new File(output);FileInputStream f = new FileInputStream( target );byte[] codeSource = new byte[(int)target.length()];f.read( codeSource );f.close();// 刪除臨時文件source.delete();target.delete();return codeSource; }得到代理類的字節碼,接下來就可以動態裝載該類了。這部分由 ProxyEx 類的 defineClassHelper 函數完成。該函數分兩步進行:第一步通過反射獲取父類 Proxy 的靜態私有方法 defineClass0;第二步傳入字節碼數組及其他相關信息并反射調用該方法以完成類的動態裝載。
清單 14. ProxyEx 的靜態方法 defineClassHelper
private static Class defineClassHelper( String pkg, String cName, byte[] codeSource ) throws Exception {Method defineClass = Proxy.class.getDeclaredMethod( "defineClass0", new Class[] { ClassLoader.class, String.class,byte[].class,int.class,int.class } );defineClass.setAccessible(true);return (Class)defineClass.invoke( Proxy.class, new Object[] { ProxyEx.class.getClassLoader(), pkg.length()==0 ? cName : pkg+"."+cName,codeSource,new Integer(0),new Integer(codeSource.length) } ); }回頁首
性能改進
原動態代理機制中對接口數組有一些有趣的特點,其中之一就是接口的順序差異會在一定程度上導致生成新的代理類,即使其實并無必要。其中的原因就是因為緩存表是以接口名稱列表作為關鍵字,所以不同的順序就意味著不同的關鍵字,如果對應的關鍵字不存在,就會生成新但是作用重復的代理類。在 ProxyEx 類中,我們通過主動排序避免了類似的問題,提高動態生成代理類的效率。而且,如果發現數組中都是接口類型,則直接調用父類 Proxy 的靜態方法 getProxyClass 生成代理類,否則才通過擴展動態代理機制生成代理類,這樣也一定程度上改進了性能。
回頁首
兼容性問題
接下來需要考慮的是與原代理機制的兼容性問題。曾記否,Proxy 中還有兩個靜態方法:isProxyClass 和 getInvocationHandler,分別被用于判斷 Class 對象是否是動態代理類和從 Object 對象獲取對應的調用處理器(如果可能的話)。
清單 15. Proxy 的靜態方法 isProxyClass 和 getInvocationHandler
static boolean isProxyClass(Class cl) static InvocationHandler getInvocationHandler(Object proxy)現在的兼容性問題,主要涉及到 ProxyEx 類與父類 Proxy 在關于動態生成的代理類的信息方面所面臨的如何保持同步的問題。曾介紹過,在 Proxy 類中有個私有的 Map 對象 proxyClasses 專門負責保存所有動態生成的代理類類型。Proxy 類的靜態函數 isProxyClass 就是通過查詢該表以確定某 Class 對象是否為動態代理類,我們需要做的就是把由 ProxyEx 生成的代理類類型也保存入該表。這部分工作由 ProxyEx 類的靜態方法 addProxyClass 輔助完成。
清單 16. ProxyEx 的靜態方法 addProxyClass
private static void addProxyClass( Class proxy ) throws IllegalArgumentException { try { // 通過反射獲取父類的私有 proxyClasses 變量并更新Field proxyClasses = Proxy.class.getDeclaredField("proxyClasses"); proxyClasses.setAccessible(true); ((Map)proxyClasses.get(Proxy.class)).put( proxy, null ); } catch(Exception e) { throw new IllegalArgumentException(e.toString()); } }相對而言,原來 Proxy 類的靜態方法 getInvocationHandler 實現相當簡單,先判斷是否為代理類,若是則直接類型轉換到 Proxy 并返回其調用處理器成員,而擴展后的代理類并不非從 Proxy 類繼承,所以在獲取調用處理器對象的方法上需要一些調整。這部分由 ProxyEx 類的同名靜態方法 getInvocationHandler 完成。
清單 17. ProxyEx 的靜態方法 getInvocationHandler
public static InvocationHandler getInvocationHandler(Object proxy) throws IllegalArgumentException {// 如果Proxy實例,直接調父類的方法if( proxy instanceof Proxy ) {return Proxy.getInvocationHandler( proxy );}// 如果不是代理類,拋異常if( !Proxy.isProxyClass( proxy.getClass() )){throw new IllegalArgumentException("Not a proxy instance");}try {// 通過反射獲取擴展代理類的調用處理器對象Field invoker = proxy.getClass().getDeclaredField("handler");invoker.setAccessible(true);return (InvocationHandler)invoker.get(proxy);}catch(Exception e){throw new IllegalArgumentException("Suspect not a proxy instance");} }回頁首
坦言:也有局限
受限于 Java 的類繼承機制,擴展的動態代理機制也有其局限,它不能支持:
回頁首
實例演示
闡述了這么多,相信讀者一定很想看一下擴展動態代理機制是如何工作的。本文最后將以 2010 世博門票售票代理為模型進行演示。
首先,我們定義了一個售票員抽象類 TicketSeller。
清單 18. TicketSeller
public abstract class TicketSeller {protected String theme;protected TicketSeller(String theme){this.theme = theme;}public String getTicketTheme(){return this.theme;}public void setTicketTheme(String theme){this.theme = theme;}public abstract int getTicketPrice();public abstract int buy(int ticketNumber, int money) throws Exception; }其次,我們會實現一個 2010 世博門票售票代理類 Expo2010TicketSeller。
清單 19. Expo2010TicketSeller
public class Expo2010TicketSeller extends TicketSeller {protected int price;protected int numTicketForSale;public Expo2010TicketSeller(){super("World Expo 2010");this.price = 180;this.numTicketForSale = 200;}public int getTicketPrice(){return price;}public int buy(int ticketNumber, int money) throws Exception{if( ticketNumber > numTicketForSale ){throw new Exception("There is no enough ticket available for sale, only " + numTicketForSale + " ticket(s) left");}int charge = money - ticketNumber * price;if( charge < 0 ){throw new Exception("Money is not enough. Still needs " + (-charge) + " RMB.");}numTicketForSale -= ticketNumber;return charge;} }接著,我們將通過購票者類 TicketBuyer 來模擬購票以演示擴展動態代理機制。
清單 20. TicketBuyer
public class TicketBuyer {public static void main(String[] args) {// 創建真正的TickerSeller對象,作為stub實體final TicketSeller stub = new Expo2010TicketSeller();// 創建擴展調用處理器對象InvocationHandler handler = new InvocationHandlerEx(){public Object getStub(Class stubClass) {// 僅對可接受的Class類型返回stub實體if( stubClass.isAssignableFrom(stub.getClass()) ){return stub;}return null;}public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Object o;try{System.out.println(" >>> Enter method: " + method.getName() );o = method.invoke(stub, args);}catch(InvocationTargetException e){throw e.getCause();}finally{System.out.println(" <<< Exit method: " + method.getName() );}return o;}};// 通過ProxyEx構造動態代理TicketSeller seller = (TicketSeller)ProxyEx.newProxyInstance(TicketBuyer.class.getClassLoader(), new Class[] {TicketSeller.class}, handler);// 顯示代理類的類型System.out.println("Ticket Seller Class: " + seller.getClass() + "\n");// 直接訪問theme變量,驗證代理類變量在對象構造時同步的有效性System.out.println("Ticket Theme: " + seller.theme + "\n");// 函數訪問price信息System.out.println("Query Ticket Price...");System.out.println("Ticket Price: " + seller.getTicketPrice() + " RMB\n");// 模擬票務交易buyTicket(seller, 1, 200);buyTicket(seller, 1, 160);buyTicket(seller, 250, 30000);// 直接更新theme變量System.out.println("Updating Ticket Theme...\n");seller.theme = "World Expo 2010 in Shanghai";// 函數訪問theme信息,驗證擴展動態代理機制對變量同步的有效性System.out.println("Query Updated Ticket Theme...");System.out.println("Updated Ticket Theme: " + seller.getTicketTheme() + "\n"); }// 購票函數protected static void buyTicket(TicketSeller seller, int ticketNumber, int money){try {System.out.println("Transaction: Order " + ticketNumber + " ticket(s) with " + money + " RMB");int charge = seller.buy(ticketNumber, money);System.out.println("Transaction: Succeed - Charge is " + charge + " RMB\n");} catch (Exception e) {System.out.println("Transaction: Fail - " + e.getMessage() + "\n");} } }最后,見演示程序的執行結果。
清單 21. 執行輸出
Ticket Seller Class: class com.demo.proxy.test.TicketSellerProxy0Ticket Theme: World Expo 2010Query Ticket Price...>>> Enter method: getTicketPrice<<< Exit method: getTicketPrice Ticket Price: 180 RMBTransaction: Order 1 ticket(s) with 200 RMB>>> Enter method: buy<<< Exit method: buy Transaction: Succeed - Charge is 20 RMBTransaction: Order 1 ticket(s) with 160 RMB>>> Enter method: buy<<< Exit method: buy Transaction: Fail - Money is not enough. Still needs 20 RMB.Transaction: Order 250 ticket(s) with 30000 RMB>>> Enter method: buy<<< Exit method: buy Transaction: Fail - There is no enough ticket available for sale, only 199 ticket(s) leftUpdating Ticket Theme...Query Updated Ticket Theme...>>> Enter method: getTicketTheme<<< Exit method: getTicketTheme Updated Ticket Theme: World Expo 2010 in Shanghai?
轉載于:https://www.cnblogs.com/davidwang456/p/3474867.html
總結
以上是生活随笔為你收集整理的Java 动态代理机制分析及扩展--转的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java实现插入排序算法 附单元测试源码
- 下一篇: java美元兑换,(Java实现) 美元