Java理論與實(shí)踐:用動態(tài)代理進(jìn)行修飾(轉(zhuǎn)載)
Java理論與實(shí)踐:用動態(tài)代理進(jìn)行修飾(轉(zhuǎn)載)
?
動態(tài)代理為實(shí)現(xiàn)許多常見設(shè)計(jì)模式(包括 Facade、Bridge、Interceptor、Decorator、Proxy(包括遠(yuǎn)程和虛擬代理)和 Adapter 模式)提供了替代的動態(tài)機(jī)制。雖然這些模式不使用動態(tài)代理,只用普通的類就能夠?qū)崿F(xiàn),但是在許多情況下,動態(tài)代理方式更方便、更緊湊,可以清除許多手寫或生成的類。
Proxy 模式
Proxy 模式中要創(chuàng)建“stub”或“surrogate”對象,它們的目的是接受請求并把請求轉(zhuǎn)發(fā)到實(shí)際執(zhí)行工作的其他對象。遠(yuǎn)程方法調(diào)用(RMI)利用 Proxy 模式,使得在其他 JVM 中執(zhí)行的對象就像本地對象一樣;企業(yè) JavaBeans (EJB)利用 Proxy 模式添加遠(yuǎn)程調(diào)用、安全性和事務(wù)分界;而 JAX-RPC Web 服務(wù)則用 Proxy 模式讓遠(yuǎn)程服務(wù)表現(xiàn)得像本地對象一樣。在每一種情況中,潛在的遠(yuǎn)程對象的行為是由接口定義的,而接口本質(zhì)上接受多種實(shí)現(xiàn)。調(diào)用者(在大多數(shù)情況下)不能區(qū)分出它們只是持有一個對 stub 而不是實(shí)際對象的引用,因?yàn)槎邔?shí)現(xiàn)了相同的接口;stub 的工作是查找實(shí)際的對象、封送參數(shù)、把參數(shù)發(fā)送給實(shí)際對象、解除封送返回值、把返回值返回給調(diào)用者。代理可以用來提供遠(yuǎn)程控制(就像在 RMI、EJB 和 JAX-RPC 中那樣),用安全性策略包裝對象(EJB)、為昂貴的對象(EJB 實(shí)體 Bean)提供惰性裝入,或者添加檢測工具(例如日志記錄)。
在 5.0 以前的 JDK 中,RMI stub(以及它對等的 skeleton)是在編譯時(shí)由 RMI 編譯器(rmic)生成的類,RMI 編譯器是 JDK 工具集的一部分。對于每個遠(yuǎn)程接口,都會生成一個 stub(代理)類,它代表遠(yuǎn)程對象,還生成一個 skeleton 對象,它在遠(yuǎn)程 JVM 中做與 stub 相反的工作 ―― 解除封送參數(shù)并調(diào)用實(shí)際的對象。類似地,用于 Web 服務(wù)的 JAX-RPC 工具也為遠(yuǎn)程 Web 服務(wù)生成代理類,從而使遠(yuǎn)程 Web 服務(wù)看起來就像本地對象一樣。
不管 stub 類是以源代碼還是以字節(jié)碼生成的,代碼生成仍然會向編譯過程添加一些額外步驟,而且因?yàn)槊嗨频念惖姆簽E,會帶來意義模糊的可能性。另一方面,動態(tài)代理機(jī)制支持在編譯時(shí)沒有生成 stub 類的情況下,在運(yùn)行時(shí)創(chuàng)建代理對象。在 JDK 5.0 及以后版本中,RMI 工具使用動態(tài)代理代替了生成的 stub,結(jié)果 RMI 變得更容易使用。許多 J2EE 容器也使用動態(tài)代理來實(shí)現(xiàn) EJB。EJB 技術(shù)嚴(yán)重地依靠使用攔截(interception)來實(shí)現(xiàn)安全性和事務(wù)分界;動態(tài)代理為接口上調(diào)用的所有方法提供了集中的控制流程路徑。
動態(tài)代理機(jī)制
動態(tài)代理機(jī)制的核心是 InvocationHandler 接口,如清單 1 所示。調(diào)用句柄的工作是代表動態(tài)代理實(shí)際執(zhí)行所請求的方法調(diào)用。傳遞給調(diào)用句柄一個 Method 對象(從 java.lang.reflect 包),參數(shù)列表則傳遞給方法;在最簡單的情況下,可能僅僅是調(diào)用反射性的方法 Method.invoke() 并返回結(jié)果。
清單 1. InvocationHandler 接口
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
每個代理都有一個與之關(guān)聯(lián)的調(diào)用句柄,只要代理的方法被調(diào)用時(shí)就會調(diào)用該句柄。根據(jù)通用的設(shè)計(jì)原則:接口定義類型、類定義實(shí)現(xiàn),代理對象可以實(shí)現(xiàn)一個或多個接口,但是不能實(shí)現(xiàn)類。因?yàn)榇眍悰]有可以訪問的名稱,它們不能有構(gòu)造函數(shù),所以它們必須由工廠創(chuàng)建。清單 2 顯示了動態(tài)代理的最簡單的可能實(shí)現(xiàn),它實(shí)現(xiàn) Set 接口并把所有 Set 方法(以及所有 Object 方法)分派給封裝的 Set 實(shí)例。
清單 2. 包裝 Set 的簡單的動態(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 類包含一個靜態(tài)工廠方法 getSetProxy(),它返回一個實(shí)現(xiàn)了 Set 的動態(tài)代理。代理對象實(shí)際實(shí)現(xiàn) Set ―― 調(diào)用者無法區(qū)分(除非通過反射)返回的對象是動態(tài)代理。SetProxyFactory 返回的代理只做一件事,把方法分派給傳遞給工廠方法的 Set 實(shí)例。雖然反射代碼通常比較難讀,但是這里的內(nèi)容很少,跟上控制流程并不難 ―― 只要某個方法在 Set 代理上被調(diào)用,它就被分派給調(diào)用句柄,調(diào)用句柄只是反射地調(diào)用底層包裝的對象上的目標(biāo)方法。當(dāng)然,絕對什么都不做的代理可能有點(diǎn)傻,是不是呢?
什么都不做的適配器
對于像 SetProxyFactory 這樣什么都不做的包裝器來說,實(shí)際有個很好的應(yīng)用 ―― 可以用它安全地把對象引用的范圍縮小到特定接口(或接口集)上,方式是,調(diào)用者不能提升引用的類型,使得可以更安全地把對象引用傳遞給不受信任的代碼(例如插件或回調(diào))。清單 3 包含一組類定義,實(shí)現(xiàn)了典型的回調(diào)場景。從中會看到動態(tài)代理可以更方便地替代通常用手工(或用 IDE 提供的代碼生成向?qū)В?shí)現(xiàn)的 Adapter 模式。
清單 3. 典型的回調(diào)場景
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 類實(shí)現(xiàn)了 ServiceCallback(這通常是支持回調(diào)的一個方便途徑)并把 this 引用傳遞給 serviceMethod() 作為回調(diào)引用。這種方法的問題是沒有機(jī)制可以阻止 Service 實(shí)現(xiàn)把 ServiceCallback 提升為 ServiceConsumer,并調(diào)用 ServiceConsumer 不希望 Service 調(diào)用的方法。有時(shí)對這個風(fēng)險(xiǎn)并不關(guān)心 ―― 但有時(shí)卻關(guān)心。如果關(guān)心,那么可以把回調(diào)對象作為內(nèi)部類,或者編寫一個什么都不做的適配器類(請參閱清單 4 中的 ServiceCallbackAdapter)并用 ServiceCallbackAdapter 包裝 ServiceConsumer。ServiceCallbackAdapter 防止 Service 把 ServiceCallback 提升為 ServiceConsumer。
清單 4. 用于安全地把對象限制在一個接口上以便不被惡意代碼不能的適配器類
public class ServiceCallbackAdapter implements ServiceCallback {
private final ServiceCallback cb;
public ServiceCallbackAdapter(ServiceCallback cb) {
this.cb = cb;
}
public void doCallback() {
cb.doCallback();
}
}
編寫 ServiceCallbackAdapter 這樣的適配器類簡單卻乏味。必須為包裝的接口中的每個方法編寫重定向類。在 ServiceCallback 的示例中,只有一個需要實(shí)現(xiàn)的方法,但是某些接口,例如 Collections 或 JDBC 接口,則包含許多方法。現(xiàn)代的 IDE 提供了“Delegate Methods”向?qū)В档土司帉戇m配器類的工作量,但是仍然必須為每個想要包裝的接口編寫一個適配器類,而且對于只包含生成的代碼的類,也有一些讓人不滿意的地方。看起來應(yīng)當(dāng)有一種方式可以更緊湊地表示“什么也不做的限制適配器模式”。
通用適配器類
清單 2 中的 SetProxyFactory 類當(dāng)然比用于 Set 的等價(jià)的適配器類更緊湊,但是它仍然只適用于一個接口:Set。但是通過使用泛型,可以容易地創(chuàng)建通用的代理工廠,由它為任何接口做同樣的工作,如清單 5 所示。它幾乎與 SetProxyFactory 相同,但是可以適用于任何接口。現(xiàn)在再也不用編寫限制適配器類了!如果想創(chuàng)建代理對象安全地把對象限制在接口 T,只要調(diào)用 getProxy(T.class,object) 就可以了,不需要一堆適配器類的額外累贅。
清單 5. 通用的限制適配器工廠類
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);
}
});
}
}
動態(tài)代理作為 Decorator
當(dāng)然,動態(tài)代理工具能做的,遠(yuǎn)不僅僅是把對象類型限制在特定接口上。從 清單 2 和 清單 5 中簡單的限制適配器到 Decorator 模式,是一個小的飛躍,在 Decorator 模式中,代理用額外的功能(例如安全檢測或日志記錄)包裝調(diào)用。清單 6 顯示了一個日志 InvocationHandler,它在調(diào)用目標(biāo)對象上的方法之外,還寫入一條日志信息,顯示被調(diào)用的方法、傳遞的參數(shù),以及返回值。除了反射性的 invoke() 調(diào)用之外,這里的全部代碼只是生成調(diào)試信息的一部分 ―― 還不是太多。代理工廠方法的代碼幾乎與 GenericProxyFactory 相同,區(qū)別在于它使用的是 LoggingInvocationHandler 而不是匿名的調(diào)用句柄。
清單 6. 基于代理的 Decorator,為每個方法調(diào)用生成調(diào)試日志
private static class LoggingInvocationHandler<T>
implements InvocationHandler {
final T underlying;
public LoggingHandler(T underlying) {
this
Proxy 模式
Proxy 模式中要創(chuàng)建“stub”或“surrogate”對象,它們的目的是接受請求并把請求轉(zhuǎn)發(fā)到實(shí)際執(zhí)行工作的其他對象。遠(yuǎn)程方法調(diào)用(RMI)利用 Proxy 模式,使得在其他 JVM 中執(zhí)行的對象就像本地對象一樣;企業(yè) JavaBeans (EJB)利用 Proxy 模式添加遠(yuǎn)程調(diào)用、安全性和事務(wù)分界;而 JAX-RPC Web 服務(wù)則用 Proxy 模式讓遠(yuǎn)程服務(wù)表現(xiàn)得像本地對象一樣。在每一種情況中,潛在的遠(yuǎn)程對象的行為是由接口定義的,而接口本質(zhì)上接受多種實(shí)現(xiàn)。調(diào)用者(在大多數(shù)情況下)不能區(qū)分出它們只是持有一個對 stub 而不是實(shí)際對象的引用,因?yàn)槎邔?shí)現(xiàn)了相同的接口;stub 的工作是查找實(shí)際的對象、封送參數(shù)、把參數(shù)發(fā)送給實(shí)際對象、解除封送返回值、把返回值返回給調(diào)用者。代理可以用來提供遠(yuǎn)程控制(就像在 RMI、EJB 和 JAX-RPC 中那樣),用安全性策略包裝對象(EJB)、為昂貴的對象(EJB 實(shí)體 Bean)提供惰性裝入,或者添加檢測工具(例如日志記錄)。
在 5.0 以前的 JDK 中,RMI stub(以及它對等的 skeleton)是在編譯時(shí)由 RMI 編譯器(rmic)生成的類,RMI 編譯器是 JDK 工具集的一部分。對于每個遠(yuǎn)程接口,都會生成一個 stub(代理)類,它代表遠(yuǎn)程對象,還生成一個 skeleton 對象,它在遠(yuǎn)程 JVM 中做與 stub 相反的工作 ―― 解除封送參數(shù)并調(diào)用實(shí)際的對象。類似地,用于 Web 服務(wù)的 JAX-RPC 工具也為遠(yuǎn)程 Web 服務(wù)生成代理類,從而使遠(yuǎn)程 Web 服務(wù)看起來就像本地對象一樣。
不管 stub 類是以源代碼還是以字節(jié)碼生成的,代碼生成仍然會向編譯過程添加一些額外步驟,而且因?yàn)槊嗨频念惖姆簽E,會帶來意義模糊的可能性。另一方面,動態(tài)代理機(jī)制支持在編譯時(shí)沒有生成 stub 類的情況下,在運(yùn)行時(shí)創(chuàng)建代理對象。在 JDK 5.0 及以后版本中,RMI 工具使用動態(tài)代理代替了生成的 stub,結(jié)果 RMI 變得更容易使用。許多 J2EE 容器也使用動態(tài)代理來實(shí)現(xiàn) EJB。EJB 技術(shù)嚴(yán)重地依靠使用攔截(interception)來實(shí)現(xiàn)安全性和事務(wù)分界;動態(tài)代理為接口上調(diào)用的所有方法提供了集中的控制流程路徑。
動態(tài)代理機(jī)制
動態(tài)代理機(jī)制的核心是 InvocationHandler 接口,如清單 1 所示。調(diào)用句柄的工作是代表動態(tài)代理實(shí)際執(zhí)行所請求的方法調(diào)用。傳遞給調(diào)用句柄一個 Method 對象(從 java.lang.reflect 包),參數(shù)列表則傳遞給方法;在最簡單的情況下,可能僅僅是調(diào)用反射性的方法 Method.invoke() 并返回結(jié)果。
清單 1. InvocationHandler 接口
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
每個代理都有一個與之關(guān)聯(lián)的調(diào)用句柄,只要代理的方法被調(diào)用時(shí)就會調(diào)用該句柄。根據(jù)通用的設(shè)計(jì)原則:接口定義類型、類定義實(shí)現(xiàn),代理對象可以實(shí)現(xiàn)一個或多個接口,但是不能實(shí)現(xiàn)類。因?yàn)榇眍悰]有可以訪問的名稱,它們不能有構(gòu)造函數(shù),所以它們必須由工廠創(chuàng)建。清單 2 顯示了動態(tài)代理的最簡單的可能實(shí)現(xiàn),它實(shí)現(xiàn) Set 接口并把所有 Set 方法(以及所有 Object 方法)分派給封裝的 Set 實(shí)例。
清單 2. 包裝 Set 的簡單的動態(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 類包含一個靜態(tài)工廠方法 getSetProxy(),它返回一個實(shí)現(xiàn)了 Set 的動態(tài)代理。代理對象實(shí)際實(shí)現(xiàn) Set ―― 調(diào)用者無法區(qū)分(除非通過反射)返回的對象是動態(tài)代理。SetProxyFactory 返回的代理只做一件事,把方法分派給傳遞給工廠方法的 Set 實(shí)例。雖然反射代碼通常比較難讀,但是這里的內(nèi)容很少,跟上控制流程并不難 ―― 只要某個方法在 Set 代理上被調(diào)用,它就被分派給調(diào)用句柄,調(diào)用句柄只是反射地調(diào)用底層包裝的對象上的目標(biāo)方法。當(dāng)然,絕對什么都不做的代理可能有點(diǎn)傻,是不是呢?
什么都不做的適配器
對于像 SetProxyFactory 這樣什么都不做的包裝器來說,實(shí)際有個很好的應(yīng)用 ―― 可以用它安全地把對象引用的范圍縮小到特定接口(或接口集)上,方式是,調(diào)用者不能提升引用的類型,使得可以更安全地把對象引用傳遞給不受信任的代碼(例如插件或回調(diào))。清單 3 包含一組類定義,實(shí)現(xiàn)了典型的回調(diào)場景。從中會看到動態(tài)代理可以更方便地替代通常用手工(或用 IDE 提供的代碼生成向?qū)В?shí)現(xiàn)的 Adapter 模式。
清單 3. 典型的回調(diào)場景
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 類實(shí)現(xiàn)了 ServiceCallback(這通常是支持回調(diào)的一個方便途徑)并把 this 引用傳遞給 serviceMethod() 作為回調(diào)引用。這種方法的問題是沒有機(jī)制可以阻止 Service 實(shí)現(xiàn)把 ServiceCallback 提升為 ServiceConsumer,并調(diào)用 ServiceConsumer 不希望 Service 調(diào)用的方法。有時(shí)對這個風(fēng)險(xiǎn)并不關(guān)心 ―― 但有時(shí)卻關(guān)心。如果關(guān)心,那么可以把回調(diào)對象作為內(nèi)部類,或者編寫一個什么都不做的適配器類(請參閱清單 4 中的 ServiceCallbackAdapter)并用 ServiceCallbackAdapter 包裝 ServiceConsumer。ServiceCallbackAdapter 防止 Service 把 ServiceCallback 提升為 ServiceConsumer。
清單 4. 用于安全地把對象限制在一個接口上以便不被惡意代碼不能的適配器類
public class ServiceCallbackAdapter implements ServiceCallback {
private final ServiceCallback cb;
public ServiceCallbackAdapter(ServiceCallback cb) {
this.cb = cb;
}
public void doCallback() {
cb.doCallback();
}
}
編寫 ServiceCallbackAdapter 這樣的適配器類簡單卻乏味。必須為包裝的接口中的每個方法編寫重定向類。在 ServiceCallback 的示例中,只有一個需要實(shí)現(xiàn)的方法,但是某些接口,例如 Collections 或 JDBC 接口,則包含許多方法。現(xiàn)代的 IDE 提供了“Delegate Methods”向?qū)В档土司帉戇m配器類的工作量,但是仍然必須為每個想要包裝的接口編寫一個適配器類,而且對于只包含生成的代碼的類,也有一些讓人不滿意的地方。看起來應(yīng)當(dāng)有一種方式可以更緊湊地表示“什么也不做的限制適配器模式”。
通用適配器類
清單 2 中的 SetProxyFactory 類當(dāng)然比用于 Set 的等價(jià)的適配器類更緊湊,但是它仍然只適用于一個接口:Set。但是通過使用泛型,可以容易地創(chuàng)建通用的代理工廠,由它為任何接口做同樣的工作,如清單 5 所示。它幾乎與 SetProxyFactory 相同,但是可以適用于任何接口。現(xiàn)在再也不用編寫限制適配器類了!如果想創(chuàng)建代理對象安全地把對象限制在接口 T,只要調(diào)用 getProxy(T.class,object) 就可以了,不需要一堆適配器類的額外累贅。
清單 5. 通用的限制適配器工廠類
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);
}
});
}
}
動態(tài)代理作為 Decorator
當(dāng)然,動態(tài)代理工具能做的,遠(yuǎn)不僅僅是把對象類型限制在特定接口上。從 清單 2 和 清單 5 中簡單的限制適配器到 Decorator 模式,是一個小的飛躍,在 Decorator 模式中,代理用額外的功能(例如安全檢測或日志記錄)包裝調(diào)用。清單 6 顯示了一個日志 InvocationHandler,它在調(diào)用目標(biāo)對象上的方法之外,還寫入一條日志信息,顯示被調(diào)用的方法、傳遞的參數(shù),以及返回值。除了反射性的 invoke() 調(diào)用之外,這里的全部代碼只是生成調(diào)試信息的一部分 ―― 還不是太多。代理工廠方法的代碼幾乎與 GenericProxyFactory 相同,區(qū)別在于它使用的是 LoggingInvocationHandler 而不是匿名的調(diào)用句柄。
清單 6. 基于代理的 Decorator,為每個方法調(diào)用生成調(diào)試日志
private static class LoggingInvocationHandler<T>
implements InvocationHandler {
final T underlying;
public LoggingHandler(T underlying) {
this