您现在的位置: 苏州弘智 > JAVA > JAVA资讯 >

Java 理论与实践: 用动态代理进行修饰(2)

时间:2012-10-17 22:19 作者:弘智教育 点击:
通用适配器类
清单 2 中的 SetProxyFactory 类当然比用于 Set 的等价的适配器类更紧凑,但是它仍然只适用于一个接口:Set。但是通过使用泛型,可以容易地创建通用的代理工厂,由它为任何接口做同样的工作,如清单 5 所示。它几乎与 SetProxyFactory 相同,但是可以适用于任何接口。现在再也不用编写限制适配器类了!如果想创建代理对象安全地把对象限制在接口 T,只要调用 getProxy(T.class,object) 就可以了,不需要一堆适配器类的额外累赘。

清单 5. 通用的限制适配器工厂类
双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GenericProxyFactory {
    
    public static<T> T getProxy(Class<T> intf, 
      final T obj) {
        return (T) 
          Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                new Class[] { intf },
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, 
                      Object[] args) throws Throwable {
                        return method.invoke(obj, args);
                    }
                });
    }
}
 
动态代理作为 Decorator
当 然,动态代理工具能做的,远不仅仅是把对象类型限制在特定接口上。从清单 2 和 清单 5 中简单的限制适配器到 Decorator 模式,是一个小的飞跃,在 Decorator 模式中,代理用额外的功能(例如安全检测或日志记录)包装调用。清单 6 显示了一个日志 InvocationHandler,它在调用目标对象上的方法之外,还写入一条日志信息,显示被调用的方法、传递的参数,以及返回值。除了反射性的 invoke() 调用之外,这里的全部代码只是生成调试信息的一部分 —— 还不是太多。代理工厂方法的代码几乎与 GenericProxyFactory 相同,区别在于它使用的是 LoggingInvocationHandler 而不是匿名的调用句柄。

清单 6. 基于代理的 Decorator,为每个方法调用生成调试日志
双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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,并执行下面这个简单的测试程序:
双击代码全选
1
2
3
4
5
Set s = newLoggingProxy(Set.class, new HashSet());
s.add("three");
if (!s.contains("four"))
    s.add("four");
System.out.println(s);
 
会得到以下输出:
双击代码全选
1
2
3
4
5
add(three) -> true
contains(four) -> false
add(four) -> true
toString() -> [four, three]
[four, three]
 
这种方式是给对象添加调试包装器的一种好的而且容易的方式。它当然比生成代理类并手工创建大量 println() 语句容易得多(也更通用)。我进一步改进了这一方法;不必无条件地生成调试输出,相反,代理可以查询动态配置存储(从配置文件初始化,可以由 JMX  MBean 动态修改),确定是否需要生成调试语句,甚至可能在逐个类或逐个实例的基础上进行。
在 这一点上,我认为读者中的 AOP 爱好者们几乎要跳出来说“这正是 AOP 擅长的啊!”是的,但是解决问题的方法不止一种 —— 仅仅因为某项技术能解决某个问题,并不意味着它就是最好的解决方案。在任何情况下,动态代理方式都有完全在“纯 Java”范围内工作的优势,不是每个公司都用(或应当用) AOP 的。
动态代理作为适配器
代理也可以用作真正的适配器,提供了对象的一个视图,导出与底层对象实现的接口不同的接口。调用句柄不需要把每个方法调用都分派给相同的底层对象;它可以检查名称,并把不同的方法分派给不同的对象。例如,假设有一组表示持久实体(PersonCompanyPurchaseOrder) 的 JavaBean 接口,指定了属性的 getter 和 setter,而且正在编写一个持久层,把数据库记录映射到实现这些接口的对象上。现在不用为每个接口编写或生成类,可以只用一个 JavaBean 风格的通用代理类,把属性保存在 Map 中。
清单 7 显示的动态代理检查被调用方法的名称,并通过查询或修改属性图直接实现 getter 和 setter 方法。现在,这一个代理类就能实现多个 JavaBean 风格接口的对象。

清单 7. 用于把 getter 和 setter 分派给 Map 的动态代理类
双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 进行的检测一样。
性能成本
正 如已经看到的,动态代理拥有简化大量代码的潜力 —— 不仅能替代许多生成的代码,而且一个代理类还能代替多个手写的类或生成的代码。什么是成本呢? 因为反射地分派方法而不是采用内置的虚方法分派,可能有一些性能上的成本。在早期的 JDK 中,反射的性能很差(就像早期 JDK 中几乎其他每件事的性能一样),但是在近 10 年,反射已经变得快多了。
不必进入基准测试构造的主题,我编写了一个简单的、不太科学的测试程序,它循环地把数据填充到 Set,随机地对 Set进行插入、查询和删除元素。我用三个Set 实现运行它:一个未经修饰的 HashSet,一个手写的、只是把所有方法转发到底层的 HashSetSet 适配器,还有一个基于代理的、也只是把所有方法转发到底层 HashSetSet 适配器。每次循环迭代都生成若干随机数,并执行一个或多个 Set 操作。手写的适配器比起原始的 HashSet 只产生很少百分比的性能负荷(大概是因为 JVM 级有效的内联缓冲和硬件级的分支预测);代理适配器则明显比原始 HashSet 慢,但是开销要少于两个量级。
我 从这个试验得出的结论是:对于大多数情况,代理方式即使对轻量级方法也执行得足够好,而随着被代理的操作变得越来越重量级(例如远程方法调用,或者使用序 列化、执行 IO 或者从数据库检索数据的方法),代理开销就会有效地接近于 0。当然也存在一些代理方式的性能开销无法接受的情况,但是这些通常只是少数情况。
结束语
动 态代理是强大而未充分利用的工具,可以用于实现许多设计模式,包括 Proxy、Decorator 和 Adapter。这些模式基于代理的实现容易编写,更难出错,并且具备更好的通用性;在许多情况下,一个动态代理类可以充当所有接口的 Decorator 或 Proxy,这样就不用每个接口都编写一个静态类。除了最关注性能的应用程序之外,动态代理方式可能比手写或机器生成 stub 的方式更可取。
弘智主页 | 学校概况 | 培训课程 | 最新开课  | 学历证书 | 在线报名 | 新闻资讯 | 人才就业