代理模式(proxy)
1. 定义代理模式
代理模式为另一个对象提供了一个替身或占位符以控制对这个对象的访问。
使用代理创建代表(representiative)对象。让代表对象控制某对象的访问,被代理的对象可以是远程对象,创建开销大的对象或需要安全控制的对象。
类图:
RealSubject
和Proxy
实现了共同的接口,这允许任何客户都可以像处理RealSubject
对象一样处理Proxy
对象RealSubjct
是真正做事的对象,它是被Proxy
代理和控制访问的对象。Proxy
拥有RealSubject
的引用,甚至有时候负责RealSubject
对象的创建与销毁。
2. 远程代理(RMI)
2.1 Java RMI 概观
Java RMI,即 远程方法调用(Remote Method Invocation)
,一种用于实现远程过程调用(RPC)(Remote procedure call)
的Java API, 能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于Java虚拟机(JVM)
,因此它仅支持从一个JVM到另一个JVM的调用。
RMI 将客户辅助对象称为stub(桩)
,服务辅助对象称为skeleton(骨架)
。
2.2 RMI 实现
步骤一:定义远程接口
定义要供客户远程调用的方法
- 扩展
java.rmi.Remote
接口,这是一个标记接口。 - 所有远程方法都抛出
RemoteException
异常。 - 返回类型必须可序列化(实现了Serializable接口)或者为原语类型。
// 远程接口 扩展了 java.rmi.Remote接口,这是个标记接口
public interface MyRemote extends Remote {
/**
* 所有的远程方法都必须声明RemoteExeception
* @return 返回值必须是实现了 Serializable 或者 原语类型
* @throws RemoteException 代表是有风险的
*/
public String sayHello() throws RemoteException;
}
步骤二:实现远程接口
- 实现远程接口。
- 扩展
java.rmi.server.UnicastRemoteObject
,让超类帮我们做某些“远程功能”的工作。 - 使用
LocateRegistry.getRegistry()
静态方法获取Registry
。 - 使用
RMI Registry
的bind
、rebind
方法注册服务,端口默认为1099。 - 开启终端。
/**
* 扩展自UnicastRemoteObject,让超类帮我们做"远程"工作
*/
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
// 超类构造器抛出了异常,我们必须处理
protected MyRemoteImpl() throws RemoteException {}
// 不需要声明RemoteException
public String sayHello() {
return "Server says: 'Hey'";
}
public static void main(String[] args) {
try {
MyRemote service = new MyRemoteImpl();
LocateRegistry.createRegistry(1099);
// 注册服务,让客户可查询调用
// 注册到 RMI registry(在服务器上)
Registry registry = LocateRegistry.getRegistry();
registry.bind("RemoteHello", service);
System.out.println("RMI Server ready!");
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
服务端开启成功时:
RMI Server ready!
步骤三:制作客户端
- 根据
ip地址
和port端口
获取指定的RMI Registry
。 - 使用Registry 的
lookup
方法寻找stub
对象(代理对象)。 - 像使用本地对象方法那样调用远程方法。
- 开启另一个终端。
public class MyRemoteClient {
public static void main(String[] args) {
new MyRemoteClient().go();
}
public void go() {
try {
// 需要ip地址或者主机名 和 服务被绑定/重绑定时用的名称
Registry registry = LocateRegistry.getRegistry("localhost");
MyRemote service = (MyRemote)registry.lookup("RemoteHello");
System.out.println(service.sayHello());
} catch (RemoteException e) {
e.printStackTrace();
} catch (NotBoundException e) {
e.printStackTrace();
}
}
}
成功调用远程方法结果:
Server says: 'Hey'
注意:必须先开启服务端,注册Registry服务,即在服务端创建stub类,然后在客户端RMI Registry才能找到相应的服务(stub类)。
2.3 工作方式
3. 虚拟代理(Virtual Proxy)
3.1 概观
虚拟代理:经常直到我们真正需要对象时才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理将请求直接委托给对象。
3.2 设计CD封面虚拟代理
-
需求
-
使用
Swing
创建一个Icon
接口从网络上加载CD封面图片,并且在等待加载时,显示一些东西,加载完成后,图像就显示出来。
设计
-
使用
虚拟代理代理
Icon
,管理加载,在加载未完成时显示“CD封面加载中,请稍后…”,加载完成后,代理把显示的责任 委托给Icon
。
类图:
运作方式:
ImageProxy
创建一个ImageIcon
,然后开始在网络URL上加载图像。- 在加载过程中,
ImageProxy
显示"CD封面加载中,请稍后…"。 - 当加载完成后,
ImageProxy
把所有方法调用委托给真正的ImageIcon。 - 如果用户请求新的图像,我们就创建新的代理,重复这样的操作。
3.3 实现虚拟代理
编写ImageProxy
图像代理类:
public class ImageProxy implements Icon {
ImageIcon imageIcon; // 拥有ImageIcon对象的引用
URL imageURL;
boolean retrieval = false; // 是否取回
public ImageProxy(URL imageURL) {
this.imageURL = imageURL;
}
// 实现Icon接口方法
// 当需要在屏幕上绘制图像时,就会调用该方法
public void paintIcon(final 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+190);
if(!retrieval) { // 还没有取回,创建ImageIcon对象
retrieval = true;
// 为了异步加载,开启一个新的线程去创建一个ImageIcon对象
new Thread(new Runnable() {
public void run() {
try {
// 实例化Icon对象,其构造器会在图像加载完成后返回
imageIcon = new ImageIcon(imageURL, "CD cover");
// 当图像准备好了告诉Swing重绘
c.repaint();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
public int getIconWidth() {
if(imageIcon != null) // 加载完成后
return imageIcon.getIconWidth();
return 800; // 默认宽800px
}
public int getIconHeight() {
if(imageIcon != null) // 加载完成后
return imageIcon.getIconHeight();
return 600; // 默认高600px
}
}
自定义一个图像组件,将方法的处理代理委托给Icon对象:ImageComponent
/**
* 封装ImageProxy,让它是一个组件
*/
public class ImageComponent extends JComponent {
private Icon icon;
public ImageComponent(Icon icon) { this.icon = icon; }
public void setIcon(Icon icon) { this.icon = icon; }
// 当JFrame调用repaint()方法刷新时,该方法被调用
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
int w = icon.getIconWidth();
int h = icon.getIconHeight();
int x = (800 - w) / 2;
int y = (600 - h) / 2;
icon.paintIcon(this, g, x, y);
}
}
构建GUI程序并测试:ImageProxyFrame
、SwingUtil
、ImageProxyTest
/**
* 显示CD图片的代理
* 创建开销大的CD图片对象,并且控制对CD对象的访问
*/
class ImageProxyFrame extends JFrame {
ImageComponent imageComponent;
JMenuBar menuBar;
JMenu menu;
HashMap<String, String> cds = new HashMap<String, String>();
public ImageProxyFrame() throws Exception {
this("CD Cover Viewer");
}
public ImageProxyFrame(String title) throws Exception {
super(title);
cds.put("Buddha Bar", "http://images.amazon.com/images/P/B00009XBYK.01.LZZZZZZZ.jpg");
cds.put("Ima", "http://images.amazon.com/images/P/B000005IRM.01.LZZZZZZZ.jpg");
cds.put("Karma", "http://images.amazon.com/images/P/B000005DCB.01.LZZZZZZZ.gif");
cds.put("MCMXC A.D.", "http://images.amazon.com/images/P/B000002URV.01.LZZZZZZZ.jpg");
cds.put("Northern Exposure", "http://images.amazon.com/images/P/B000003SFN.01.LZZZZZZZ.jpg");
cds.put("Selected Ambient Works, Vol. 2", "http://images.amazon.com/images/P/B000002MNZ.01.LZZZZZZZ.jpg");
// 定义默认图片
URL initialURL = new URL(cds.get("Selected Ambient Works, Vol. 2"));
menuBar = new JMenuBar();
menu = new JMenu("Favorite CDs");
menuBar.add(menu);
setJMenuBar(menuBar);
for (Map.Entry<String, String> cd : cds.entrySet()) {
String cdName = cd.getKey().trim();
JMenuItem menuItem = new JMenuItem(cdName);
menu.add(menuItem);
// 请求新的图像,就创建新的代理
menuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
imageComponent.setIcon(
new ImageProxy(
getCDUrl(e.getActionCommand())));
repaint(); // 重绘
}
});
}
Icon icon = new ImageProxy(initialURL);
imageComponent = new ImageComponent(icon);
// 规范得做法:
// 直接获得JFrame的内容面板,然后添加组件
// 或者把添加一个面板组件JPane,然后添加imageComponent
getContentPane().add(imageComponent);
}
// 从cds拿到url,并处理异常
private URL getCDUrl(String name) {
try {
return new URL(cds.get(name));
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
}
/**
* Swing工具类
* 负责开启专门的线程创建和初始化JFrame程序
*/
class SwingUtil {
public static void run(
final JFrame frame, final int width, final int height) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width, height);
frame.setVisible(true);
}
});
}
}
// 运行
public class ImageProxyTest {
public static void main(String[] args) throws Exception {
SwingUtil.run(new ImageProxyFrame(), 800, 600);
}
}
测试结果:
加载中:
加载后:
4. 保护代理(Access Proxy) – 使用动态代理
4.1 认识动态代理
Java内置了对代理的支持,通过使用java.lang.reflect
包可以创建动态代理,即在运行时创建代理类。
类图:
Proxy
不是我们直接实现的,是Java产生的。InvocationHandler
类是代理的调用处理器,工作是响应Proxy
的任何调用,拥有RealSubject
对象的引用,控制对该对象的访问。
4.2 设计保护控制
设计一个约会系统,用户可以查看约会对象的信息,并且给约会对象评分,但是不允许修改别人的信息,和自己给自己评分,这是不规范的操作。
每个用户都必须实现PersonBean
接口:
// 设置或者获取一个个人信息的接口
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);
}
下面是PersonBean
的实现:
public class PersonBeanImpl implements PersonBean {
private String name;
private String gender;
private String interests;
int rating; // 评级(1-10)
int ratingCount = 0; // 评级人数
// getter 方法
public String getName() { return name; }
public String getGender() { return gender; }
public String getInterests() { return interests; }
public int getHotOrNotRating() {
if(ratingCount == 0) return 0;
return (rating / ratingCount);
}
// setter 方法
public void setName(String name) { this.name = name; }
public void setGender(String gender) { this.gender = gender; }
public void setInterests(String interests) { this.interests = interests; }
public void setHotOrNotRating(int rating) {
this.rating += rating;
ratingCount++;
}
@Override public String toString() {
// 打印用户信息...
}
}
为了实现以上的功能,我们应用保护代理模式,保护控制访问PersonBean
对象,并且使用动态代理技术:
-
步骤一:
-
创建
两个
InvocationHandler
,即需要两个代理。一个控制访问自己,另一个控制访问别人
步骤二:
-
写代理创建动态代理。导入
java.lang.reflect.Proxy
包,使用Proxy.newProxyInstance()
静态方法
步骤三:
-
利用
适当的代理包装任何
PersonBean
对象。即用户自己修改自己的信息那么就使用"拥有者代理",否则使用"非拥有者代理"。
4.3 实现保护代理
分别创建一个访问自己和别人的InvocationHandler,并在invoke
方法实现中进行保护控制:OwnerInvocationHandler
,NonOwnerInvocationHandler
// 给拥有者使用的调用处理器
// 保护控制 --> 不允许自己评级
public class OwnerInvocationHandler implements InvocationHandler {
PersonBean person;
public OwnerInvocationHandler(PersonBean person) {
this.person = person;
}
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();
}
// 调用其他方法也不允许,返回null
return null;
}
}
// 给非拥有者使用的调用处理器
// 保护控制 --> 不允许修改自己的信息
public class NonOwnerInvocationHandler implements InvocationHandler {
PersonBean person;
public NonOwnerInvocationHandler(PersonBean person) {
this.person = person;
}
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"))
return method.invoke(person, args);
else if(method.getName().startsWith("set"))
throw new IllegalAccessException();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
动态创建代理的代码:
// 通过java.util.reflect.Proxy的静态方法newProxyInstance()
// 动态创建Proxy代理对象
// 拥有者代理对象
PersonBean getOwnerProxy(PersonBean person) {
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person));
}
// 非拥有者代理对象
PersonBean getNonOwnerProxy(PersonBean person) {
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new NonOwnerInvocationHandler(person));
}
测试约会系统:MatchMakingTest
package headfirst.designpatterns.proxy.javaproxy;
import java.lang.reflect.Proxy;
import java.util.HashMap;
public class MatchMakingTest {
private HashMap<String, PersonBean> datingDB =
new HashMap<String, PersonBean>();
public MatchMakingTest() {
initializeDatabase();
}
public static void main(String[] args) {
MatchMakingTest test = new MatchMakingTest();
test.drive();
}
// 测试代码在这里...
public void drive() {
PersonBean joe = getPersonFromDatabase("Joe Javabean");
System.out.println("Date information: " + joe + "\n");
// 获得Joe的拥有者代理对象;
PersonBean ownerProxy = getOwnerProxy(joe);
System.out.println("Name is " + ownerProxy.getName());
try {
// 尝试不合规定的操作,看会不会抛出异常?
ownerProxy.setHotOrNotRating(10);
} catch (Exception e) {
System.out.println("Can't set rating from owner proxy");
}
System.out.println("Rating is " + ownerProxy.getHotOrNotRating());
System.out.println();
// 获得Joe的非拥有者代理对象;
PersonBean nonOwnerProxy = getNonOwnerProxy(joe);
System.out.println("Name is " + ownerProxy.getName());
try {
// 尝试不合规定的操作,看会不会抛出异常?
nonOwnerProxy.setInterests("bowing, GO");
} catch (Exception e) {
System.out.println("Can't set interests from non owner proxy");
}
nonOwnerProxy.setHotOrNotRating(3);
System.out.println("Rating set from non owner proxy");
System.out.println("Rating is " + nonOwnerProxy.getHotOrNotRating());
}
// 动态创建Proxy代理对象的方法
// getOwnerProxy,getNotOwnerProxy
// 模拟从数据库中获取数据的方法 getPersonFromDatabase(String name)
// 实例化两个对象,模拟数据库初始化的方法 initializeDatabase()
}
测试结果:
Date information: [Joe Javabean, female, (cars, computers, music), 7]
Name is Joe Javabean
Can't set rating from owner proxy
Rating is 7
Name is Joe Javabean
Can't set interests from non owner proxy
Rating set from non owner proxy
Rating is 5
可见保护代理起作用了,测试对象Joe不能自己修改自己的评分,也不能修改非拥有者代理的对象的信息。
5. 认识其他代理
防火墙代理(FireWall Proxy):控制网络资源的访问,保护主题免于“坏客户”的侵害。
智能引用代理(Smart Reference Proxy):当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数。
缓存代理(Caching Proxy):为开销大的运算结果提供暂时的缓存:它也允许多个用户共享结果,以减少计算或网络延迟。
同步代理(Synchronization Proxy):在多线程的情况下为主题提供安全的访问。
复杂隐藏代理(Complexity Hiding Proxy):用来隐藏一个类的复杂集合的复杂度,并进行控制访问。
写入时复制代理(Copy-On-Write Proxy):用来控制对象的复制,方法是延迟对象的复制,直到客户真正需要为止。这是一个虚拟代理的变体。
6. 要点
- 代理模式为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种。
- 远程代理管理客户的远程对象之间的交互。
- 虚拟代理控制访问实例化开销大的对象。
- 保护代理基于调用者控制对象方法的访问。
- 代理在结构上类似装饰者,但是目的不同。
- Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器。
- 就和其他包装者一样,代理会造成你的设计类的数目增加。