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并不是一种常见的操作。还有其他许多方式可以使用动态代理。它们提供了一个调试层,可方便地记录一个对象上的所有方法调用的具体细节。它们可执行绑定检查,并对方法参数进行验证。在与远程数据源发生冲突的前提下,甚至可用它们将备用的本地测试后端动态地交换出去。如果你采用的是良好的、由接口驱动的设计方案,我个人觉得动态代理的用处肯定要比你想象的多,最终你会叹服于它从容解决许多问题的本事!