结构型模式-代理模式:替身与本体的博弈
在软件设计与开发中,我们常常会遇到一种情况:客户端需要与一个对象交互,但这个对象由于某些原因(如创建开销大、位于远程、需要安全控制等)不宜或不适合被直接访问。此时,一个优雅的解决方案便应运而生——代理模式。它如同一位精明的“替身演员”,在“本体”对象与客户端之间架起了一座智能的桥梁,掌控着访问的流程。
一、 什么是代理模式?
代理模式是一种结构型设计模式,它为另一个对象提供一个替身或占位符,以控制对这个对象的访问。
这个定义包含了三个核心角色:
- 抽象主题: 声明了真实主题和代理主题的共同接口。这样,任何使用真实主题的地方都可以使用代理主题。
- 真实主题: 定义了代理所代表的真实对象,是业务逻辑的真正执行者,是最终我们引用的“本体”。
- 代理: 持有一个引用,使得代理可以访问真实主题。代理主题与真实主题实现相同的接口,因此可以在任何需要真实主题的地方替代它。它负责在适当的时候创建和删除真实主题对象,并在调用真实主题的前后处理一些额外逻辑。
它们之间的关系,可以用下面的类图清晰地表示:
客户端 --> 抽象主题接口
真实主题 --> 抽象主题接口
代理 --> 抽象主题接口
代理 --> 真实主题
二、 代理模式的三种主要形式
代理模式根据其实现时机和目的,主要分为三种形式:静态代理、动态代理和虚拟代理。
1. 静态代理
静态代理,顾名思义,在编译阶段就已经确定代理关系。我们需要手动为每一个真实主题类编写一个对应的代理类。
代码示例:文件加载器
假设我们有一个文件加载接口 FileLoader
和它的真实实现 RealFileLoader
。现在我们想为文件加载添加权限校验。
// 1. 抽象主题
public interface FileLoader {
String loadFile(String fileName);
}
// 2. 真实主题
public class RealFileLoader implements FileLoader {
@Override
public String loadFile(String fileName) {
System.out.println("正在加载文件: " + fileName);
// 模拟耗时的文件加载操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "文件: " + fileName + " 的内容";
}
}
// 3. 代理
public class FileLoaderProxy implements FileLoader {
private RealFileLoader realFileLoader;
private String userRole;
public FileLoaderProxy(String userRole) {
this.userRole = userRole;
}
@Override
public String loadFile(String fileName) {
// 前置处理:权限控制
if (!"admin".equals(userRole)) {
System.out.println("权限不足,拒绝访问文件。");
return null;
}
// 延迟初始化:在需要时才创建真实对象
if (realFileLoader == null) {
realFileLoader = new RealFileLoader();
}
// 调用真实对象的方法
String content = realFileLoader.loadFile(fileName);
// 后置处理:记录日志(这里只是示例,未实际记录)
System.out.println("文件加载操作已完成。");
return content;
}
}
// 4. 客户端使用
public class Client {
public static void main(String[] args) {
// 使用代理访问
FileLoader loader = new FileLoaderProxy("user"); // 普通用户
String content = loader.loadFile("confidential.txt");
System.out.println(content); // 输出:权限不足,拒绝访问文件。 null
FileLoader adminLoader = new FileLoaderProxy("admin"); // 管理员
content = adminLoader.loadFile("confidential.txt");
System.out.println(content); // 输出:正在加载文件: confidential.txt ... 文件: confidential.txt 的内容
}
}
优缺点分析:
- 优点:在不修改真实主题类的情况下,通过代理类扩展了功能(权限校验、日志等),符合开闭原则。
- 缺点:如果真实主题接口有很多方法,代理类也需要实现所有方法,即使有些方法不需要增强,会导致代码冗余。并且,每个真实主题都需要一个对应的代理类,当系统规模变大时,类的数量会急剧增加。
2. 动态代理
动态代理解决了静态代理的缺点。它不是在编译期创建代理类,而是在运行时动态生成。在Java中,主要有两种实现方式:基于JDK的原生动态代理和基于CGLIB的字节码增强。
JDK动态代理示例
JDK动态代理要求真实主题必须实现至少一个接口。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 动态代理的调用处理器
public class FileLoaderInvocationHandler implements InvocationHandler {
private Object realTarget;
private String userRole;
public FileLoaderInvocationHandler(Object realTarget, String userRole) {
this.realTarget = realTarget;
this.userRole = userRole;
}
// 创建代理对象
public static Object createProxy(Object target, String userRole) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new FileLoaderInvocationHandler(target, userRole)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强:权限校验
if ("loadFile".equals(method.getName()) && !"admin".equals(userRole)) {
System.out.println("[动态代理] 权限不足,拒绝访问文件。");
return null;
}
// 调用真实对象的方法
Object result = method.invoke(realTarget, args);
// 后置增强:记录日志
System.out.println("[动态代理] 方法 " + method.getName() + " 被调用。");
return result;
}
}
// 客户端使用
public class DynamicProxyClient {
public static void main(String[] args) {
FileLoader realLoader = new RealFileLoader();
// 创建动态代理
FileLoader proxyLoader = (FileLoader) FileLoaderInvocationHandler.createProxy(realLoader, "user");
proxyLoader.loadFile("test.txt"); // 输出:[动态代理] 权限不足,拒绝访问文件。
FileLoader adminProxyLoader = (FileLoader) FileLoaderInvocationHandler.createProxy(realLoader, "admin");
adminProxyLoader.loadFile("test.txt");
// 输出:
// 正在加载文件: test.txt
// [动态代理] 方法 loadFile 被调用。
}
}
优缺点分析:
- 优点:非常灵活,一个
InvocationHandler
可以代理多种类型的对象,大大减少了代码量。 - 缺点:只能代理实现了接口的类。
3. 虚拟代理
虚拟代理是一种用于延迟初始化(懒加载)的代理。当创建一个对象的开销非常大时,虚拟代理可以延迟对象的实际创建,直到真正需要它的时候。
典型场景:加载高分辨率图片。在图片完全加载前,先用一个占位图(代理)显示,同时在后台加载真实图片,加载完毕后再替换。
// 抽象主题
public interface Image {
void display();
}
// 真实主题 - 高开销对象
public class HighResolutionImage implements Image {
private String filename;
public HighResolutionImage(String filename) {
this.filename = filename;
loadImageFromDisk(); // 模拟昂贵的初始化过程
}
private void loadImageFromDisk() {
System.out.println("从磁盘加载高分辨率图片: " + filename + ",这需要很长时间...");
try {
Thread.sleep(2000); // 模拟加载耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("图片加载完成!");
}
@Override
public void display() {
System.out.println("显示图片: " + filename);
}
}
// 虚拟代理
public class ImageProxy implements Image {
private String filename;
private HighResolutionImage realImage; // 开始时为null
public ImageProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
System.out.println("【代理】图片预览区域已准备就绪。");
// 只有当真正需要显示时,才创建真实对象
if (realImage == null) {
realImage = new HighResolutionImage(filename);
}
// 委托给真实对象
realImage.display();
}
}
// 客户端使用
public class VirtualProxyClient {
public static void main(String[] args) {
System.out.println("程序启动...");
Image image = new ImageProxy("photo_10MB.jpg");
System.out.println("--- 进行其他一些操作 ---");
// 此时真实的HighResolutionImage还未被创建
System.out.println("--- 第一次调用display ---");
image.display(); // 此时才会创建真实对象并加载图片
System.out.println("--- 第二次调用display ---");
image.display(); // 真实对象已存在,直接使用
}
}
三、 代理模式的应用场景
代理模式在现实世界的软件开发中无处不在:
- 远程代理:为一个对象在不同的地址空间提供局部代表。例如,RMI(Remote Method Invocation)和各种RPC框架的客户端存根就是远程代理。
- 虚拟代理:如上例所示,用于延迟创建开销很大的对象。
- 保护代理:控制对原始对象的访问,用于对象应该有不同的访问权限的时候。如上文中的权限校验例子。
- 智能引用代理:在访问对象时执行附加操作,例如:
- 计算一个对象的引用次数。
- 当第一次引用一个持久对象时,将它装入内存。
- 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。
- 缓存代理:为开销大的运算结果提供临时存储,允许多个客户端共享结果以减少计算或网络延迟。
- Spring AOP:Spring框架的面向切面编程,其底层大量使用了动态代理(JDK Proxy或CGLIB)来实现事务管理、日志、安全等横切关注点的织入。
四、 总结
代理模式通过引入一个“替身”,巧妙地实现了对“本体”访问的控制和增强。它完美地体现了面向对象设计中的“单一职责原则”和“开闭原则”,将核心业务逻辑与周边的系统服务(如安全、日志、延迟加载)解耦。
- 静态代理简单直观,但灵活性差,适用于代理类较少的情况。
- 动态代理灵活强大,极大地减少了冗余代码,是现代框架(如Spring)的基石。
- 虚拟代理是性能优化的利器,通过延迟加载提升了系统响应速度。
理解并熟练运用代理模式,能够让你在设计系统时更加游刃有余,构建出更加灵活、健壮和高效的软件架构。在这场“替身”与“本体”的博弈中,开发者正是那位运筹帷幄的导演。