熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Java 實踐: 用動態代理進行修飾

2022-06-13   來源: Java核心技術 

  動態代理工具 是 javalangreflect 包的一部分在 JDK 版本中添加到 JDK它允許程序創建 代理對象代理對象能實現一個或多個已知接口並用反射代替內置的虛方法分派編程地分派對接口方法的調用這個過程允許實現截取方法調用重新路由它們或者動態地添加功能

  動態代理為實現許多常見設計模式(包括 FacadeBridgeInterceptorDecoratorProxy(包括遠程和虛擬代理)和 Adapter 模式)提供了替代的動態機制雖然這些模式不使用動態代理只用普通的類就能夠實現但是在許多情況下動態代理方式更方便更緊湊可以清除許多手寫或生成的類

  Proxy模式

  Proxy 模式中要創建stubsurrogate對象它們的目的是接受請求並把請求轉發到實際執行工作的其他對象遠程方法調用(RMI)利用 Proxy 模式使得在其他 JVM 中執行的對象就像本地對象一樣企業 JavaBeans (EJB)利用 Proxy 模式添加遠程調用安全性和事務分界而 JAXRPC Web 服務則用 Proxy 模式讓遠程服務表現得像本地對象一樣在每一種情況中潛在的遠程對象的行為是由接口定義的而接口本質上接受多種實現調用者(在大多數情況下)不能區分出它們只是持有一個對 stub 而不是實際對象的引用因為二者實現了相同的接口stub 的工作是查找實際的對象封送參數把參數發送給實際對象解除封送返回值把返回值返回給調用者代理可以用來提供遠程控制(就像在 RMIEJB 和 JAXRPC 中那樣)用安全性策略包裝對象(EJB)為昂貴的對象(EJB 實體 Bean)提供惰性裝入或者添加檢測工具(例如日志記錄)

  在 以前的 JDK 中RMI stub(以及它對等的 skeleton)是在編譯時由 RMI 編譯器(rmic)生成的類RMI 編譯器是 JDK 工具集的一部分對於每個遠程接口都會生成一個 stub(代理)類它代表遠程對象還生成一個 skeleton 對象它在遠程 JVM 中做與 stub 相反的工作 —— 解除封送參數並調用實際的對象類似地用於 Web 服務的 JAXRPC 工具也為遠程 Web 服務生成代理類從而使遠程 Web 服務看起來就像本地對象一樣

  不管 stub 類是以源代碼還是以字節碼生成的代碼生成仍然會向編譯過程添加一些額外步驟而且因為命名相似的類的泛濫會帶來意義模糊的可能性另一方面動態代理機制支持在編譯時沒有生成 stub 類的情況下在運行時創建代理對象在 JDK 及以後版本中RMI 工具使用動態代理代替了生成的 stub結果 RMI 變得更容易使用許多 JEE 容器也使用動態代理來實現 EJBEJB 技術嚴重地依靠使用攔截(interception)來實現安全性和事務分界動態代理為接口上調用的所有方法提供了集中的控制流程路徑

  動態代理機制

  動態代理機制的核心是 InvocationHandler 接口如清單 所示調用句柄的工作是代表動態代理實際執行所請求的方法調用傳遞給調用句柄一個 Method 對象(從 javalangreflect 包)參數列表則傳遞給方法在最簡單的情況下可能僅僅是調用反射性的方法 Methodinvoke() 並返回結果

  清單 InvocationHandler 接口

  public interface InvocationHandler { Object invoke(Object proxy Method method Object[] args) throws Throwable; }

  每個代理都有一個與之關聯的調用句柄只要代理的方法被調用時就會調用該句柄根據通用的設計原則接口定義類型類定義實現代理對象可以實現一個或多個接口但是不能實現類因為代理類沒有可以訪問的名稱它們不能有構造函數所以它們必須由工廠創建清單 顯示了動態代理的最簡單的可能實現它實現 Set 接口並把所有 Set 方法(以及所有 Object 方法)分派給封裝的 Set 實例

  清單 包裝 Set 的簡單的動態代理

  public class SetProxyFactory { public static Set getSetProxy(final Set s) { return (Set) ProxynewProxyInstance (sgetClass()getClassLoader() new Class[] { Setclass } new InvocationHandler() { public Object invoke(Object proxy Method method Object[] args) throws Throwable { return methodinvoke(s args); } }); } }

  SetProxyFactory 類包含一個靜態工廠方法 getSetProxy()它返回一個實現了 Set 的動態代理代理對象實際實現 Set —— 調用者無法區分(除非通過反射)返回的對象是動態代理SetProxyFactory 返回的代理只做一件事把方法分派給傳遞給工廠方法的 Set 實例雖然反射代碼通常比較難讀但是這裡的內容很少跟上控制流程並不難 —— 只要某個方法在 Set 代理上被調用它就被分派給調用句柄調用句柄只是反射地調用底層包裝的對象上的目標方法當然絕對什麼都不做的代理可能有點傻是不是呢?

  什麼都不做的適配器

  對於像 SetProxyFactory 這樣什麼都不做的包裝器來說實際有個很好的應用 —— 可以用它安全地把對象引用的范圍縮小到特定接口(或接口集)上方式是調用者不能提升引用的類型使得可以更安全地把對象引用傳遞給不受信任的代碼(例如插件或回調)清單 包含一組類定義實現了典型的回調場景從中會看到動態代理可以更方便地替代通常用手工(或用 IDE 提供的代碼生成向導)實現的 Adapter 模式

  清單 典型的回調場景public interface ServiceCallback { public void doCallback(); } public interface Service { public void serviceMethod(ServiceCallback callback); } public class ServiceConsumer implements ServiceCallback { private Service service; public void someMethod() { serviceserviceMethod(this); } }

  ServiceConsumer 類實現了 ServiceCallback(這通常是支持回調的一個方便途徑)並把 this 引用傳遞給 serviceMethod() 作為回調引用這種方法的問題是沒有機制可以阻止 Service 實現把 ServiceCallback 提升為 ServiceConsumer並調用 ServiceConsumer 不希望 Service 調用的方法有時對這個風險並不關心 —— 但有時卻關心如果關心那麼可以把回調對象作為內部類或者編寫一個什麼都不做的適配器類(請參閱清單 中的 ServiceCallbackAdapter)並用 ServiceCallbackAdapter 包裝 ServiceConsumerServiceCallbackAdapter 防止 Service 把 ServiceCallback 提升為 ServiceConsumer

  清單 用於安全地把對象限制在一個接口上以便不被惡意代碼不能的適配器類public class ServiceCallbackAdapter implements ServiceCallback { private final ServiceCallback cb; public ServiceCallbackAdapter(ServiceCallback cb) { thiscb = cb; } public void doCallback() { cbdoCallback(); } }

  編寫 ServiceCallbackAdapter 這樣的適配器類簡單卻乏味必須為包裝的接口中的每個方法編寫重定向類在 ServiceCallback 的示例中只有一個需要實現的方法但是某些接口例如 Collections 或 JDBC 接口則包含許多方法現代的 IDE 提供了Delegate Methods向導降低了編寫適配器類的工作量但是仍然必須為每個想要包裝的接口編寫一個適配器類而且對於只包含生成的代碼的類也有一些讓人不滿意的地方看起來應當有一種方式可以更緊湊地表示什麼也不做的限制適配器模式

  通用適配器類

  清單 中的 SetProxyFactory 類當然比用於 Set 的等價的適配器類更緊湊但是它仍然只適用於一個接口Set但是通過使用泛型可以容易地創建通用的代理工廠由它為任何接口做同樣的工作如清單 所示它幾乎與 SetProxyFactory 相同但是可以適用於任何接口現在再也不用編寫限制適配器類了!如果想創建代理對象安全地把對象限制在接口 T只要調用 getProxy(Tclassobject) 就可以了不需要一堆適配器類的額外累贅

  清單 通用的限制適配器工廠類public class GenericProxyFactory { public static<T> T getProxy(Class<T> intf final T obj) { return (T) ProxynewProxyInstance(objgetClass()getClassLoader() new Class[] { intf } new InvocationHandler() { public Object invoke(Object proxy Method method Object[] args) throws Throwable { return methodinvoke(obj args); } }); } }

  動態代理作為Decorator

  當然動態代理工具能做的遠不僅僅是把對象類型限制在特定接口上從 清單 和 清單 中簡單的限制適配器到 Decorator 模式是一個小的飛躍在 Decorator 模式中代理用額外的功能(例如安全檢測或日志記錄)包裝調用清單 顯示了一個日志 InvocationHandler它在調用目標對象上的方法之外還寫入一條日志信息顯示被調用的方法傳遞的參數以及返回值除了反射性的 invoke() 調用之外這裡的全部代碼只是生成調試信息的一部分 —— 還不是太多代理工廠方法的代碼幾乎與 GenericProxyFactory 相同區別在於它使用的是 LoggingInvocationHandler 而不是匿名的調用句柄

  清單 基於代理的 Decorator為每個方法調用生成調試日志

  private static class LoggingInvocationHandler<T> implements InvocationHandler { final T underlying; public LoggingHandler(T underlying) { thisunderlying = underlying; } public Object invoke(Object proxy Method method Object[] args) throws Throwable { StringBuffer sb = new StringBuffer(); sbappend(methodgetName()); sbappend((); for (int i=; args != null && i<argslength; i++) { if (i != ) sbappend( ); sbappend(args[i]); } sbappend()); Object ret = methodinvoke(underlying args); if (ret != null) { sbappend( > ); sbappend(ret); } Systemoutprintln(sb); return ret; } }

  如果用日志代理包裝 HashSet並執行下面這個簡單的測試程序

  Set s = newLoggingProxy(Setclass new HashSet()); sadd(three); if (!ntains(four)) sadd(four); Systemoutprintln(s);

  會得到以下輸出

  add(three) > true contains(four) > false add(four) > true toString() > [four three] [four three]

  這種方式是給對象添加調試包裝器的一種好的而且容易的方式它當然比生成代理類並手工創建大量 println() 語句容易得多(也更通用)我進一步改進了這一方法不必無條件地生成調試輸出相反代理可以查詢動態配置存儲(從配置文件初始化可以由 JMX MBean 動態修改)確定是否需要生成調試語句甚至可能在逐個類或逐個實例的基礎上進行

  在這一點上我認為讀者中的 AOP 愛好者們幾乎要跳出來說這正是 AOP 擅長的啊!是的但是解決問題的方法不止一種 —— 僅僅因為某項技術能解決某個問題並不意味著它就是最好的解決方案在任何情況下動態代理方式都有完全在純 Java范圍內工作的優勢不是每個公司都用(或應當用) AOP 的

  動態代理作為適配器

  代理也可以用作真正的適配器提供了對象的一個視圖導出與底層對象實現的接口不同的接口調用句柄不需要把每個方法調用都分派給相同的底層對象它可以檢查名稱並把不同的方法分派給不同的對象例如假設有一組表示持久實體(PersonCompany 和 PurchaseOrder) 的 JavaBean 接口指定了屬性的 getter 和 setter而且正在編寫一個持久層把數據庫記錄映射到實現這些接口的對象上現在不用為每個接口編寫或生成類可以只用一個 JavaBean 風格的通用代理類把屬性保存在 Map 中

  清單 顯示的動態代理檢查被調用方法的名稱並通過查詢或修改屬性圖直接實現 getter 和 setter 方法現在這一個代理類就能實現多個 JavaBean 風格接口的對象

  清單 用於把 getter 和 setter 分派給 Map 的動態代理類

  public class JavaBeanProxyFactory { private static class JavaBeanProxy implements InvocationHandler { Map<String Object> properties = new HashMap<String Object>(); public JavaBeanProxy(Map<String Object> properties) { thispropertiesputAll(properties); } public Object invoke(Object proxy Method method Object[] args) throws Throwable { String meth = methodgetName(); if (methstartsWith(get)) { String prop = methsubstring(); Object o = propertiesget(prop); if (o != null && !methodgetReturnType()isInstance(o)) throw new ClassCastException(ogetClass()getName() + is not a + methodgetReturnType()getName()); return o; } else if (methstartsWith(set)) { // Dispatch setters similarly } else if (methstartsWith(is)) { // Alternate version of get for boolean properties } else { // Can dispatch non get/set/is methods as desired } } } public static<T> T getProxy(Class<T> intf Map<String Object> values) { return (T) ProxynewProxyInstance (JavaBeanProxyFactoryclassgetClassLoader() new Class[] { intf } new JavaBeanProxy(values)); } }

  雖然因為反射在 Object 上工作會有潛在的類型安全性上的損失但是JavaBeanProxyFactory 中的 getter 處理會進行一些必要的額外的類型檢測就像我在這裡用 isInstance() 對 getter 進行的檢測一樣

  性能成本

  正如已經看到的動態代理擁有簡化大量代碼的潛力 —— 不僅能替代許多生成的代碼而且一個代理類還能代替多個手寫的類或生成的代碼什麼是成本呢?因為反射地分派方法而不是采用內置的虛方法分派可能有一些性能上的成本在早期的 JDK 中反射的性能很差(就像早期 JDK 中幾乎其他每件事的性能一樣)但是在近 反射已經變得快多了

  不必進入基准測試構造的主題我編寫了一個簡單的不太科學的測試程序它循環地把數據填充到 Set隨機地對 Set進行插入查詢和刪除元素我用三個 Set 實現運行它一個未經修飾的 HashSet一個手寫的只是把所有方法轉發到底層的 HashSet 的 Set 適配器還有一個基於代理的也只是把所有方法轉發到底層 HashSet 的 Set 適配器每次循環迭代都生成若干隨機數並執行一個或多個 Set 操作手寫的適配器比起原始的 HashSet 只產生很少百分比的性能負荷(大概是因為 JVM 級有效的內聯緩沖和硬件級的分支預測)代理適配器則明顯比原始 HashSet 慢但是開銷要少於兩個量級

  我從這個試驗得出的結論是對於大多數情況代理方式即使對輕量級方法也執行得足夠好而隨著被代理的操作變得越來越重量級(例如遠程方法調用或者使用序列化執行 IO 或者從數據庫檢索數據的方法)代理開銷就會有效地接近於 當然也存在一些代理方式的性能開銷無法接受的情況但是這些通常只是少數情況

  結束語

  動態代理是強大而未充分利用的工具可以用於實現許多設計模式包括 ProxyDecorator 和 Adapter這些模式基於代理的實現容易編寫更難出錯並且具備更好的通用性在許多情況下一個動態代理類可以充當所有接口的 Decorator 或 Proxy這樣就不用每個接口都編寫一個靜態類除了最關注性能的應用程序之外動態代理方式可能比手寫或機器生成 stub 的方式更可取


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26667.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.