动态代理工具 是 javalangreflect 包的一部分在 JDK 版本中添加到 JDK它允许程序创建 代理对象代理对象能实现一个或多个已知接口并用反射代替内置的虚方法分派编程地分派对接口方法的调用这个过程允许实现截取方法调用重新路由它们或者动态地添加功能
动态代理为实现许多常见设计模式(包括 FacadeBridgeInterceptorDecoratorProxy(包括远程和虚拟代理)和 Adapter 模式)提供了替代的动态机制虽然这些模式不使用动态代理只用普通的类就能够实现但是在许多情况下动态代理方式更方便更紧凑可以清除许多手写或生成的类
Proxy模式
Proxy 模式中要创建stub或surrogate对象它们的目的是接受请求并把请求转发到实际执行工作的其他对象远程方法调用(RMI)利用 Proxy 模式使得在其他 JVM 中执行的对象就像本地对象一样企业 JavaBeans (EJB)利用 Proxy 模式添加远程调用安全性和事务分界而 JAXRPC Web 服务则用 Proxy 模式让远程服务表现得像本地对象一样在每一种情况中潜在的远程对象的行为是由接口定义的而接口本质上接受多种实现调用者(在大多数情况下)不能区分出它们只是持有一个对 stub 而不是实际对象的引用因为二者实现了相同的接口stub 的工作是查找实际的对象封送参数把参数发送给实际对象解除封送返回值把返回值返回给调用者代理可以用来提供远程控制(就像在 RMIEJB 和 JAXRPC 中那样)用安全性策略包装对象(EJB)为昂贵的对象(EJB 实体 Bean)提供惰性装入或者添加检测工具(例如日志记录)
在 以前的 JDK 中RMI stub(以及它对等的 skeleton)是在编译时由 RMI 编译器(rmic)生成的类RMI 编译器是 JDK 工具集的一部分对于每个远程接口都会生成一个 stub(代理)类它代表远程对象还生成一个 skeleton 对象它在远程 JVM 中做与 stub 相反的工作 —— 解除封送参数并调用实际的对象类似地用于 Web 服务的 JAXRPC 工具也为远程 Web 服务生成代理类从而使远程 Web 服务看起来就像本地对象一样
不管 stub 类是以源代码还是以字节码生成的代码生成仍然会向编译过程添加一些额外步骤而且因为命名相似的类的泛滥会带来意义模糊的可能性另一方面动态代理机制支持在编译时没有生成 stub 类的情况下在运行时创建代理对象在 JDK 及以后版本中RMI 工具使用动态代理代替了生成的 stub结果 RMI 变得更容易使用许多 JEE 容器也使用动态代理来实现 EJBEJB 技术严重地依靠使用拦截(interception)来实现安全性和事务分界动态代理为接口上调用的所有方法提供了集中的控制流程路径
动态代理机制
动态代理机制的核心是 InvocationHandler 接口如清单 所示调用句柄的工作是代表动态代理实际执行所请求的方法调用传递给调用句柄一个 Method 对象(从 javalangreflect 包)参数列表则传递给方法在最简单的情况下可能仅仅是调用反射性的方法 Methodinvoke() 并返回结果
清单 InvocationHandler 接口
public interface InvocationHandler { Object invoke(Object proxy Method method Object[] args) throws Throwable; }
每个代理都有一个与之关联的调用句柄只要代理的方法被调用时就会调用该句柄根据通用的设计原则接口定义类型类定义实现代理对象可以实现一个或多个接口但是不能实现类因为代理类没有可以访问的名称它们不能有构造函数所以它们必须由工厂创建清单 显示了动态代理的最简单的可能实现它实现 Set 接口并把所有 Set 方法(以及所有 Object 方法)分派给封装的 Set 实例
清单 包装 Set 的简单的动态代理
public class SetProxyFactory { public static Set getSetProxy(final Set s) { return (Set) ProxynewProxyInstance (sgetClass()getClassLoader() new Class[] { Setclass } new InvocationHandler() { public Object invoke(Object proxy Method method Object[] args) throws Throwable { return methodinvoke(s args); } }); } }
SetProxyFactory 类包含一个静态工厂方法 getSetProxy()它返回一个实现了 Set 的动态代理代理对象实际实现 Set —— 调用者无法区分(除非通过反射)返回的对象是动态代理SetProxyFactory 返回的代理只做一件事把方法分派给传递给工厂方法的 Set 实例虽然反射代码通常比较难读但是这里的内容很少跟上控制流程并不难 —— 只要某个方法在 Set 代理上被调用它就被分派给调用句柄调用句柄只是反射地调用底层包装的对象上的目标方法当然绝对什么都不做的代理可能有点傻是不是呢?
什么都不做的适配器
对于像 SetProxyFactory 这样什么都不做的包装器来说实际有个很好的应用 —— 可以用它安全地把对象引用的范围缩小到特定接口(或接口集)上方式是调用者不能提升引用的类型使得可以更安全地把对象引用传递给不受信任的代码(例如插件或回调)清单 包含一组类定义实现了典型的回调场景从中会看到动态代理可以更方便地替代通常用手工(或用 IDE 提供的代码生成向导)实现的 Adapter 模式
清单 典型的回调场景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() { serviceserviceMethod(this); } }
ServiceConsumer 类实现了 ServiceCallback(这通常是支持回调的一个方便途径)并把 this 引用传递给 serviceMethod() 作为回调引用这种方法的问题是没有机制可以阻止 Service 实现把 ServiceCallback 提升为 ServiceConsumer并调用 ServiceConsumer 不希望 Service 调用的方法有时对这个风险并不关心 —— 但有时却关心如果关心那么可以把回调对象作为内部类或者编写一个什么都不做的适配器类(请参阅清单 中的 ServiceCallbackAdapter)并用 ServiceCallbackAdapter 包装 ServiceConsumerServiceCallbackAdapter 防止 Service 把 ServiceCallback 提升为 ServiceConsumer
清单 用于安全地把对象限制在一个接口上以便不被恶意代码不能的适配器类public class ServiceCallbackAdapter implements ServiceCallback { private final ServiceCallback cb; public ServiceCallbackAdapter(ServiceCallback cb) { thiscb = cb; } public void doCallback() { cbdoCallback(); } }
编写 ServiceCallbackAdapter 这样的适配器类简单却乏味必须为包装的接口中的每个方法编写重定向类在 ServiceCallback 的示例中只有一个需要实现的方法但是某些接口例如 Collections 或 JDBC 接口则包含许多方法现代的 IDE 提供了Delegate Methods向导降低了编写适配器类的工作量但是仍然必须为每个想要包装的接口编写一个适配器类而且对于只包含生成的代码的类也有一些让人不满意的地方看起来应当有一种方式可以更紧凑地表示什么也不做的限制适配器模式
通用适配器类
清单 中的 SetProxyFactory 类当然比用于 Set 的等价的适配器类更紧凑但是它仍然只适用于一个接口Set但是通过使用泛型可以容易地创建通用的代理工厂由它为任何接口做同样的工作如清单 所示它几乎与 SetProxyFactory 相同但是可以适用于任何接口现在再也不用编写限制适配器类了!如果想创建代理对象安全地把对象限制在接口 T只要调用 getProxy(Tclassobject) 就可以了不需要一堆适配器类的额外累赘
清单 通用的限制适配器工厂类public class GenericProxyFactory { public static<T> T getProxy(Class<T> intf final T obj) { return (T) ProxynewProxyInstance(objgetClass()getClassLoader() new Class[] { intf } new InvocationHandler() { public Object invoke(Object proxy Method method Object[] args) throws Throwable { return methodinvoke(obj args); } }); } }
动态代理作为Decorator
当然动态代理工具能做的远不仅仅是把对象类型限制在特定接口上从 清单 和 清单 中简单的限制适配器到 Decorator 模式是一个小的飞跃在 Decorator 模式中代理用额外的功能(例如安全检测或日志记录)包装调用清单 显示了一个日志 InvocationHandler它在调用目标对象上的方法之外还写入一条日志信息显示被调用的方法传递的参数以及返回值除了反射性的 invoke() 调用之外这里的全部代码只是生成调试信息的一部分 —— 还不是太多代理工厂方法的代码几乎与 GenericProxyFactory 相同区别在于它使用的是 LoggingInvocationHandler 而不是匿名的调用句柄
清单 基于代理的 Decorator为每个方法调用生成调试日志
private static class LoggingInvocationHandler<T> implements InvocationHandler { final T underlying; public LoggingHandler(T underlying) { thisunderlying = underlying; } public Object invoke(Object proxy Method method Object[] args) throws Throwable { StringBuffer sb = new StringBuffer(); sbappend(methodgetName()); sbappend((); for (int i=; args != null && i<argslength; i++) { if (i != ) sbappend( ); sbappend(args[i]); } sbappend()); Object ret = methodinvoke(underlying args); if (ret != null) { sbappend( > ); sbappend(ret); } Systemoutprintln(sb); return ret; } }
如果用日志代理包装 HashSet并执行下面这个简单的测试程序
Set s = newLoggingProxy(Setclass new HashSet()); sadd(three); if (!ntains(four)) sadd(four); Systemoutprintln(s);
会得到以下输出
add(three) > true contains(four) > false add(four) > true toString() > [four three] [four three]
这种方式是给对象添加调试包装器的一种好的而且容易的方式它当然比生成代理类并手工创建大量 println() 语句容易得多(也更通用)我进一步改进了这一方法不必无条件地生成调试输出相反代理可以查询动态配置存储(从配置文件初始化可以由 JMX MBean 动态修改)确定是否需要生成调试语句甚至可能在逐个类或逐个实例的基础上进行
在这一点上我认为读者中的 AOP 爱好者们几乎要跳出来说这正是 AOP 擅长的啊!是的但是解决问题的方法不止一种 —— 仅仅因为某项技术能解决某个问题并不意味着它就是最好的解决方案在任何情况下动态代理方式都有完全在纯 Java范围内工作的优势不是每个公司都用(或应当用) AOP 的
动态代理作为适配器
代理也可以用作真正的适配器提供了对象的一个视图导出与底层对象实现的接口不同的接口调用句柄不需要把每个方法调用都分派给相同的底层对象它可以检查名称并把不同的方法分派给不同的对象例如假设有一组表示持久实体(PersonCompany 和 PurchaseOrder) 的 JavaBean 接口指定了属性的 getter 和 setter而且正在编写一个持久层把数据库记录映射到实现这些接口的对象上现在不用为每个接口编写或生成类可以只用一个 JavaBean 风格的通用代理类把属性保存在 Map 中
清单 显示的动态代理检查被调用方法的名称并通过查询或修改属性图直接实现 getter 和 setter 方法现在这一个代理类就能实现多个 JavaBean 风格接口的对象
清单 用于把 getter 和 setter 分派给 Map 的动态代理类
public class JavaBeanProxyFactory { private static class JavaBeanProxy implements InvocationHandler { Map<String Object> properties = new HashMap<String Object>(); public JavaBeanProxy(Map<String Object> properties) { thispropertiesputAll(properties); } public Object invoke(Object proxy Method method Object[] args) throws Throwable { String meth = methodgetName(); if (methstartsWith(get)) { String prop = methsubstring(); Object o = propertiesget(prop); if (o != null && !methodgetReturnType()isInstance(o)) throw new ClassCastException(ogetClass()getName() + is not a + methodgetReturnType()getName()); return o; } else if (methstartsWith(set)) { // Dispatch setters similarly } else if (methstartsWith(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) ProxynewProxyInstance (JavaBeanProxyFactoryclassgetClassLoader() new Class[] { intf } new JavaBeanProxy(values)); } }
虽然因为反射在 Object 上工作会有潜在的类型安全性上的损失但是JavaBeanProxyFactory 中的 getter 处理会进行一些必要的额外的类型检测就像我在这里用 isInstance() 对 getter 进行的检测一样
性能成本
正如已经看到的动态代理拥有简化大量代码的潜力 —— 不仅能替代许多生成的代码而且一个代理类还能代替多个手写的类或生成的代码什么是成本呢?因为反射地分派方法而不是采用内置的虚方法分派可能有一些性能上的成本在早期的 JDK 中反射的性能很差(就像早期 JDK 中几乎其他每件事的性能一样)但是在近 年反射已经变得快多了
不必进入基准测试构造的主题我编写了一个简单的不太科学的测试程序它循环地把数据填充到 Set随机地对 Set进行插入查询和删除元素我用三个 Set 实现运行它一个未经修饰的 HashSet一个手写的只是把所有方法转发到底层的 HashSet 的 Set 适配器还有一个基于代理的也只是把所有方法转发到底层 HashSet 的 Set 适配器每次循环迭代都生成若干随机数并执行一个或多个 Set 操作手写的适配器比起原始的 HashSet 只产生很少百分比的性能负荷(大概是因为 JVM 级有效的内联缓沖和硬件级的分支预测)代理适配器则明显比原始 HashSet 慢但是开销要少于两个量级
我从这个试验得出的结论是对于大多数情况代理方式即使对轻量级方法也执行得足够好而随着被代理的操作变得越来越重量级(例如远程方法调用或者使用序列化执行 IO 或者从数据库检索数据的方法)代理开销就会有效地接近于 当然也存在一些代理方式的性能开销无法接受的情况但是这些通常只是少数情况
结束语
动态代理是强大而未充分利用的工具可以用于实现许多设计模式包括 ProxyDecorator 和 Adapter这些模式基于代理的实现容易编写更难出错并且具备更好的通用性在许多情况下一个动态代理类可以充当所有接口的 Decorator 或 Proxy这样就不用每个接口都编写一个静态类除了最关注性能的应用程序之外动态代理方式可能比手写或机器生成 stub 的方式更可取