代理模式应用场景
1、远程代理
远程代理:远程对象的本地代表
-
远程对象:位于不同地址空间的对象。
-
本地代表:由本地方法调用的对象,其行为会转发到远程对象中。
1.1、RMI
本地堆无法获得远程堆的对象,
即变量只能引用本地堆的对象。
示例:变量 machine 无法获得远程堆的对象。
CandyMachine cm1 = <本地堆的对象>; // ok
CandyMachine machine = <远程堆的对象>; // no!
远程方法调用(Remote Method Invocation, RMI)
可以找到远程 JVM 的对象,并调用远程方法。
工作原理
涉及结构
-
客户堆:
- 客户对象:即客户端,需要通过客户辅助对象(代理)间接访问服务对象。
- 客户辅助对象(代理):打包调用信息,通过网络向服务辅助对象发出请求。
-
服务器堆:
- 服务辅助对象:解析客户辅助对象的请求,调用真正的服务对象的方法。
- 服务对象:真正提供方法。
-
按以上顺序反方向返回请求结果。
Java RMI API
提供了客户辅助对象(stub)、服务辅助对象(skeleton)
- 提供所有运行时的基础设施(如绑定 rebind、查找 lookup)。
- 底层使用网络和 I/O,需要处理相应异常。
1.2、实现远程代理
实现步骤
- 定义远程接口:作为 stub(代理)和 服务对象(真实主题) 的共同接口。
- 创建远程实现:即服务对象。
- rmic 产生 Stub 和 Skeleton
- 启动 RMI registry
- 开启远程服务
- 客户获取 Stub 对象,请求执行方法
工作原理
远程接口
继承 Remote 接口(属于标记接口,没有方法)
接口方法的要求:
-
抛出异常:
- 客户通过 stub 访问远程,而 stub 底层使用网络 和 I/O。
- 相关方法需要抛出 RemoteException 异常。
-
变量、返回值:必须是原语或可序列化类型。
-
变量和返回值会被打包和解包,通过网络传输,因此要求可序列化。
-
可以使用 Java API(如原语类型、字符串等),若是自定义类则需实现 Serializable 接口。
public interface MyRemote extends Remote { String hello() throws RemoteException; }
-
远程实现
实现远程接口
-
继承 UnicastRemoteObject:用于处理远程服务。
-
注册 stub 服务:定义方法用于注册 stub 服务。
-
将服务实例化,并使用 RMI Registry 注册;(RMI Registry 需提前运行)
-
使用 rebind() 方法注册。
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote { protected MyRemoteImpl() throws RemoteException { } @Override public String hello() throws RemoteException { return "Hello, I'm server!"; } public static void main(String[] args) { MyRemoteImpl service; try { service = new MyRemoteImpl(); Naming.rebind("Remote Hello", service); } catch (Exception e) { e.printStackTrace(); } } }
-
Stub 和 Skeleton
使用命令行,在远程实现类(即服务对象)中执行 rmic
详细步骤:
-
切换目录:切换到当前项目的 class 文件生成目录。
- 若是 IntelliJ IDEA,class 文件在当前项目的 \target\classes 下。
- 若是 Eclipse,class 文件在当前项目的 \bin 下。
-
执行 rmic 命令:注意使用全限类名
-
生成 stub:
- Java 1.2+:不再需要生成 Skeleton。
- Java 1.5+:不再需要使用 rmic 生成 Stub,通过动态代理生成。
RMI registry
在当前项目的 class 文件生成目录下,
执行 rmiregistry 命令。
远程服务
创建一个类,使用
rebind()
启动服务。
客户请求
使用 RMI 提供的
lookup()
获取 stub。
-
方法参数:
rmi://IP地址/服务名
。 -
返回值:Remote 接口,需要强转为 MyRemote 类型。
public class MyRemoteClient { public static void main(String[] args) { new MyRemoteClient().startClient(); } private void startClient() { try { MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello"); System.out.println(service.hello()); } catch (Exception e) { e.printStackTrace(); } } }
1.3、case:糖果监视器
需求:设计一个监控器(即代理),获取各地糖果机的状态及库存。
1.3.1、准备
CandyMachine 类:糖果机
-
位置:添加 location 变量,对应构造器参数、getter;
-
状态:getState() 方法;
-
库存:getCount() 方法
State 类:状态
重写 toString() 方法,方便调试。
1.3.2、CandyMonitor
-
成员变量:CandyMachine,构造器注入。
-
report():打印所需信息。
public class CandyMonitor { private CandyMachine machine; public CandyMonitor(CandyMachine machine) { this.machine = machine; } public void report() { System.out.println("======= " + machine.getLocation() + " ======="); System.out.println("Stock:\t\t " + machine.getCount()); System.out.println("State:\t\t " + machine.getState()); } }
1.3.3、远程监视器
分析:当前 CandyMachine 和 CandyMonitor 只能在同一个 JVM 上运行。
通过 RMI 实现远程代理。
远程接口
-
继承 Remote 接口
-
接口方法抛出 RemoteException 异常。
public interface MachineRemote extends Remote { String getLocation() throws RemoteException; State getState() throws RemoteException; int getCount() throws RemoteException; }
State 类
-
是自定义类,需要实现 Serializable 接口。
-
CandyMachine 实例不需要被序列化,添加 transient 关键字。
远程实现
-
实现远程接口
-
继承 UnicastRemoteObject
Stub 和 Skeleton
切换目录,执行 rmic 命令,生成 Stub 对象。
RMI registry
在当前项目的 class 文件生成目录下,
执行 rmiregistry 命令。
远程服务
新建一个服务启动类,使用
rebind()
用于启动服务。
客户请求
使用 RMI 提供的
lookup()
获取 stub。
2、虚拟代理
2.1、含义
虚拟代理:创建开销大的对象的代表。
-
在对象被成功创建之前,虚拟代理会代替 RealSubject 处理请求。
-
在对象创建后,虚拟代理将请求委托给 RealSubject。
2.2、case:图片代理
需求:下载并显示图片。
- 使用虚拟代理来代理 ImageIcon 类,管理图片的加载。
- 下载未完成时显示提示信息,下载完成后委托 Icon 类显示图片。
类图
设计一个 ImageProxy,代理 ImageIcon 类。
(其中 Icon 和 ImageIcon 都是 java.swing 提供的 API)
实现
实现 Icon 接口,持有真实主题的引用。
-
成员变量:
- 真实主题:ImageIcon,若值为 null 说明未下载完成。
- URL:图片 URL,构造器注入
- retrieve:标记图片是否已被取出。
-
代理方法:paintIcon() 根据不同场景的操作。
-
下载完成:调用真实主题的方法,即显示图片。
-
未下载完成:判断未取出过图片,新建线程并创建真实主题,调用 repaint() 绘制真正的图片。
public class ImageProxy implements Icon { private ImageIcon imageIcon; private URL imageUrl; private boolean retrieve; public ImageProxy(URL imageUrl) { this.imageUrl = imageUrl; } @Override public void paintIcon(Component c, Graphics g, int x, int y) { if (imageIcon != null) { imageIcon.paintIcon(c, g, x, y); } g.drawString("Loading...", x, y); if (!retrieve) { retrieve = true; Thread retrievalThread = new Thread(() -> { try { imageIcon = new ImageIcon(imageUrl, "Image"); c.repaint(); } catch (Exception e) { e.printStackTrace(); } }); retrievalThread.start(); } } @Override public int getIconWidth() { return imageIcon != null ? imageIcon.getIconWidth() : 800; } @Override public int getIconHeight() { return imageIcon != null ? imageIcon.getIconHeight() : 600; } }
-
3、保护代理
保护代理:基于权限控制对资源的访问,通过动态代理实现。
case:用户评分
需求:模拟一个平台的评分功能。
- 允许查看任何人的资料和评分;
- 允许修改个人资料,不允许修改他人资料;
- 允许对别人评分,不能对自己评分。
结构
-
Person 接口:name 表示个人资料,like 和 dislike 表示评分。
public interface Person { String getName(); int getLike(); int getDislike(); void setName(String name); void setLike(int like); void setDislike(int dislike); }
-
实现类:
public class PersonImpl implements Person { private String name; private int like; private int dislike; // 实现接口方法 @Override public String toString() { return name + ":like=" + like + ", dislike=" + dislike; } }
实现保护代理
为 Person 创建动态代理(此处即保护代理)
设计两个 InvocationHandler,限制访问权限。
- 拥有者代理:用于访问自己的 Person 对象;
- 访问者代理:用于访问其他用户的 Person 对象。
InvocationHandler
拥有者代理
用户要访问自己的 Person 对象时,通过拥有者代理。
-
允许调用 getter,查看任意属性。
-
允许调用 setter,除了 like 和 unlike。
public class OwnerInvocationHandler implements InvocationHandler { private Person person; public OwnerInvocationHandler(Person person) { this.person = person; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (isAccessible(method.getName())) { return method.invoke(person, args); } else { throw new IllegalAccessException("You can't modify your own likes/dislikes"); } } private boolean isAccessible(String methodName) { return !"setLike".equals(methodName) && !"setDislike".equals(methodName); } }
访问者代理
用户要访问其他用户的 Person 对象时,通过访问者代理。
-
允许调用 getter,查看任意属性。
-
允许调用 setter,除了 name。
public class VisitorInvocationHandler implements InvocationHandler { private Person person; public VisitorInvocationHandler(Person person) { this.person = person; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (isAccessible(method.getName())) { return method.invoke(person, args); } else { throw new IllegalAccessException("You can't modify other people's name"); } } private boolean isAccessible(String methodName) { return !"setName".equals(methodName); } }
代理工厂
创建一个静态工厂,用于实例化代理对象。
public class PersonProxyFactory {
public static Person getOwnerProxy(PersonImpl person) {
return (Person) Proxy
.newProxyInstance(person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person));
}
public static Person getVisitorProxy(PersonImpl person) {
return (Person) Proxy
.newProxyInstance(person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new VisitorInvocationHandler(person));
}
}
测试
拥有者
-
调用 setLike() 和 setDislike() 报错;
-
其它方法正常调用。
@Test public void testOwnerProxy() { PersonImpl person = new PersonImpl(); person.setName("Jaywee"); Person ownerProxy = PersonProxyFactory.getOwnerProxy(person); System.out.println(ownerProxy); // ownerProxy.setLike(1000); // ownerProxy.setDislike(0); ownerProxy.setName("SecretMrJ"); System.out.println(ownerProxy); }
访问者
-
调用 setName() 报错;
-
其它方法正常调用。
@Test public void testVisitorProxy() { PersonImpl person = new PersonImpl(); person.setName("Jaywee"); Person visitorProxy = PersonProxyFactory.getVisitorProxy(person); System.out.println(visitorProxy); // visitorProxy.setName("SecretMrJ"); visitorProxy.setLike(98); visitorProxy.setDislike(2); System.out.println(visitorProxy); }