当然,动态代理工具能做的,远不仅仅是把对象类型限制在特定接口上。从 清单 2 和 清单 5 中简单的限制适配器到 Decorator 模式,是一个小的飞跃,在 Decorator 模式中,代理用额外的功能(例如安全检测或日志记录)包装调用。清单 6 显示了一个日志 InvocationHandler
,它在调用目标对象上的方法之外,还写入一条日志信息,显示被调用的方法、传递的参数,以及返回值。除了反射性的 invoke()
调用之外,这里的全部代码只是生成调试信息的一部分 —— 还不是太多。代理工厂方法的代码几乎与 GenericProxyFactory
相同,区别在于它使用的是 LoggingInvocationHandler
而不是匿名的调用句柄。
清单 6. 基于代理的 Decorator,为每个方法调用生成调试日志
private static class LoggingInvocationHandler<T>
implements InvocationHandler {
final T underlying;
public LoggingHandler(T underlying) {
this.underlying = underlying;
}
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
StringBuffer sb = new StringBuffer();
sb.append(method.getName()); sb.append("(");
for (int i=0; args != null && i<args.length; i++) {
if (i != 0)
sb.append(", ");
sb.append(args[i]);
}
sb.append(")");
Object ret = method.invoke(underlying, args);
if (ret != null) {
sb.append(" -> "); sb.append(ret);
}
System.out.println(sb);
return ret;
}
}
|
如果用日志代理包装 HashSet
,并执行下面这个简单的测试程序:
Set s = newLoggingProxy(Set.class, new HashSet()); s.add("three"); if (!s.contains("four")) s.add("four"); System.out.println(s); |
会得到以下输出:
add(three) -> true contains(four) -> false add(four) -> true toString() -> [four, three] [four, three] |
这种方式是给对象添加调试包装器的一种好的而且容易的方式。它当然比生成代理类并手工创建大量 println()
语句容易得多(也更通用)。我进一步改进了这一方法;不必无条件地生成调试输出,相反,代理可以查询动态配置存储(从配置文件初始化,可以由 JMX MBean
动态修改),确定是否需要生成调试语句,甚至可能在逐个类或逐个实例的基础上进行。
在这一点上,我认为读者中的 AOP 爱好者们几乎要跳出来说“这正是 AOP 擅长的啊!”是的,但是解决问题的方法不止一种 —— 仅仅因为某项技术能解决某个问题,并不意味着它就是最好的解决方案。在任何情况下,动态代理方式都有完全在“纯 Java”范围内工作的优势,不是每个公司都用(或应当用) AOP 的。
代理也可以用作真正的适配器,提供了对象的一个视图,导出与底层对象实现的接口不同的接口。调用句柄不需要把每个方法调用都分派给相同的底层对象;它可以检查名称,并把不同的方法分派给不同的对象。例如,假设有一组表示持久实体(Person
、Company
和 PurchaseOrder
) 的 JavaBean 接口,指定了属性的 getter 和 setter,而且正在编写一个持久层,把数据库记录映射到实现这些接口的对象上。现在不用为每个接口编写或生成类,可以只用一个 JavaBean 风格的通用代理类,把属性保存在 Map 中。
清单 7 显示的动态代理检查被调用方法的名称,并通过查询或修改属性图直接实现 getter 和 setter 方法。现在,这一个代理类就能实现多个 JavaBean 风格接口的对象。
清单 7. 用于把 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) { this.properties.putAll(properties); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String meth = method.getName(); if (meth.startsWith("get")) { String prop = meth.substring(3); Object o = properties.get(prop); if (o != null && !method.getReturnType().isInstance(o)) throw new ClassCastException(o.getClass().getName() + " is not a " + method.getReturnType().getName()); return o; } else if (meth.startsWith("set")) { // Dispatch setters similarly } else if (meth.startsWith("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) Proxy.newProxyInstance (JavaBeanProxyFactory.class.getClassLoader(), new Class[] { intf }, new JavaBeanProxy(values)); } } |
虽然因为反射在 Object
上工作会有潜在的类型安全性上的损失,但是,JavaBeanProxyFactory 中的 getter 处理会进行一些必要的额外的类型检测,就像我在这里用 isInstance()
对 getter 进行的检测一样。