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