Java设计模式-代理模式
模式概述
代理模式简介
核心思想:通过定义一个代理对象(Proxy),控制对真实对象(RealSubject)的访问。代理对象在真实对象的方法执行前后添加额外逻辑(如权限校验、日志记录、延迟加载等),从而在不修改真实对象的前提下扩展其功能,实现客户端与真实对象的解耦。
模式类型:结构型设计模式(关注对象的组成与交互,通过代理控制访问)。
作用:
- 控制访问:限制真实对象的直接访问(如保护代理仅允许授权用户调用)。
- 功能增强:在真实对象方法前后添加通用逻辑(如日志记录、性能监控)。
- 解耦客户端与真实对象:客户端仅依赖代理接口,无需感知真实对象的存在或修改。
- 延迟加载:虚拟代理可推迟真实对象的创建(如图片懒加载,仅用户查看时加载)。
典型应用场景:
- 远程代理(RPC调用):客户端通过代理对象调用远程服务(如Dubbo的服务消费者代理)。
- 虚拟代理(Lazy Loading):图片/文件懒加载(如网页中图片未滚动到可视区域不加载)。
- 保护代理(Permission Control):接口权限校验(如Spring Security的访问控制代理)。
- 缓存代理(Cache):数据库查询缓存(如MyBatis的一级/二级缓存代理)。
- 智能引用(Smart Reference):对象访问统计(如记录方法调用次数)。
我认为:代理模式是"对象的代言人",在不改变真实对象的前提下,为其添加"保护壳"或"增强功能",让客户端更安全、便捷地使用对象。
课程目标
- 理解代理模式的核心思想和经典应用场景
- 识别应用场景,使用代理模式解决功能要求
- 了解代理模式的优缺点
核心组件
角色-职责表
角色 | 职责 | 示例类名 |
---|---|---|
主题接口(Subject) | 定义真实主题与代理的共同接口,声明需要实现的方法 | UserService |
真实主题(RealSubject) | 实现主题接口,包含具体业务逻辑(被代理的真实对象) | RealUserService |
代理(Proxy) | 实现主题接口,持有真实主题的引用,在方法调用前后添加额外逻辑(如校验、日志) | UserServiceProxy |
类图
下面是一个简化的类图表示,展示了代理模式中的主要角色及其交互方式:
实现 实现 引用 使用 <<interface>> UserService +queryUserInfo(userId: String) RealUserService +queryUserInfo(userId: String) UserServiceProxy -realService: UserService -currentUserRole: String +UserServiceProxy(realService: UserService, currentUserRole: String) +queryUserInfo(userId: String) Client +main(args: String[])
传统实现 VS 代理模式
案例需求
案例背景 :实现一个用户信息服务(UserService
),提供查询用户信息的方法(queryUserInfo
)。要求:仅允许管理员(role=admin
)调用该方法,且调用时记录操作日志。
传统实现(痛点版)
代码实现:
java
// 真实主题:具体实现用户信息查询
class RealUserService{
@Override
public String queryUserInfo(String userId) {
// 模拟数据库查询
return "用户信息:ID=" + userId + ", 姓名=张三, 年龄=25";
}
}
// 传统实现:客户端直接调用真实对象(无权限校验和日志)
public class Client {
public static void main(String[] args) {
UserService realService = new RealUserService();
String userId = "1001";
// 问题1:需在客户端手动添加权限校验(代码冗余)
if (!"admin".equals(getCurrentUserRole())) { // 假设获取当前用户角色
throw new SecurityException("无权限查询用户信息");
}
// 调用真实对象方法
String userInfo = realService.queryUserInfo(userId);
// 问题2:需在客户端手动添加日志记录(重复代码)
System.out.println("日志:用户" + getCurrentUserId() + "查询了用户" + userId + "的信息");
}
// 模拟当前用户信息(假设)
private static String getCurrentUserRole() {
return "guest"; // 测试时改为"admin"可绕过校验
}
private static String getCurrentUserId() {
return "admin_123";
}
}
痛点总结:
- 代码冗余:每个客户端调用真实对象时都需重复编写权限校验、日志记录等逻辑。
- 可维护性差:若需修改校验规则(如新增角色)或日志格式,需同步修改所有客户端代码。
- 真实对象暴露 :客户端直接依赖真实对象(
RealUserService
),可能意外修改其状态或绕过校验逻辑。
代理模式 实现(优雅版)
代码实现:
java
// 1. 主题接口:定义用户信息服务
interface UserService {
String queryUserInfo(String userId);
}
// 2. 真实主题:具体实现用户信息查询(不修改)
class RealUserService implements UserService {
@Override
public String queryUserInfo(String userId) {
// 模拟数据库查询
return "用户信息:ID=" + userId + ", 姓名=张三, 年龄=25";
}
}
// 3. 代理类:控制访问并增强功能(权限校验+日志记录)
class UserServiceProxy implements UserService {
private final UserService realService; // 持有真实主题引用
private final String currentUserRole; // 当前用户角色(可通过构造注入)
public UserServiceProxy(UserService realService, String currentUserRole) {
this.realService = realService;
this.currentUserRole = currentUserRole;
}
@Override
public String queryUserInfo(String userId) {
// 前置逻辑:权限校验
if (!"admin".equals(currentUserRole)) {
throw new SecurityException("无权限查询用户信息");
}
// 调用真实对象方法
String result = realService.queryUserInfo(userId);
// 后置逻辑:日志记录
System.out.println("日志:用户" + getCurrentUserId() + "查询了用户" + userId + "的信息");
return result;
}
// 模拟当前用户ID(假设)
private String getCurrentUserId() {
return "admin_123";
}
}
// 4. 客户端使用:仅依赖代理类
public class Client {
public static void main(String[] args) {
UserService realService = new RealUserService();
UserService proxy = new UserServiceProxy(realService, "admin"); // 注入当前用户角色
try {
String userInfo = proxy.queryUserInfo("1001"); // 客户端仅调用代理方法
System.out.println(userInfo);
} catch (SecurityException e) {
System.out.println("错误:" + e.getMessage());
}
}
}
优势:
- 代码复用:权限校验、日志记录逻辑集中在代理类中,新增客户端无需重复编写。
- 可维护性高 :修改校验规则(如支持
super_admin
角色)或日志格式时,仅需调整代理类。 - 访问控制:客户端仅能通过代理访问真实对象,避免直接修改真实对象状态或绕过校验。
- 解耦客户端与真实对象 :客户端依赖接口(
UserService
),而非具体实现(RealUserService
),符合依赖倒置原则。
局限:
- 类数量增加 :每个需要代理的主题需定义对应的代理类(如
UserServiceProxy
),可能增加系统类膨胀(动态代理可缓解此问题)。 - 代理逻辑侵入:若代理需要添加大量额外逻辑(如多个校验步骤),可能导致代理类代码冗余(可通过组合模式或AOP优化)。
- 性能开销:代理方法调用需经过额外逻辑(如校验、日志),可能轻微影响性能(对性能敏感场景需权衡)。
模式变体
- 静态代理:代理类在编译期手动编写(如上述案例),灵活性低但性能高(无反射开销)。
- 动态代理:运行时通过反射生成代理类(如JDK动态代理、CGLIB),支持为多个接口动态生成代理,减少代码冗余(适用于Spring AOP)。
- 远程代理(Remote Proxy) :封装远程服务调用(如RPC框架中的
Stub
和Skeleton
),隐藏网络通信细节(如Dubbo的服务消费者代理)。 - 虚拟代理(Virtual Proxy):延迟创建真实对象(如图片懒加载,仅用户查看时加载真实图片资源)。
- 保护代理(Protection Proxy) :控制对真实对象的访问权限(如Spring Security的
MethodSecurityInterceptor
)。 - 缓存代理(Cache Proxy):缓存真实对象的方法结果(如MyBatis的二级缓存代理)。
最佳实践
建议 | 理由 |
---|---|
代理类实现主题接口 | 确保代理与真实对象行为一致,客户端无感知(符合里氏替换原则)。 |
代理职责单一 | 仅处理访问控制或功能增强,不添加与核心逻辑无关的业务规则(避免代理类膨胀)。 |
使用动态代理减少冗余 | 对于需要为多个接口生成代理的场景(如Spring AOP),动态代理可避免手动编写多个静态代理类。 |
明确代理的创建方式 | 通过工厂类或依赖注入(如Spring的@Bean )创建代理对象,提高测试灵活性(支持Mock真实对象)。 |
控制代理的粒度 | 避免为单个方法生成代理(如仅对queryUserInfo 添加日志),保持代理的通用性。 |
文档说明代理行为 | 在代理类注释中明确说明其添加的额外逻辑(如"此代理会校验管理员权限并记录日志"),便于维护。 |
一句话总结
代理模式通过引入代理对象控制对真实对象的访问,在不修改真实对象的前提下扩展其功能,是实现访问控制、功能增强和系统解耦的经典工具。
如果关注Java设计模式内容,可以查阅作者的其他Java设计模式系列文章。😊