这种方式是给对象添加调试包装器的一种好的而且容易的方式。它当然比生成代理类并手工创建大量 println() 语句容易得多(也更通用)。我进一步改进了这一方法;不必无条件地生成调试输出,相反,代理可以查询动态配置存储(从配置文件初始化,可以由 JMX MBean 动态修改),确定是否需要生成调试语句,甚至可能在逐个类或逐个实例的基础上进行。

  在这一点上,我认为读者中的 AOP 爱好者们几乎要跳出来说“这正是 AOP 擅长的啊!”是的,但是解决问题的方法不止一种 —— 仅仅因为某项技术能解决某个问题,并不意味着它是好的解决方案。在任何情况下,动态代理方式都有完全在“纯 Java”范围内工作的优势,不是每个公司都用(或应当用) AOP 的。

  动态代理作为适配器

  代理也可以用作真正的适配器,提供了对象的一个视图,导出与底层对象实现的接口不同的接口。调用句柄不需要把每个方法调用都分派给相同的底层对象;它可以检查名称,并把不同的方法分派给不同的对象。例如,假设有一组表示持久实体 (Person、Company 和 PurchaseOrder) 的 JavaBean 接口,指定了属性的 getter 和 setter,而且正在编写一个持久层,把数据库记录映射到实现这些接口的对象上。现在不用为每个接口编写或生成类,可以只用一个 JavaBean 风格的通用代理类,把属性保存在 Map 中。

  清单 7 显示的动态代理检查被调用方法的名称,并通过查询或修改属性图直接实现 getter 和 setter 方法。现在,这一个代理类能实现多个 JavaBean 风格接口的对象。

  清单 7. 用于把 getter 和 setter 分派给 Map 的动态代理类


1.public class JavaBeanProxyFactory {  
2. private static class JavaBeanProxy implements InvocationHandler {  
3. Map properties = new HashMap();  
4.public JavaBeanProxy(Mapproperties) {  
5. this.properties.putAll(properties);  
6. }  
7.public Object invoke(Object proxy, Method method,  
8. Object[] args)  
9. throws Throwable {  
10. String meth = method.getName();  
11. if (meth.startsWith("get")) {  
12. String prop = meth.substring(3);  
13. Object o = properties.get(prop);  
14. if (o != null && !method.getReturnType().isInstance(o))  
15. throw new ClassCastException(o.getClass().getName() +  
16. " is not a " + method.getReturnType().getName());  
17. return o;  
18. }  
19. else if (meth.startsWith("set")) {  
20. // Dispatch setters similarly  
21. }  
22. else if (meth.startsWith("is")) {  
23. // Alternate version of get for boolean properties  
24. }  
25. else {  
26. // Can dispatch non get/set/is methods as desired  
27. }  
28. }  
29. }  
30.public staticT getProxy(Classintf,  
31. Map values) {  
32. return (T) Proxy.newProxyInstance  
33. (JavaBeanProxyFactory.class.getClassLoader(),  
34. new Class[] { intf }, new JavaBeanProxy(values));  
35. }  
36. }
 
  虽然因为反射在 Object 上工作会有潜在的类型安全性上的损失,但是,JavaBeanProxyFactory 中的 getter 处理会进行一些必要的额外的类型检测,像我在这里用 isInstance() 对 getter 进行的检测一样。

  性能成本

  正如已经看到的,动态代理拥有简化大量代码的潜力 —— 不仅能替代许多生成的代码,而且一个代理类还能代替多个手写的类或生成的代码。什么是成本呢? 因为反射地分派方法而不是采用内置的虚方法分派,可能有一些性能上的成本。在早期的 JDK 中,反射的性能很差(像早期 JDK 中几乎其他每件事的性能一样),但是在近 10 年,反射已经变得快多了。

  不必进入基准测试构造的主题,我编写了一个简单的、不太科学的测试程序,它循环地把数据填充到 Set,随机地对 Set进行插入、查询和删除元素。我用三个 Set 实现运行它:一个未经修饰的 HashSet,一个手写的、只是把所有方法转发到底层的 HashSet 的 Set 适配器,还有一个基于代理的、也只是把所有方法转发到底层 HashSet 的 Set 适配器。每次循环迭代都生成若干随机数,并执行一个或多个 Set 操作。手写的适配器比起原始的 HashSet 只产生很少百分比的性能负荷(大概是因为 JVM 级有效的内联缓冲和硬件级的分支预测);代理适配器则明显比原始 HashSet 慢,但是开销要少于两个量级。

  我从这个试验得出的结论是:对于大多数情况,代理方式即使对轻量级方法也执行得足够好,而随着被代理的操作变得越来越重量级(例如远程方法调用,或者使用序列化、执行 IO 或者从数据库检索数据的方法),代理开销会有效地接近于 0。当然也存在一些代理方式的性能开销无法接受的情况,但是这些通常只是少数情况。

  结束语

  动态代理是强大而未充分利用的工具,可以用于实现许多设计模式,包括 Proxy、Decorator 和 Adapter。这些模式基于代理的实现容易编写,更难出错,并且具备更好的通用性;在许多情况下,一个动态代理类可以充当所有接口的 Decorator 或 Proxy,这样不用每个接口都编写一个静态类。除了关注性能的应用程序之外,动态代理方式可能比手写或机器生成 stub 的方式更可取。