一、代理模式简介
代理模式(Proxy Pattern) 是在软件开发中常用的结构型设计模式之一,其主要思想是:在访问真实对象(Real Subject)时,提供一个代理(Proxy)对象来控制对真实对象的访问、增强或隔离某些功能,从而使客户端与真实对象之间的交互更灵活、更安全。
简单来说,代理 就像一个中间人或门卫,客户端想访问目标对象时,不是直接访问,而是通过代理去访问,从而在这个过程中,代理可以进行一些额外的处理,如权限控制、缓存、延迟加载、日志记录等。
1. 代理模式的动机
- 某些操作需要访问外部资源、远程服务、数据库等,直接访问开销大或存在延迟,需要缓存或延迟加载。
- 某些操作需要进行权限控制、流量过滤、资源管控或审计,必须经过一个“把关者”来决定是否继续调用真实对象。
- 需要对原有业务逻辑进行扩展或增强(如添加统计、日志、事务等),但不希望或无法直接修改原有实现。
2. 代理模式的优点
- 隔离复杂度:客户端无需知道如何与复杂的或远程的对象进行交互,从而减轻了客户端的负担。
- 增强功能:可以在不修改真实对象的前提下,为调用过程添加额外的处理逻辑,如权限控制、缓存、日志等。
- 灵活性和可扩展性:代理对象与真实对象可以分别独立开发、维护;若要在访问真实对象前后增加功能,只需修改代理类。
3. 代理模式的缺点
- 增加系统复杂度:需要额外创建代理对象,导致类的数量增加。
- 客户端延迟:如果引入了远程代理或一些复杂的代理逻辑,可能会影响系统性能。
二、代理模式结构
在 UML 类图中,代理模式的关键角色主要包括:
-
Subject(抽象主题接口)
- 定义了代理对象与真实对象需要实现的公共接口,通常包括客户端会直接调用的方法。
-
RealSubject(真实主题或真实对象)
- 实际提供业务逻辑或核心功能的类。
-
Proxy(代理类)
- 持有对真实对象的引用,实现与 Subject 相同的接口,以便在客户端与真实对象之间进行中介或控制。
- 可以在调用真实对象之前或之后添加自定义逻辑,例如权限控制、延迟加载、日志记录、性能监控等。
其示意图如下:
三、代理模式的应用场景
-
远程(Remote)代理
- 当真实对象在远程服务器上时,可通过一个本地的代理对象来代表这个远程对象,客户端程序无需关心底层通信细节(RMI、Web Service、Socket 等)。
-
虚拟(Virtual)代理
- 用于惰性加载、延迟创建开销较大的对象。例如,程序需要展示某些大图片、视频等资源时,可以先用代理对象代替真实对象,只有在需要时才真正加载。
-
保护(Protection)代理
- 做权限控制,避免一些不当访问。
-
缓存(Cache)代理
- 将从真实对象获取到的结果缓存起来,减少对真实对象的调用次数,提高访问效率。
-
日志/统计(Logging/Monitoring)代理
- 在访问真实对象前后插入日志、调用次数统计、性能监控等增强功能。
-
智能引用(Smart Reference)代理
- 代理除了管理对真实对象的引用,还可能负责引用计数、回调处理等。
四、代理模式实战思路
在项目中使用代理模式主要考虑以下步骤:
-
定义接口或抽象类(Subject)
- 抽象出真实对象的核心操作,让代理类和真实类都实现(或继承)此接口(或抽象类)。
-
实现真实类(RealSubject)
- 维护核心功能或业务逻辑,完成系统所需的实际操作。
-
实现代理类(Proxy)
-
持有对真实对象的引用,并在调用真实对象前后进行额外的处理,例如:
- 权限校验
- 缓存或延迟加载
- 日志、统计、监控
- 资源管理
-
-
客户端调用(Client)
- 客户端只需要面向抽象编程,直接通过 Subject 接口来调用代理对象的功能,实际逻辑由代理决定是否转发给真实对象或做其它处理。
五、示例代码
下面给出两个示例,示例1:静态代理,示例2:动态代理。
示例1:静态代理
1. 定义抽象主题接口
public interface UserService {
void createUser(String username);
String getUser(String username);
}
2. 实现真实主题(真实对象)
public class UserServiceImpl implements UserService {
@Override
public void createUser(String username) {
System.out.println("创建用户:" + username);
}
@Override
public String getUser(String username) {
System.out.println("查询用户信息:" + username);
return "UserInfo(" + username + ")";
}
}
3. 实现代理类
public class UserServiceProxy implements UserService {
private final UserService realService;
public UserServiceProxy(UserService realService) {
this.realService = realService;
}
@Override
public void createUser(String username) {
// 在调用真实方法前可进行权限控制、日志记录等
System.out.println("[Proxy] 开始创建用户,进行权限校验...");
realService.createUser(username);
System.out.println("[Proxy] 创建用户结束,记录日志...");
}
@Override
public String getUser(String username) {
System.out.println("[Proxy] 准备查询用户,进行缓存或权限校验...");
String userInfo = realService.getUser(username);
System.out.println("[Proxy] 用户查询结束,记录日志...");
return userInfo;
}
}
4. 客户端测试
public class Client {
public static void main(String[] args) {
UserService realService = new UserServiceImpl();
UserService proxy = new UserServiceProxy(realService);
proxy.createUser("Tom");
String userInfo = proxy.getUser("Jerry");
System.out.println("获取到的用户信息:" + userInfo);
}
}
运行效果:
[Proxy] 开始创建用户,进行权限校验...
创建用户:Tom
[Proxy] 创建用户结束,记录日志...
[Proxy] 准备查询用户,进行缓存或权限校验...
查询用户信息:Jerry
[Proxy] 用户查询结束,记录日志...
获取到的用户信息:UserInfo(Jerry)
从输出可以看到,通过代理类,我们在调用真实业务方法前后,添加了权限校验、日志等功能。
示例2:动态代理(JDK 动态代理)
在 Java 中,静态代理需要手动编写代理类,一旦接口方法较多或接口随时变化,需要不断改动代理类。JDK 动态代理可以在运行时动态生成代理类,减少大量冗余代码。
1. 定义抽象主题接口
(与示例1 相同,这里直接沿用 UserService)
public interface UserService {
void createUser(String username);
String getUser(String username);
}
2. 实现真实主题
(与示例1 相同,这里直接沿用 UserServiceImpl)
public class UserServiceImpl implements UserService {
@Override
public void createUser(String username) {
System.out.println("创建用户:" + username);
}
@Override
public String getUser(String username) {
System.out.println("查询用户信息:" + username);
return "UserInfo(" + username + ")";
}
}
3. 代理处理器(InvocationHandler)
通过实现 InvocationHandler 接口,编写调用处理逻辑:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserServiceInvocationHandler implements InvocationHandler {
private final Object target;
public UserServiceInvocationHandler(Object target) {
this.target = target; // target 通常是 RealSubject
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 这里可以在调用真实对象方法之前做一些操作
System.out.println("[DynamicProxy] 调用方法:" + method.getName() + ",开始处理前置逻辑...");
// 调用真实对象方法
Object result = method.invoke(target, args);
// 这里可以在调用真实对象方法之后做一些操作
System.out.println("[DynamicProxy] 调用方法:" + method.getName() + ",处理后置逻辑...");
return result;
}
}
4. 客户端利用 JDK 动态代理创建代理对象
import java.lang.reflect.Proxy;
public class Client {
public static void main(String[] args) {
// 1. 创建真实对象
UserService realService = new UserServiceImpl();
// 2. 创建 InvocationHandler 对象
UserServiceInvocationHandler handler = new UserServiceInvocationHandler(realService);
// 3. 使用 JDK Proxy 动态创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
realService.getClass().getInterfaces(),
handler);
// 4. 调用代理对象的方法
proxy.createUser("Tom");
String userInfo = proxy.getUser("Jerry");
System.out.println("获取到的用户信息:" + userInfo);
}
}
运行效果:
[DynamicProxy] 调用方法:createUser,开始处理前置逻辑...
创建用户:Tom
[DynamicProxy] 调用方法:createUser,处理后置逻辑...
[DynamicProxy] 调用方法:getUser,开始处理前置逻辑...
查询用户信息:Jerry
[DynamicProxy] 调用方法:getUser,处理后置逻辑...
获取到的用户信息:UserInfo(Jerry)
通过动态代理,我们就无需再手动编写代理类 UserServiceProxy,大大减少了代理类的编写工作量。同时也可以根据需求,对各个接口方法进行统一或不同的前后置处理。
六、总结
- 代理模式的核心思想是:让代理对象(Proxy)控制或者增强对真实对象(Real Subject)的访问。
- 典型应用场景包括远程代理、虚拟代理、保护代理、缓存代理、日志/监控代理等。
- 实际项目中,静态代理适合接口或功能比较稳定、调用方法不多的场景;动态代理更适合接口方法可能不断演进或者对于多种接口都想进行代理处理的情况。
- 引入代理后,可以不修改原有代码就实现对业务方法的扩展或增强,提高系统的灵活性与可维护性。
当然,当我们需要在访问真实对象前后,或在访问过程中插入一些额外操作(权限校验、缓存、日志、事务、限流等)时,便可考虑采用代理模式来提高代码的可维护性和扩展性。
交流学习
最后,如果这篇文章对你有所启发,请帮忙转发给更多的朋友,让更多人受益!如果你有任何疑问或想法,欢迎随时留言与我讨论,我们一起学习、共同进步。别忘了关注我,我将持续分享更多有趣且实用的技术文章,期待与你的交流!