Java 理論與實(shí)踐: 用動(dòng)態(tài)代理進(jìn)行修飾
動(dòng)態(tài)代理工具 是 java.lang.reflect
包的一部分,在 JDK 1.3 版本中添加到 JDK,它允許程序創(chuàng)建 代理對(duì)象,代理對(duì)象能實(shí)現(xiàn)一個(gè)或多個(gè)已知接口,并用反射代替內(nèi)置的虛方法分派,編程地分派對(duì)接口方法的調(diào)用。這個(gè)過(guò)程允許實(shí)現(xiàn)“截取”方法調(diào)用,重新路由它們或者動(dòng)態(tài)地添加功能。本期文章中,Brian Goetz 介紹了幾個(gè)用于動(dòng)態(tài)代理的應(yīng)用程序。請(qǐng)?jiān)诒疚陌殡S的 討論論壇 上與作者和其他讀者分享您對(duì)這篇文章的想法。(也可以單擊文章頂部或底部的 討論 訪問(wèn)討論論壇。)
動(dòng)態(tài)代理為實(shí)現(xiàn)許多常見(jiàn)設(shè)計(jì)模式(包括 Facade、Bridge、Interceptor、Decorator、Proxy(包括遠(yuǎn)程和虛擬代理)和 Adapter 模式)提供了替代的動(dòng)態(tài)機(jī)制。雖然這些模式不使用動(dòng)態(tài)代理,只用普通的類就能夠?qū)崿F(xiàn),但是在許多情況下,動(dòng)態(tài)代理方式更方便、更緊湊,可以清除許多手寫或生成的類。
Proxy 模式中要?jiǎng)?chuàng)建“stub”或“surrogate”對(duì)象,它們的目的是接受請(qǐng)求并把請(qǐng)求轉(zhuǎn)發(fā)到實(shí)際執(zhí)行工作的其他對(duì)象。遠(yuǎn)程方法調(diào)用(RMI)利用 Proxy 模式,使得在其他 JVM 中執(zhí)行的對(duì)象就像本地對(duì)象一樣;企業(yè) JavaBeans (EJB)利用 Proxy 模式添加遠(yuǎn)程調(diào)用、安全性和事務(wù)分界;而 JAX-RPC Web 服務(wù)則用 Proxy 模式讓遠(yuǎn)程服務(wù)表現(xiàn)得像本地對(duì)象一樣。在每一種情況中,潛在的遠(yuǎn)程對(duì)象的行為是由接口定義的,而接口本質(zhì)上接受多種實(shí)現(xiàn)。調(diào)用者(在大多數(shù)情況下)不能區(qū)分出它們只是持有一個(gè)對(duì) stub 而不是實(shí)際對(duì)象的引用,因?yàn)槎邔?shí)現(xiàn)了相同的接口;stub 的工作是查找實(shí)際的對(duì)象、封送參數(shù)、把參數(shù)發(fā)送給實(shí)際對(duì)象、解除封送返回值、把返回值返回給調(diào)用者。代理可以用來(lái)提供遠(yuǎn)程控制(就像在 RMI、EJB 和 JAX-RPC 中那樣),用安全性策略包裝對(duì)象(EJB)、為昂貴的對(duì)象(EJB 實(shí)體 Bean)提供惰性裝入,或者添加檢測(cè)工具(例如日志記錄)。
在 5.0 以前的 JDK 中,RMI stub(以及它對(duì)等的 skeleton)是在編譯時(shí)由 RMI 編譯器(rmic)生成的類,RMI 編譯器是 JDK 工具集的一部分。對(duì)于每個(gè)遠(yuǎn)程接口,都會(huì)生成一個(gè) stub(代理)類,它代表遠(yuǎn)程對(duì)象,還生成一個(gè) skeleton 對(duì)象,它在遠(yuǎn)程 JVM 中做與 stub 相反的工作 —— 解除封送參數(shù)并調(diào)用實(shí)際的對(duì)象。類似地,用于 Web 服務(wù)的 JAX-RPC 工具也為遠(yuǎn)程 Web 服務(wù)生成代理類,從而使遠(yuǎn)程 Web 服務(wù)看起來(lái)就像本地對(duì)象一樣。
不管 stub 類是以源代碼還是以字節(jié)碼生成的,代碼生成仍然會(huì)向編譯過(guò)程添加一些額外步驟,而且因?yàn)槊嗨频念惖姆簽E,會(huì)帶來(lái)意義模糊的可能性。另一方面,動(dòng)態(tài)代理機(jī)制支持在編譯時(shí)沒(méi)有生成 stub 類的情況下,在運(yùn)行時(shí)創(chuàng)建代理對(duì)象。在 JDK 5.0 及以后版本中,RMI 工具使用動(dòng)態(tài)代理代替了生成的 stub,結(jié)果 RMI 變得更容易使用。許多 J2EE 容器也使用動(dòng)態(tài)代理來(lái)實(shí)現(xiàn) EJB。EJB 技術(shù)嚴(yán)重地依靠使用攔截(interception)來(lái)實(shí)現(xiàn)安全性和事務(wù)分界;動(dòng)態(tài)代理為接口上調(diào)用的所有方法提供了集中的控制流程路徑。
![]() ![]() |
![]()
|
動(dòng)態(tài)代理機(jī)制的核心是 InvocationHandler
接口,如清單 1 所示。調(diào)用句柄的工作是代表動(dòng)態(tài)代理實(shí)際執(zhí)行所請(qǐng)求的方法調(diào)用。傳遞給調(diào)用句柄一個(gè) Method
對(duì)象(從 java.lang.reflect
包),參數(shù)列表則傳遞給方法;在最簡(jiǎn)單的情況下,可能僅僅是調(diào)用反射性的方法 Method.invoke()
并返回結(jié)果。
清單 1. InvocationHandler 接口
|
每個(gè)代理都有一個(gè)與之關(guān)聯(lián)的調(diào)用句柄,只要代理的方法被調(diào)用時(shí)就會(huì)調(diào)用該句柄。根據(jù)通用的設(shè)計(jì)原則:接口定義類型、類定義實(shí)現(xiàn),代理對(duì)象可以實(shí)現(xiàn)一個(gè)或多個(gè)接口,但是不能實(shí)現(xiàn)類。因?yàn)榇眍悰](méi)有可以訪問(wèn)的名稱,它們不能有構(gòu)造函數(shù),所以它們必須由工廠創(chuàng)建。清單 2 顯示了動(dòng)態(tài)代理的最簡(jiǎn)單的可能實(shí)現(xiàn),它實(shí)現(xiàn) Set
接口并把所有 Set
方法(以及所有 Object 方法)分派給封裝的 Set
實(shí)例。
清單 2. 包裝 Set 的簡(jiǎn)單的動(dòng)態(tài)代理
|
SetProxyFactory
類包含一個(gè)靜態(tài)工廠方法 getSetProxy()
,它返回一個(gè)實(shí)現(xiàn)了 Set
的動(dòng)態(tài)代理。代理對(duì)象實(shí)際實(shí)現(xiàn) Set
—— 調(diào)用者無(wú)法區(qū)分(除非通過(guò)反射)返回的對(duì)象是動(dòng)態(tài)代理。SetProxyFactory
返回的代理只做一件事,把方法分派給傳遞給工廠方法的 Set
實(shí)例。雖然反射代碼通常比較難讀,但是這里的內(nèi)容很少,跟上控制流程并不難 —— 只要某個(gè)方法在 Set
代理上被調(diào)用,它就被分派給調(diào)用句柄,調(diào)用句柄只是反射地調(diào)用底層包裝的對(duì)象上的目標(biāo)方法。當(dāng)然,絕對(duì)什么都不做的代理可能有點(diǎn)傻,是不是呢?
對(duì)于像 SetProxyFactory
這樣什么都不做的包裝器來(lái)說(shuō),實(shí)際有個(gè)很好的應(yīng)用 —— 可以用它安全地把對(duì)象引用的范圍縮小到特定接口(或接口集)上,方式是,調(diào)用者不能提升引用的類型,使得可以更安全地把對(duì)象引用傳遞給不受信任的代碼(例如插件或回調(diào))。清單 3 包含一組類定義,實(shí)現(xiàn)了典型的回調(diào)場(chǎng)景。從中會(huì)看到動(dòng)態(tài)代理可以更方便地替代通常用手工(或用 IDE 提供的代碼生成向?qū)В?shí)現(xiàn)的 Adapter 模式。
清單 3. 典型的回調(diào)場(chǎng)景
|
ServiceConsumer
類實(shí)現(xiàn)了 ServiceCallback
(這通常是支持回調(diào)的一個(gè)方便途徑)并把 this
引用傳遞給 serviceMethod()
作為回調(diào)引用。這種方法的問(wèn)題是沒(méi)有機(jī)制可以阻止 Service
實(shí)現(xiàn)把 ServiceCallback
提升為 ServiceConsumer
,并調(diào)用 ServiceConsumer
不希望 Service
調(diào)用的方法。有時(shí)對(duì)這個(gè)風(fēng)險(xiǎn)并不關(guān)心 —— 但有時(shí)卻關(guān)心。如果關(guān)心,那么可以把回調(diào)對(duì)象作為內(nèi)部類,或者編寫一個(gè)什么都不做的適配器類(請(qǐng)參閱清單 4 中的 ServiceCallbackAdapter
)并用 ServiceCallbackAdapter
包裝 ServiceConsumer
。ServiceCallbackAdapter
防止 Service
把 ServiceCallback
提升為 ServiceConsumer
。
清單 4. 用于安全地把對(duì)象限制在一個(gè)接口上以便不被惡意代碼不能的適配器類
|
編寫 ServiceCallbackAdapter
這樣的適配器類簡(jiǎn)單卻乏味。必須為包裝的接口中的每個(gè)方法編寫重定向類。在 ServiceCallback
的示例中,只有一個(gè)需要實(shí)現(xiàn)的方法,但是某些接口,例如 Collections 或 JDBC 接口,則包含許多方法。現(xiàn)代的 IDE 提供了“Delegate Methods”向?qū)В档土司帉戇m配器類的工作量,但是仍然必須為每個(gè)想要包裝的接口編寫一個(gè)適配器類,而且對(duì)于只包含生成的代碼的類,也有一些讓人不滿意的地方。看起來(lái)應(yīng)當(dāng)有一種方式可以更緊湊地表示“什么也不做的限制適配器模式”。
清單 2
中的 SetProxyFactory
類當(dāng)然比用于 Set
的等價(jià)的適配器類更緊湊,但是它仍然只適用于一個(gè)接口:Set
。但是通過(guò)使用泛型,可以容易地創(chuàng)建通用的代理工廠,由它為任何接口做同樣的工作,如清單 5 所示。它幾乎與 SetProxyFactory
相同,但是可以適用于任何接口。現(xiàn)在再也不用編寫限制適配器類了!如果想創(chuàng)建代理對(duì)象安全地把對(duì)象限制在接口 T
,只要調(diào)用 getProxy(T.class,object)
就可以了,不需要一堆適配器類的額外累贅。
清單 5. 通用的限制適配器工廠類
|
![]() ![]() |
![]()
|
當(dāng)然,動(dòng)態(tài)代理工具能做的,遠(yuǎn)不僅僅是把對(duì)象類型限制在特定接口上。從 清單 2 和 清單 5 中簡(jiǎn)單的限制適配器到 Decorator 模式,是一個(gè)小的飛躍,在 Decorator 模式中,代理用額外的功能(例如安全檢測(cè)或日志記錄)包裝調(diào)用。清單 6 顯示了一個(gè)日志 InvocationHandler
,它在調(diào)用目標(biāo)對(duì)象上的方法之外,還寫入一條日志信息,顯示被調(diào)用的方法、傳遞的參數(shù),以及返回值。除了反射性的 invoke()
調(diào)用之外,這里的全部代碼只是生成調(diào)試信息的一部分 —— 還不是太多。代理工廠方法的代碼幾乎與 GenericProxyFactory
相同,區(qū)別在于它使用的是 LoggingInvocationHandler
而不是匿名的調(diào)用句柄。
清單 6. 基于代理的 Decorator,為每個(gè)方法調(diào)用生成調(diào)試日志
|
如果用日志代理包裝 HashSet
,并執(zhí)行下面這個(gè)簡(jiǎn)單的測(cè)試程序:
|
會(huì)得到以下輸出:
|
這種方式是給對(duì)象添加調(diào)試包裝器的一種好的而且容易的方式。它當(dāng)然比生成代理類并手工創(chuàng)建大量 println()
語(yǔ)句容易得多(也更通用)。我進(jìn)一步改進(jìn)了這一方法;不必?zé)o條件地生成調(diào)試輸出,相反,代理可以查詢動(dòng)態(tài)配置存儲(chǔ)(從配置文件初始化,可以由 JMX MBean
動(dòng)態(tài)修改),確定是否需要生成調(diào)試語(yǔ)句,甚至可能在逐個(gè)類或逐個(gè)實(shí)例的基礎(chǔ)上進(jìn)行。
在這一點(diǎn)上,我認(rèn)為讀者中的 AOP 愛(ài)好者們幾乎要跳出來(lái)說(shuō)“這正是 AOP 擅長(zhǎng)的啊!”是的,但是解決問(wèn)題的方法不止一種 —— 僅僅因?yàn)槟稠?xiàng)技術(shù)能解決某個(gè)問(wèn)題,并不意味著它就是最好的解決方案。在任何情況下,動(dòng)態(tài)代理方式都有完全在“純 Java”范圍內(nèi)工作的優(yōu)勢(shì),不是每個(gè)公司都用(或應(yīng)當(dāng)用) AOP 的。
代理也可以用作真正的適配器,提供了對(duì)象的一個(gè)視圖,導(dǎo)出與底層對(duì)象實(shí)現(xiàn)的接口不同的接口。調(diào)用句柄不需要把每個(gè)方法調(diào)用都分派給相同的底層對(duì)象;它可以檢查名稱,并把不同的方法分派給不同的對(duì)象。例如,假設(shè)有一組表示持久實(shí)體(Person
、Company
和 PurchaseOrder
) 的 JavaBean 接口,指定了屬性的 getter 和 setter,而且正在編寫一個(gè)持久層,把數(shù)據(jù)庫(kù)記錄映射到實(shí)現(xiàn)這些接口的對(duì)象上。現(xiàn)在不用為每個(gè)接口編寫或生成類,可以只用一個(gè) JavaBean 風(fēng)格的通用代理類,把屬性保存在 Map 中。
清單 7 顯示的動(dòng)態(tài)代理檢查被調(diào)用方法的名稱,并通過(guò)查詢或修改屬性圖直接實(shí)現(xiàn) getter 和 setter 方法。現(xiàn)在,這一個(gè)代理類就能實(shí)現(xiàn)多個(gè) JavaBean 風(fēng)格接口的對(duì)象。
清單 7. 用于把 getter 和 setter 分派給 Map 的動(dòng)態(tài)代理類
|
雖然因?yàn)榉瓷湓?Object
上工作會(huì)有潛在的類型安全性上的損失,但是,JavaBeanProxyFactory 中的 getter 處理會(huì)進(jìn)行一些必要的額外的類型檢測(cè),就像我在這里用 isInstance()
對(duì) getter 進(jìn)行的檢測(cè)一樣。
![]() ![]() |
![]()
|
正如已經(jīng)看到的,動(dòng)態(tài)代理?yè)碛泻?jiǎn)化大量代碼的潛力 —— 不僅能替代許多生成的代碼,而且一個(gè)代理類還能代替多個(gè)手寫的類或生成的代碼。什么是成本呢? 因?yàn)榉瓷涞胤峙煞椒ǘ皇遣捎脙?nèi)置的虛方法分派,可能有一些性能上的成本。在早期的 JDK 中,反射的性能很差(就像早期 JDK 中幾乎其他每件事的性能一樣),但是在近 10 年,反射已經(jīng)變得快多了。
不必進(jìn)入基準(zhǔn)測(cè)試構(gòu)造的主題,我編寫了一個(gè)簡(jiǎn)單的、不太科學(xué)的測(cè)試程序,它循環(huán)地把數(shù)據(jù)填充到 Set
,隨機(jī)地對(duì) Set
進(jìn)行插入、查詢和刪除元素。我用三個(gè) Set
實(shí)現(xiàn)運(yùn)行它:一個(gè)未經(jīng)修飾的 HashSet
,一個(gè)手寫的、只是把所有方法轉(zhuǎn)發(fā)到底層的 HashSet
的 Set
適配器,還有一個(gè)基于代理的、也只是把所有方法轉(zhuǎn)發(fā)到底層 HashSet
的 Set
適配器。每次循環(huán)迭代都生成若干隨機(jī)數(shù),并執(zhí)行一個(gè)或多個(gè) Set
操作。手寫的適配器比起原始的 HashSet
只產(chǎn)生很少百分比的性能負(fù)荷(大概是因?yàn)?JVM 級(jí)有效的內(nèi)聯(lián)緩沖和硬件級(jí)的分支預(yù)測(cè));代理適配器則明顯比原始 HashSet
慢,但是開(kāi)銷要少于兩個(gè)量級(jí)。
我從這個(gè)試驗(yàn)得出的結(jié)論是:對(duì)于大多數(shù)情況,代理方式即使對(duì)輕量級(jí)方法也執(zhí)行得足夠好,而隨著被代理的操作變得越來(lái)越重量級(jí)(例如遠(yuǎn)程方法調(diào)用,或者使用序列化、執(zhí)行 IO 或者從數(shù)據(jù)庫(kù)檢索數(shù)據(jù)的方法),代理開(kāi)銷就會(huì)有效地接近于 0。當(dāng)然也存在一些代理方式的性能開(kāi)銷無(wú)法接受的情況,但是這些通常只是少數(shù)情況。
posted on 2006-08-24 17:35 Binary 閱讀(214) 評(píng)論(0) 編輯 收藏 所屬分類: j2se