Java 1.3引入了名為“動態代理類”(Dynamic Proxy Class)的新特性,利用它可為“已知接口的實現”動態地創建包裝器(wrapper)類。1.3版本問世以前,當我首次聽說當時正在提議的動態代理類時,還以為它只是一種用來吸引人的眼球的特性。雖然把它包括到語言中是一件好事,但我卻想不出它有任何實際用處。帶著這一成見,我試著用動態代理寫了一個示例程序,卻驚訝于它的巨大威力,并當即決定把它放到我的工具箱中,以便在將來的項目中使用。此后,我不斷體驗到它的好處,它總是能用正確的方法來做你想要做的事情!
假如沒有動態代理
深入探索動態代理類之前,先來看看在某些情況下,假如沒有動態代理類會是什么樣子:
public interface Robot {
void moveTo(int x, int y);
void workOn(Project p, Tool t);
}
public class MyRobot implements Robot {
public void moveTo(int x, int y) {
// stuff happens here
}
public void workOn(Project p, Tool t) {
// optionally destructive stuff happens here
}
}
上述代碼展示了一個名為Robot的接口,以及該接口的一個名為MyRobot的大致的實現。假定你現在想攔截對MyRobot類發出的方法調用(可能是為了限制一個參數的值)。
public class BuilderRobot implements Robot {
private Robot wrapped;
public BuilderRobot(Robot r) {
wrapped = r;
}
public void moveTo(int x, int y) {
wrapped.moveTo(x, y);
}
public void workOn(Project p, Tool t) {
if (t.isDestructive()) {
t = Tool.RATCHET;
}
wrapped.workOn(p, t);
}
}
一個辦法就是使用顯式的包裝器類,就像上面顯示的那樣。BuilderRobot類在其構造函數中獲取一個Robot,并攔截workOn方法,確保在任何項目中使用的工具都沒有破壞性。另外,由于BuilderRobot這一包裝器實現了Robot接口,所以凡是能夠使用一個Robot的任何地方,都能使用一個BuilderRobot實例。
對于這種包裝器風格的BuilderRobot來說,一旦你想修改或擴展Robot接口,它的缺點就會暴露無遺。為Robot接口添加一個方法,就得為BuilderRobot類添加一個包裝器方法。為Robot添加10個方法,就得為BuilderRobot添加10個方法。如果BuilderRobot、CrusherRobot、SpeedyRobot和SlowRobot都是Robot包裝器類,就必須分別為它們添加10個方法。這顯然是效率極差的一種方案。
public class BuilderRobot extends MyRobot {
public void workOn(Project p, Tool t) {
if (t.isDestructive()) {
t = Tool.RATCHET;
}
super.workOn(p, t);
}
}
上述代碼是對 BuilderRobot進行編程的另一種方式。注意BuilderRobot變成了MyRobot的一個子類。這樣可解決在第2段代碼的包裝器方案中出現的問題。也就是說,修改Robot接口不必修改BuilderRobot。但這又產生了一個新問題:只有MyRobot對象才能是BuilderRobot。而在此之前,實現了Robot接口的任何對象都可以成為一個BuilderRobot。現在,由Java施加的“線性類出身限制”(linear class parentage restrictions)禁止我們將任意Robot(ArbitraryRobot)變成一個BuilderRobot。
動態代理也有限制
動態代理則綜合了以上兩種方案的優點。使用動態代理,你創建的包裝器類不要求為所有方法都使用顯式的包裝器,創建的子類也不要求具有嚴格的出身,兩者方法可任選一種你認為最好的。但是,動態代理仍然有一個限制。當你使用動態代理時,要包裝/擴展的對象必須實現一個接口,該接口定義了準備在包裝器中使用的所有方法。這一限制的宗旨是鼓勵良好的設計,而不是為你帶來更多的麻煩。根據經驗,每個類都至少應該實現一個接口(nonconstant接口)。良好的接口用法不僅使動態代理成為可能,還有利于程序的模塊化。
使用動態代理
下面的代碼演示了用動態代理來創建一個BuilderRobot時所必需的類。注意我們創建的這個BuilderRobotInvocationHandler類甚至根本沒有實現Robot接口。相反,它實現了java.lang.reflect.InvocationHandler,只提供了一個invoke方法。代理對象上的任何方法調用都要通過這一方法進行。觀察invoke的主體,我們發現它會檢查準備調用的方法的名稱。如果這個名稱是workOn,第二個參數就切換成一個非破壞性的工具。
然而,我們得到的仍然只是一個具有invoke方法的InvocationHandler,而不是我們真正想要的Robot對象。動態代理真正的魅力要到創建實際的Robot實例時才能反映出來。在源代碼的任何地方,我們都沒有定義一個Robot包裝器或者子類。雖然如此,我們最終仍能獲得一個動態創建的類,它通過調用BuilderRobotInvocationHandler的靜態方法createBuilderRobot中的代碼片斷,從而實現了Robot接口,并集成了Builder工具過濾器。
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class BuilderRobotInvocationHandler implements InvocationHandler {
private Robot wrapped;
public BuilderRobotInvocationHandler(Robot r) {
wrapped = r;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if ("workOn".equals(method.getName())) {
args[1] = Tool.RATCHET;
}
return method.invoke(wrapped, args);
}
public static Robot createBuilderRobot(Robot toWrap) {
return (Robot)(Proxy.newProxyInstance(Robot.class.getClassLoader(),
new Class[] {Robot.class},
new BuilderRobotInvocationHandler(toWrap)));
}
public static final void main(String[] args) {
Robot r = createBuilderRobot(new MyRobot());
r.workOn("scrap", Tool.CUTTING_TORCH);
}
}
createBuilderRobot中的代碼表面上很復雜,但它的作用其實很簡單,就是告訴Proxy類用一個指定的類加載器來動態創建一個對象,該對象要實現指定的接口(本例為Robot),并用提供的InvocationHandler來代替傳統的方法主體。結果對象在一個instanceof Robot測試中返回true,并提供了在實現了Robot接口的任何類中都能找到的方法。
有趣的是,在BuilderRobotInvocationHandler類的invoke方法中,完全不存在對Robot接口的引用。InvocationHandlers并不是它們向其提供了“代理方法實現”的接口所專用的,你完全可以寫一個InvocationHandler,并將其作為眾多代理類的后端來使用。
但在本例中,我們以構造函數參數的形式,為BuilderRobotInvocationHandler提供了RobotInterface的另一個實例。代理Robot實例上的任何方法調用最終都由BuilderRobotInvocationHandler委托給這個“包裝的”Robot。但是,雖然這是最常見的設計,但你必須了解,InvocationHandler不一定非要委托給被代理的接口的另一個實例。事實上,InvocationHandler完全能自行提供方法主體,而無需一個委托目標。
最后要注意,如果Robot接口中發生改變,那么BuilderRobotInvocationHandler中的invoke方法將反應遲鈍。例如,假定workOn方法被重命名,那么非破壞性工具陷阱會悄悄地失敗,這時的BuilderRobots就有可能造成損害。較容易檢測、但卻不一定會造成問題的是workOn方法的重載版本。如果方法具有相同的名稱,但使用一個不同的參數列表,就可能在運行時造成一個ClassCastException或者ArrayIndexOutOfBoundsException異常。為此,以下代碼給出了一個解決方案,它能生成一個更靈活的BuilderRobotInvocationHandler。在這段代碼中,任何時候在任何方法中使用一個工具,這個工具就會被替換成一個非破壞性工具。請試著用子類化處理或者傳統的委托來進行試驗。
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class BuilderRobotInvocationHandler implements InvocationHandler {
private Robot wrapped;
public BuilderRobotInvocationHandler(Robot r) {
wrapped = r;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Class[] paramTypes = method.getParameterTypes();
for (int i=0; i < paramTypes.length; i++) {
if (Tool.class.isAssignableFrom(paramTypes[i])) {
args[i] = Tool.RATCHET;
}
}
return method.invoke(wrapped, args);
}
public static Robot createBuilderRobot(Robot toWrap) {
return (Robot)(Proxy.newProxyInstance(Robot.class.getClassLoader(),
new Class[] {Robot.class},
new BuilderRobotInvocationHandler(toWrap)));
}
public static final void main(String[] args) {
Robot r = createBuilderRobot(new MyRobot());
r.workOn("scrap", Tool.CUTTING_TORCH);
}
}
使用建議
在大多數開發環境中,用工具來取代Robot并不是一種常見的操作。還有其他許多方式可以使用動態代理。它們提供了一個調試層,可方便地記錄一個對象上的所有方法調用的具體細節。它們可執行綁定檢查,并對方法參數進行驗證。在與遠程數據源發生沖突的前提下,甚至可用它們將備用的本地測試后端動態地交換出去。如果你采用的是良好的、由接口驅動的設計方案,我個人覺得動態代理的用處肯定要比你想象的多,最終你會嘆服于它從容解決許多問題的本事!