设计模式 - D10 - 代理模式
代理模式
定义
代理模式:为另一个对象提供一个替身或占位符以控制对这个对象的访问
使用代理模式创建代表,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。
代理控制访问的方式:
- 远程代理控制访问远程对象
- 虚拟代理控制访问创建开销大的资源
- 保护代理基于权限控制对资源的访问
Subject:为RealSubject和Proxy提供接口,通过实现同一接口,Proxy在RealSubject出现的地方取代它
RealSubject:真正做事的对象,被proxy代理和控制访问
Proxy:持有RealSubject的引用,甚至负责其创建与销毁;客户与RealSubject之间的交互必须通过Proxy。
远程代理
远程代理好比“远程对象的本地代表”,其中“远程对象”指活在不同JVM堆中的对象,“本地代表”指由本地方法调用的对象,其行为会转发到远程对象中
客户对象所做的事就像在做远程方法调用,但其实只是调用本地堆中的“代理”对象上的方法,再有代理处理所有网络通信的低层细节
但是我们不能取得另一个堆中的对象引用,即:Duck d = <另一个堆的对象>是无效的,变量d只能引用当前代码语句的同一堆空间的对象;解决方法:RMI-Java远程方法调用
远程方法
当我们想要设计一个系统能够调用本地对象,然后将每个请求转发到远程对象上进行时,我们需要一些辅助对象帮助我们进行真正的沟通
- 客户辅助对象:联系远程服务器,传送方法调用信息(如:方法名、变量…),然后等待服务器返回。对于客户对象来说,客户辅助对象就像是真正的服务,但其实它只负责转发请求。
- 服务辅助对象:从客户辅助对象中接收请求(Socket链接),将调用信息解包,然后调用真正服务对象上的真正方法;当服务辅助对象获得返回值时,将返回值打包再相应给客户辅助对象(Socket输出流)。对于服务对象来说,调用是本地的,来自服务辅助对象而不是远程客户。
Java RMI
RMI提供了客户辅助对象和服务辅助对象,为客户辅助辅助对象创建和服务对象相同的方法,并提供了用于寻找和访问远程对象的查找服务。RMI将客户辅助对象称为Stub-“桩”,服务辅助对象称为Skelteton-“骨架”
由于客户辅助对象会通过网络发送方法调用,而网络和IO是有风险且容易失败的,因此随时可能抛出异常
制作远程服务
- 制作远程接口
远程接口定义出可以让客户远程调用的方法,客户再将其作为服务的类类型。Stub和实际的服务都是先此接口
// 1. 扩展Remote接口,这是一个无方法的标记接口
public interface MyRemote extends Remote {
// 2. 声明所有方法都会抛出RemoteException,以防网络及IO的异常
// 3. 确定变量和返回值是属于原语类型或可序列化类型(实现Serializable接口)
// - 对象通过网络运回必须可序列化
public String sayHello() throws RemoteException;
}
- 制作远程实现
这是实际工作的类,为远程接口中定义的远程方法提供了真正的实现,这就是客户真正想要调用方法的对象(如:GumballMachine)
// 2. 扩展UnicastRemoteObject,以提供远程功能
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{
// 1. 实现远程接口MyRemote
@Override
public String sayHello() throws RemoteException {
return "Server says, 'Hey'";
}
// 3. 设计一个不带变量的构造器并声明RemoteException
// - 结局超类UnicastRemoteObject的构造器抛出RemoteException的问题
public MyRemoteImpl() throws RemoteException {
}
}
try{
// 利用RMI注册服务(需要先开启一个终端执行rmiregistry,见第4步)
MyRemote service = new MyRemoteImpl();
Naming.rebind("RemoteHello", service);
} catch (Exception ex) { ... }
- 利用rmic产生stub和skeleton
这是客户和服务的辅助类,我们不需要自己创建;当运行rmic工具时,会自动处理
// 在MyRemoteImpl.class存放目录下执行rmic命令,生成stub和skeleton
rmic MyRemoteImpl
- 启动RMI registry(rmiregistry)
rmiregistry就像是电话簿,客户可以从中查找到代理的位置(也就是客户的stub helper对象)
// 开启一个终端执行rmiregistry
rmiregistry
- 开始远程服务
必须让服务对象开始运行:服务实现类会去实例化一个服务的实例,并将这个服务注册到RMI registry;注册后,这个服务就可以供客户调用
// 开启另一个终端,启动服务:可从一个远程实现类中的main()方法或一个独立的启动类中启动
public class ProxyTestDrive {
public static void main(String[] args) {
try {
MyRemote lookup = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
System.out.println(lookup.sayHello());
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:
- 启动远程服务之前必须先启动rmiregistry(要用Naming.rebind()注册服务,rmiregistry必须是运行的)
- 变量和返回值类型必须是可序列化类型(实现Serializable接口,transient关键字可告诉JVM不要序列化某个字段)
- 必须为客户提供stub类(skeleton不再必须)
虚拟代理
虚拟代理作为创建开销大的对象的代表,经常在我们真正需要一个对象时才创建这个开销大的真实对象。在真实对象的创建前和创建中时,虚拟代理扮演该对象的替身;创建后,代理就会将请求直接委托给对象
示例
假设现在我们需要在一个应用程序上加载某个图像,因为加载图像是一个开销较大的工作,所以我们可以利用虚拟代理在图像加载未完成时显示一些信息,如:“图像加载中”,然后再加载完成后,将显示责任委托给创建出来的图像对象
工作流程
- ImageProxy创建一个ImageIcon,然后从网络URL上加载图像
- 加载过程中,ImageProxy显示"图像加载中"
- 当图像加载完毕,ImageProxy吧所有方法调用委托给真正的ImageIcon,这些方法包括了paintIcon()、getWidth()和getHeight()
- 如果用户请求新的图像,就创建新的代理,重复这样的过程
public class ImageProxy implements Icon {
ImageIcon imageIcon;
URL imageUrl;
Thread retrievalThread;
boolean retrieving = false;
public ImageProxy(URL url) {
imageUrl = url;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
if (imageIcon != null) {
imageIcon.paintIcon(c, g, x, y);
} else {
g.drawString("Loading CD cover, please wait...", x+300, y+300);
if (!retrieving) {
retrieving = true;
retrievalThread = new Thread(new Runnable() {
@Override
public void run() {
try {
imageIcon = new ImageIcon(imageUrl,"CD Cover");
c.repaint();
} catch (Exception e) {
e.printStackTrace();
}
}
});
retrievalThread.start();
}
}
}
@Override
public int getIconWidth() {
if (imageIcon != null) {
return imageIcon.getIconWidth();
} else {
return 800;
}
}
@Override
public int getIconHeight() {
if (imageIcon != null) {
return imageIcon.getIconHeight();
} else {
return 600;
}
}
}
保护代理
动态代理
Java的动态代理提供了在运行时动态创建一个代理类、实现一个或多个接口并将方法的调用转发刀所指定的类的支持
- Proxy:由Java产生,且实现了完整的Subject接口
- InvocationHnadler:Proxy上的任何方法调用都会传入此类,控制对RealSubject的访问
假设,我们现在有一个PersonBean接口和其实现类PersonBeanImpl
public interface PersonBean {
String getName();
String getGender();
String getInterests();
int getHotOrNotRating();
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
void setHotOrNotRating(int rating);
}
public class PersonBeanImpl implements PersonBean{
String name;
String gender;
String interests;
int rating;
int ratingCount;
@Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getGender() {
return gender;
}
@Override
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String getInterests() {
return interests;
}
@Override
public int getHotOrNotRating() {
if (ratingCount == 0) return 0;
return (rating/ratingCount);
}
public void setInterests(String interests) {
this.interests = interests;
}
@Override
public void setHotOrNotRating(int rating) {
this.rating += rating;
ratingCount++;
}
}
显然,由于PersonImpl上的方法都是公有的,因此任何人只要获取到一个PersonBeanImpl对象,就能修改数据;对此我们可以使用保护代理,根据访问权限,决定客户可否访问对象。
创建InvocationHandler
我们需要创建两个InvocationHandler,一个给拥有者使用,另一个给非拥有者使用
InvocationHandler:当代理的方法被调用时,代理就会把这个调用转发给InvocationHandler,但这并不是通过调用IncovationHandler的相应方法做到的,其原理如下
public class OwnerInvocationHandler implements InvocationHandler {
PersonBean person;
public OwnerInvocationHandler(PersonBean person) {
this.person = person;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (method.getName().startsWith("get")) {
return method.invoke(person, args);
} else if (method.getName().equals("setHotOrNotRating")) {
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
创建Proxy类并实例化Proxy对象
public class ProxyFactory {
public static PersonBean getOwnerProxy(PersonBean person) {
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person));
}
}
测试
public class ProxyDriveTest {
public static void main(String[] args) {
PersonBean person = new PersonBeanImpl();
person.setGender("male");
person.setHotOrNotRating(1);
person.setInterests("running");
person.setName("Tom");
PersonBean ownerProxy = ProxyFactory.getOwnerProxy(person);
try{
ownerProxy.setName("Tom1");
System.out.println("Set Success");
} catch (Exception e) {
System.out.println("You can't set");
}
try {
System.out.println(ownerProxy.getGender());
} catch (Exception e) {
System.out.println("You can't get");
}
try {
ownerProxy.setHotOrNotRating(2);
System.out.println("Set Success");
} catch (Exception e) {
System.out.println("You can't change rating");
}
}
}
输出:
Set Successmale
You can’t change rating
可见,权限控制生效,无法修改HotOrNotRating,但其他访问或修改是成功的
其他代理
- 防火墙代理:控制网络资源的访问,保护主题免于“坏客户”的侵害
- 智能引用代理:当主题被引用时,进行额外地动作,例如计算一个对象被引用的次数
- 缓存代理:为开销大的运算结果提供暂时存储;同时也允许多个客户共享结果,以减少计算或网络延迟
- 同步代理:多线程的情况下为主题提供安全访问
- 复杂隐藏代理:用来隐藏一个类的复杂集合的复杂度,并惊醒访问控制,有时也称为外观代理(区分外观模式,外观代理控制访问,外观模式只提供另一组接口)
- 写入时复制代理:用来控制对象的复制,方法是延迟对象的复制,直到客户真正需要为止(虚拟代理的变体)