去年买的Head First Pattern英文版,看了一点点,看起来还是比较吃力。。今年开始一点点的看,慢慢的看进去了,真是好书啊,一点点的从实际例子入手,一步步的、循序渐进的说明每一个设计模式,真是足够的深入浅出!以前也看过阎宏的《Java与模式》,结合中国的传统道家文化、儒家思想,甚至西游记、红楼梦、女娲造人都用上了,说的是也算够透彻了的,但是总感觉还是有些东西理解的不太深。
个人理解,代理模式在现实例子里,可以有非常多的变种,关键在于代理对象如何实现对真实对象的访问控制。变化在于访问控制的方式。着重说明下书中的3个例子,就是3种代理模式的使用场合。。
远程代理
远程代理的例子是java中的RMI。真是足够深入浅出的,让我以前对RMI非常模糊的印象也渐渐清晰起来。咱们一步步细细道来。。
第一步:定义远程接口
1.继承java.rmi.Remote接口
定义服务接口,服务接口必须继承自Remote接口。Remote接口是一个标记接口,就是这个接口,没有任何要实现的方法,仅仅是用来标识其实现类具有某种功能(个人理解),就像Serializable接口,仅仅表示实现这个接口的类能被序列化。
public interface MyRemote extends Remote {
2.服务接口中所有方法抛出RemoteException异常
RMI客户端的方法调用其实是调用实现Remote接口的Stub(桩),桩的实现是基于网络和IO的(底层就是socket),客户端在调用方法过程中,任何错误都有可能发生,所以必须让客户端知道所发生的异常,并能捕捉。
import java.rmi.*;
public interface MyRemote extends Remote {
public String sayHello() throws RemoteException;
}
3.保证返回值和参数必须是可序列化的
远程方法的参数要通过网络传输,因此必须是可序列化的,返回值也是同样。如果用原生类型(int、float等)、String、集合等,就没问题,如果用自己的类型,必须实现Serializable接口(和Remote接口一样,都是标记接口)。
第二步:实现远程服务
1.实现远程接口
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{
public String sayHello(){
return "Hello, I'm server.";
}
}
2.继承UnicastRemoteObject
要想成为一个远程服务对象,需要有远程的功能。最简单的方法就是实现UnicastRemoteObject方法了。
3.声明一个无参数的构造函数,且抛出RemoteException
public MyRemoteImpl() throws RemoteException{}
4.用RMI registry注册服务
实现远程服务后,要布远程服务供客户端使用。要实例化一个远程服务,放入RMI注册表中。注册了服务实现对象后,RMI会把Stub(桩)放入注册表,让客户端使用。
try{
MyRemote service = new MyRemoteImpl();
Naming.rebind("RemoteHello",service);
}catch(Exception e){
// ...
}
第三步:生成Stub和Skeletons(桩和骨架)
1.在远程实现类上运行rmic(不是远程接口)
rmic MyRemoteImpl(类名,不带.class)
会生成桩和骨架代码:MyRemoteImpl_Stub.class、MyRemoteImpl_Skel.class
rmic是jdk bin目录下的工具
第四步:运行rmiregistry
1.rmiregistry
必须让rmiregistry能访问到你的服务相关类,要么把类放入classpath,要么在classes目录下直接运行rmiregistry
第五步:启动服务
1.另一个dos窗口里启动服务类
java MyRemoteImpl
客户端调用方法:
MyRemote service = (MyRemote)Naming.lookup("rmi://127.0.0.1/RemoteHello");
String msg = service.sayHello();// 调用桩的方法
通过RMI registry查找服务后,返回桩,客户端必须用MyRemoteImpl_Stub.class和MyRemote.class。桩MyRemoteImpl_Stub.class、骨架MyRemoteImpl_Skel.class、MyRemote.class、MyRemoteImpl.class必须在服务端。
这么多乱七八糟的跟代理模式有什么关系?
其实客户端返回的MyRemote,其实是MyRemote_Stub,就是代理对象了,服务端的MyRemoteImpl及时实际对象,通过RMI,来获得远程对象的代理,再通过代理,来访问实际对象(远程服务实现类MyRemoteImpl)所实现的远程服务方法(MyRemote定义)。对应类图,每个类在代理模式中的角色分别是:
Subject:MyRemote接口
RealObject:MyRemoteImpl服务实现类
Proxy:MyRemote_Stub桩
在RMI中,找到服务后,拿到的MyRemote service其实是一个代理对象(MyRemote_Stub),对代理对象的方法调用,实际是通过RMI来访问远程服务实现对象的方法。也就是说代理对象MyRemote service(实际是MyRemote_Stub)通过RMI机制对远程服务对象来做访问控制,也就实现了代理模式。
虚拟代理
虚拟代理举的是一个Swing的例子。
我是这么理解的:一个对象的创建非常耗时,通过代理对象去调用,在真实对象创建前,返回一个假的调用,等真实对象创建好了,这时候返回给客户端的就是一个真实对象的相应方法调用。
也就是延迟加载的问题,Swing例子中,要显示一个Icon,但是要通过网络加载一个图片,在图片通过网络加载成功前,先显示一个“加载中,请稍候...”(如果是真实对象的调用,应该显示一个图片),在代理对象中通过后台线程去加载图片,加载完了后,再偷偷的把“加载中,请稍候...”的字样偷偷换成加载成功后的图片。
没想到这也算代理模式的一种应用场景。以前有这么在Swing中用过,需要从数据库中查找数据,但是比较耗时,就先显示“加载数据中,请稍候...”,等加载完了,再在JTable中显示出来。如果用代理模式的方式来思考,好像比较的好吧。。
同样在jsp页面里,通过ajax来加载数据好像也是这样的道理,数据没加载之前就是“加载中...”,加载完了再通过innerHTML来改变显示,也是同样的延迟加载问题。
如果用代理模式的方式来考虑,可以定义一个JavaScript类(这个类其实是个代理),这个类有个方法要显示一些从Server取出的数据,但是调用显示方法时,后台数据还没有加载,就先显示加载中请稍候之类的文本,这时候通过ajax从Server取数据(创建真实对象),取出来之后在回调函数中更新显示HTML元素的innerHTML。跟那个Swing的例子一模一样吧。不过好像JavaScript中好像没有谁会定义接口、实现、代理对象吧,但是思路其实是一样的。
不知道这样理解代理模式,算不算曲解。。。
JDK动态代理
jdk里的动态代理支持,主要是通过java.lang.reflect包中Proxy、InvocationHandler等几个类来实现的。具体如何实现可参考JDK中文文档。
使用场合:
好像在在一本Hibernate的书上,对数据库Connection的close方法调用,用动态代理的方式来拦截,并不真正关闭连接,而是返回到数据库连接池中。
在Spring中的拦截貌似有些是用动态代理实现的?不过动态代理使用时要基于接口,但是Spring是使用动态生成字节码的方式?对Spring内部实现机制不熟。。不敢妄自猜测。。等有时间好好研究再来说明。。
动态代理,我觉得最好的使用场合是给方法调用增加预处理和后处理,更加灵活了,可以做一些额外的事,同时也做到无侵入的解耦合,因为代理对象和实际对象的接口是一样的,唯一需要注意的地方是,客户端调用者是拿的接口,接口到底是使用代理对象还是实际对象,调用者并不知道,这就需要对代理对象的创建用类似工厂的方式来封装创建。比如一下代码:
PersonBean getOwnerProxy(PersonBean person){
return (PersonBean)Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person));
}
PersonBean为Subject接口,OwnerInvocationHandler实现InvocationHandler接口。
和Decorator的比较
Decorator模式在jdk的java.io包中使用非常广泛。主要用来为一个类添加新的行为。
而Proxy模式中,代理对象并不对实际对象添加新的行为,只是对实际对象做访问控制。