http://www-128.ibm.com/developerworks/cn/java/j-jtp08305.html
Java 理論與實(shí)踐:
用動(dòng)態(tài)代理進(jìn)行修飾
動(dòng)態(tài)代理是構(gòu)建 Decorator 和 Adapter 的方便工具

動(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)代理,只用普通的類(lèi)就能夠?qū)崿F(xiàn),但是在許多情況下,動(dòng)態(tài)代理方式更方便、更緊湊,可以清除許多手寫(xiě)或生成的類(lèi)。

Proxy 模式

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)生成的類(lèi),RMI 編譯器是 JDK 工具集的一部分。對(duì)于每個(gè)遠(yuǎn)程接口,都會(huì)生成一個(gè) stub(代理)類(lèi),它代表遠(yuǎn)程對(duì)象,還生成一個(gè) skeleton 對(duì)象,它在遠(yuǎn)程 JVM 中做與 stub 相反的工作 —— 解除封送參數(shù)并調(diào)用實(shí)際的對(duì)象。類(lèi)似地,用于 Web 服務(wù)的 JAX-RPC 工具也為遠(yuǎn)程 Web 服務(wù)生成代理類(lèi),從而使遠(yuǎn)程 Web 服務(wù)看起來(lái)就像本地對(duì)象一樣。

不管 stub 類(lèi)是以源代碼還是以字節(jié)碼生成的,代碼生成仍然會(huì)向編譯過(guò)程添加一些額外步驟,而且因?yàn)槊嗨频念?lèi)的泛濫,會(huì)帶來(lái)意義模糊的可能性。另一方面,動(dòng)態(tài)代理機(jī)制支持在編譯時(shí)沒(méi)有生成 stub 類(lèi)的情況下,在運(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)用的所有方法提供了集中的控制流程路徑。



回頁(yè)首


動(dòng)態(tài)代理機(jī)制

動(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 接口

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

每個(gè)代理都有一個(gè)與之關(guān)聯(lián)的調(diào)用句柄,只要代理的方法被調(diào)用時(shí)就會(huì)調(diào)用該句柄。根據(jù)通用的設(shè)計(jì)原則:接口定義類(lèi)型、類(lèi)定義實(shí)現(xiàn),代理對(duì)象可以實(shí)現(xiàn)一個(gè)或多個(gè)接口,但是不能實(shí)現(xiàn)類(lèi)。因?yàn)榇眍?lèi)沒(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)代理

public class SetProxyFactory {

    public static Set getSetProxy(final Set s) {
        return (Set) Proxy.newProxyInstance
          (s.getClass().getClassLoader(),
                new Class[] { Set.class },
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, 
                      Object[] args) throws Throwable {
                        return method.invoke(s, args);
                    }
                });
    }
}

SetProxyFactory 類(lèi)包含一個(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)用者不能提升引用的類(lèi)型,使得可以更安全地把對(duì)象引用傳遞給不受信任的代碼(例如插件或回調(diào))。清單 3 包含一組類(lèi)定義,實(shí)現(xiàn)了典型的回調(diào)場(chǎng)景。從中會(huì)看到動(dòng)態(tài)代理可以更方便地替代通常用手工(或用 IDE 提供的代碼生成向?qū)В?shí)現(xiàn)的 Adapter 模式。


清單 3. 典型的回調(diào)場(chǎng)景

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() {
        ...
        service.serviceMethod(this);
    }
}

ServiceConsumer 類(lèi)實(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)部類(lèi),或者編寫(xiě)一個(gè)什么都不做的適配器類(lèi)(請(qǐng)參閱清單 4 中的 ServiceCallbackAdapter)并用 ServiceCallbackAdapter 包裝 ServiceConsumer。ServiceCallbackAdapter 防止 ServiceServiceCallback 提升為 ServiceConsumer。


清單 4. 用于安全地把對(duì)象限制在一個(gè)接口上以便不被惡意代碼不能的適配器類(lèi)

public class ServiceCallbackAdapter implements ServiceCallback {
    private final ServiceCallback cb;

    public ServiceCallbackAdapter(ServiceCallback cb) {
        this.cb = cb;
    }

    public void doCallback() {
        cb.doCallback();
    }
}

編寫(xiě) ServiceCallbackAdapter 這樣的適配器類(lèi)簡(jiǎn)單卻乏味。必須為包裝的接口中的每個(gè)方法編寫(xiě)重定向類(lèi)。在 ServiceCallback 的示例中,只有一個(gè)需要實(shí)現(xiàn)的方法,但是某些接口,例如 Collections 或 JDBC 接口,則包含許多方法。現(xiàn)代的 IDE 提供了“Delegate Methods”向?qū)?,降低了編?xiě)適配器類(lèi)的工作量,但是仍然必須為每個(gè)想要包裝的接口編寫(xiě)一個(gè)適配器類(lèi),而且對(duì)于只包含生成的代碼的類(lèi),也有一些讓人不滿意的地方。看起來(lái)應(yīng)當(dāng)有一種方式可以更緊湊地表示“什么也不做的限制適配器模式”。

通用適配器類(lèi)

清單 2 中的 SetProxyFactory 類(lèi)當(dāng)然比用于 Set 的等價(jià)的適配器類(lèi)更緊湊,但是它仍然只適用于一個(gè)接口:Set。但是通過(guò)使用泛型,可以容易地創(chuàng)建通用的代理工廠,由它為任何接口做同樣的工作,如清單 5 所示。它幾乎與 SetProxyFactory 相同,但是可以適用于任何接口。現(xiàn)在再也不用編寫(xiě)限制適配器類(lèi)了!如果想創(chuàng)建代理對(duì)象安全地把對(duì)象限制在接口 T,只要調(diào)用 getProxy(T.class,object) 就可以了,不需要一堆適配器類(lèi)的額外累贅。


清單 5. 通用的限制適配器工廠類(lèi)

public class GenericProxyFactory {

    public static<T> T getProxy(Class<T> intf, 
      final T obj) {
        return (T) 
          Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                new Class[] { intf },
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, 
                      Object[] args) throws Throwable {
                        return method.invoke(obj, args);
                    }
                });
    }
}



回頁(yè)首


動(dòng)態(tài)代理作為 Decorator

當(dāng)然,動(dòng)態(tài)代理工具能做的,遠(yuǎn)不僅僅是把對(duì)象類(lèi)型限制在特定接口上。從 清單 2清單 5 中簡(jiǎn)單的限制適配器到 Decorator 模式,是一個(gè)小的飛躍,在 Decorator 模式中,代理用額外的功能(例如安全檢測(cè)或日志記錄)包裝調(diào)用。清單 6 顯示了一個(gè)日志 InvocationHandler,它在調(diào)用目標(biāo)對(duì)象上的方法之外,還寫(xiě)入一條日志信息,顯示被調(diào)用的方法、傳遞的參數(shù),以及返回值。除了反射性的 invoke() 調(diào)用之外,這里的全部代碼只是生成調(diào)試信息的一部分 —— 還不是太多。代理工廠方法的代碼幾乎與 GenericProxyFactory 相同,區(qū)別在于它使用的是 LoggingInvocationHandler 而不是匿名的調(diào)用句柄。


清單 6. 基于代理的 Decorator,為每個(gè)方法調(diào)用生成調(diào)試日志

    private static class LoggingInvocationHandler<T> 
      implements InvocationHandler {
        final T underlying;

        public LoggingHandler(T underlying) {
            this.underlying = underlying;
        }

        public Object invoke(Object proxy, Method method, 
          Object[] args) throws Throwable {
            StringBuffer sb = new StringBuffer();
            sb.append(method.getName()); sb.append("(");
            for (int i=0; args != null && i<args.length; i++) {
                if (i != 0)
                    sb.append(", ");
                sb.append(args[i]);
            }
            sb.append(")");
            Object ret = method.invoke(underlying, args);
            if (ret != null) {
                sb.append(" -> "); sb.append(ret);
            }
            System.out.println(sb);
            return ret;
        }
    }

如果用日志代理包裝 HashSet,并執(zhí)行下面這個(gè)簡(jiǎn)單的測(cè)試程序:


    Set s = newLoggingProxy(Set.class, new HashSet());
    s.add("three");
    if (!s.contains("four"))
        s.add("four");
    System.out.println(s);

會(huì)得到以下輸出:


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

這種方式是給對(duì)象添加調(diào)試包裝器的一種好的而且容易的方式。它當(dāng)然比生成代理類(lèi)并手工創(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è)類(lèi)或逐個(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 的。

動(dòng)態(tài)代理作為適配器

代理也可以用作真正的適配器,提供了對(duì)象的一個(gè)視圖,導(dǎo)出與底層對(duì)象實(shí)現(xiàn)的接口不同的接口。調(diào)用句柄不需要把每個(gè)方法調(diào)用都分派給相同的底層對(duì)象;它可以檢查名稱,并把不同的方法分派給不同的對(duì)象。例如,假設(shè)有一組表示持久實(shí)體(Person、CompanyPurchaseOrder) 的 JavaBean 接口,指定了屬性的 getter 和 setter,而且正在編寫(xiě)一個(gè)持久層,把數(shù)據(jù)庫(kù)記錄映射到實(shí)現(xiàn)這些接口的對(duì)象上?,F(xiàn)在不用為每個(gè)接口編寫(xiě)或生成類(lèi),可以只用一個(gè) JavaBean 風(fēng)格的通用代理類(lèi),把屬性保存在 Map 中。

清單 7 顯示的動(dòng)態(tài)代理檢查被調(diào)用方法的名稱,并通過(guò)查詢或修改屬性圖直接實(shí)現(xiàn) getter 和 setter 方法?,F(xiàn)在,這一個(gè)代理類(lèi)就能實(shí)現(xiàn)多個(gè) JavaBean 風(fēng)格接口的對(duì)象。


清單 7. 用于把 getter 和 setter 分派給 Map 的動(dòng)態(tài)代理類(lèi)

public class JavaBeanProxyFactory {
    private static class JavaBeanProxy implements InvocationHandler {
        Map<String, Object> properties = new HashMap<String, 
          Object>();

        public JavaBeanProxy(Map<String, Object> properties) {
            this.properties.putAll(properties);
        }

        public Object invoke(Object proxy, Method method, 
          Object[] args) 
          throws Throwable {
            String meth = method.getName();
            if (meth.startsWith("get")) {
                String prop = meth.substring(3);
                Object o = properties.get(prop);
                if (o != null && !method.getReturnType().isInstance(o))
                    throw new ClassCastException(o.getClass().getName() + 
                      " is not a " + method.getReturnType().getName());
                return o;
            }
            else if (meth.startsWith("set")) {
                // Dispatch setters similarly
            }
            else if (meth.startsWith("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) Proxy.newProxyInstance
          (JavaBeanProxyFactory.class.getClassLoader(),
                new Class[] { intf }, new JavaBeanProxy(values));
    }
}

雖然因?yàn)榉瓷湓?Object 上工作會(huì)有潛在的類(lèi)型安全性上的損失,但是,JavaBeanProxyFactory 中的 getter 處理會(huì)進(jìn)行一些必要的額外的類(lèi)型檢測(cè),就像我在這里用 isInstance() 對(duì) getter 進(jìn)行的檢測(cè)一樣。



回頁(yè)首


性能成本

正如已經(jīng)看到的,動(dòng)態(tài)代理?yè)碛泻?jiǎn)化大量代碼的潛力 —— 不僅能替代許多生成的代碼,而且一個(gè)代理類(lèi)還能代替多個(gè)手寫(xiě)的類(lèi)或生成的代碼。什么是成本呢? 因?yàn)榉瓷涞胤峙煞椒ǘ皇遣捎脙?nèi)置的虛方法分派,可能有一些性能上的成本。在早期的 JDK 中,反射的性能很差(就像早期 JDK 中幾乎其他每件事的性能一樣),但是在近 10 年,反射已經(jīng)變得快多了。

不必進(jìn)入基準(zhǔn)測(cè)試構(gòu)造的主題,我編寫(xiě)了一個(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è)手寫(xiě)的、只是把所有方法轉(zhuǎn)發(fā)到底層的 HashSetSet 適配器,還有一個(gè)基于代理的、也只是把所有方法轉(zhuǎn)發(fā)到底層 HashSetSet 適配器。每次循環(huán)迭代都生成若干隨機(jī)數(shù),并執(zhí)行一個(gè)或多個(gè) Set 操作。手寫(xiě)的適配器比起原始的 HashSet 只產(chǎn)生很少百分比的性能負(fù)荷(大概是因?yàn)?JVM 級(jí)有效的內(nèi)聯(lián)緩沖和硬件級(jí)的分支預(yù)測(cè));代理適配器則明顯比原始 HashSet 慢,但是開(kāi)銷(xiāo)要少于兩個(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)銷(xiāo)就會(huì)有效地接近于 0。當(dāng)然也存在一些代理方式的性能開(kāi)銷(xiāo)無(wú)法接受的情況,但是這些通常只是少數(shù)情況。