《设计模式的艺术》 刘伟 著 读书笔记
静态代理
由于某些原因,客户端不能或不像直接访问某个对象,此时可以通过一个称之为“代理”的第三者来实现间接访问,这种设计模式就是代理模式。
代理模式:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
常用的代理模式:
- 远程代理(Remote Proxy):在电脑A想用电脑B的对象instance,就需要在A中做一个对电脑B中对象instance的引用,这个引用就是“代理”(Proxy)。
- 虚拟代理(Virtual Proxy):对于资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。比如微信朋友圈中的图片,默认只加载缩略图,点开才加载大图。
- 保护代理(Protect Proxy):控制一个对象的访问,比如权限控制,对不同用户提供不同级别的使用权限,在对象访问时先进行权限判定。
- 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时存储空间,以便多个客户端可以共享这些结果。
- 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如记录对象被调用次数。
类图如下:
客户端有一个Subject接口的引用,RealSubject和Proxy都实现这个接口,RealSubject类就是被代理的类,在客户端中实际使用的是Proxy类的对象,其成员变量中有被代理的对象的引用,在Proxy类的实现Subject接口的方法中,执行如下代码
before();
realSubject.dosomething();
after();
其中before和after就是额外添加的职能了,比如权限验证,日志记录等。
代理模式和装饰模式有些类似,但代理模式是添加全新的职责,如上面所说的,与原职责不再同一个问题域,而装饰模式是为具体构建类添加相关职责,比如拓展算法类,给文本框加滚动条。代理模式是控制对对象的访问,而装饰模式是为对象动态增加功能。
动态代理
但是,如果我们有很多个类需要代理,不就每个要代理的类都要写个接口然后写个代理对象?这种代理成为静态代理。
而动态代理就是能让系统在运行时动态创建代理类,我们只需要为每个类写接口和实现类,代理类只需要写一次。。
Java中有这么一个方法
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandlder h)
这个方法返回的一个动态创建的代理类。那么怎么与接口(Subject)以及被代理的类(RealSubject)联系起来呢?该方法第一个参数表示代理类的类加载器,就是Subject的classLoader. 第二个参数是代理类所实现的接口列表(到时要调用接口定义的方法),第三个参数表示所指派的调用处理程序类(就是在这个类写权限认证,日志记录的东西)。
而InvocationHandler是一个接口,需要实现方法:
public Object invoke(Object proxy, Method method, Object[] args)
第一个参数是代理类的实例,第二个参数表示调用代理类的方法,第三个参数表示方法的参数数组。
来个实际例子吧,
public interface Item {
int getNum();
void printNum();
}
public class RealSubjectB implements Item {
@Override
public int getNum() {
return 0;
}
@Override
public void printNum() {
}
}
我要添加一个日志记录功能,对所有方法都输出当前时间,并对调用了get开头的方法输出提示。
public class LogHandler implements InvocationHandler {
private Object objectBeProxied;
//诸如需要提供代理的realSubjcet
public LogHandler(Object o){
objectBeProxied=o;
}
public void before(){
System.out.println(new Date().toString());
}
public void invokeGet(){
System.out.println("调用了get开头的方法");
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
if(method.getName().matches("get.+")){
invokeGet();
}
return method.invoke(objectBeProxied,args);
}
}
public class Client {
public static void main(String[] args){
Item realSubject=new RealSubjectB();
InvocationHandler handler=new LogHandler(realSubject);
Item proxy= (Item) Proxy.newProxyInstance(Item.class.getClassLoader(),new Class[]{Item.class},handler);
proxy.getNum();
System.out.println("--------");
proxy.printNum();
}
}
输出
Fri Dec 23 00:38:51 CST 2016
调用了get开头的方法
--------
Fri Dec 23 00:38:51 CST 2016
但是,这里有个疑惑,如果realSubject实现了多个接口,且代理类想调用不同接口的方法怎么办?这样在newProxyInstance的第一个参数写哪个接口都不合适啊?写了A接口就不能用B接口的方法。而如果写一个抽象类实现所有接口,然后第一个参数写抽象类,就会报ClassCastException。难道直接在第一个参数写realSubject而不是Subject?似乎也没什么不妥啊?
上面的问题找到解决办法了,首先newProxyInstance的第一个参数只是指定了如何定义而已,而第二个参数是返回的对象实现了哪些方法。
如何,如果要调用另一个接口的方法,只需要:
Item realSubject=new RealSubjectB();
InvocationHandler handler=new LogHandler(realSubject);
Item proxy= (Item) Proxy.newProxyInstance(RealSubjectB.class.getClassLoader(),new Class[]{Item.class,Category.class},handler);
proxy.getNum();
System.out.println("--------");
proxy.printNum();
System.out.println("--------");
//下面两句是新增代码,Category是新的接口,里面有getI的方法。
Category proxy1= (Category) proxy;
proxy1.getI();
竟然可以直接转型!应该是newProxyInstance第二个参数写了Category 这个接口的缘故!
其实说到底,前两个参数只是定义,实际对象还是在第三个参数传入。