代理 (Proxy) 是一种结构型设计模式,通过引入一个“代理对象”,在客户端和真实对象之间加一层控制。
客户端不直接操作真实对象,而是通过代理来访问它。代理可以在调用前后插入权限校验、日志、缓存、懒加载等逻辑,但对客户端来说,看起来和直接调用真实对象是一样的。
- 有些对象创建或调用成本较高,希望在真正需要时再创建(懒加载)。
- 某些操作需要权限控制、操作审计或统一日志,又不想把这些“横切逻辑”散落在各个业务类中。
- 希望客户端仍然通过一套稳定的接口来使用服务,而不需要关心背后是直接调用真实对象,还是经过了某种“包装”和增强。
- 提炼出统一的主题接口 (Subject),真实对象和代理对象都实现这份接口。
- 真实主题 (RealSubject) 专注于业务本身,比如访问数据库、调用第三方接口等。
- 代理 (Proxy) 同样实现主题接口,但内部持有真实对象引用,在接口方法中先执行附加逻辑(权限、日志、懒加载、缓存等),再把调用委托给真实对象。
- 客户端只依赖主题接口,可以在“是否走代理”这件事上保持透明。
classDiagram
class Subject {
<<interface>>
+request()* void
}
class RealSubject {
+request() void
}
class Proxy {
-realSubject RealSubject
+request() void
}
class Client
Subject <|.. RealSubject
Subject <|.. Proxy
Proxy o-- RealSubject
Client ..> Subject : uses
示例一:成绩查询服务的权限代理
Section titled “示例一:成绩查询服务的权限代理”在学校的成绩管理系统中,有一个核心服务 ScoreQueryService,可以按照学号查询学生成绩。
- 教师可以查询自己任课班级学生的成绩。
- 学生只能查询自己的成绩。
- 管理员可以查询任意学生的成绩。
如果把权限判断逻辑全部写在 ScoreQueryService 里,这个类会快速膨胀;如果在每个调用处都写权限判断,又容易重复且出错。可以通过代理模式,把权限控制集中放在一个代理里。
1. 主题接口:统一的成绩查询服务
Section titled “1. 主题接口:统一的成绩查询服务”import java.util.List;
/** * 主题接口:成绩查询服务 */public interface ScoreQueryService {
List<String> queryScores(String operatorId, String targetStudentId);}说明:
operatorId表示当前操作人(可能是老师、学生、管理员的账号)。targetStudentId表示要查询成绩的学生学号。
2. 真实主题:只关注业务查询
Section titled “2. 真实主题:只关注业务查询”import java.util.List;
/** * 真实主题:只负责从数据库查询成绩 * 假设这里已完成权限验证,只根据 studentId 查数据 */public class ScoreQueryServiceImpl implements ScoreQueryService {
@Override public List<String> queryScores(String operatorId, String targetStudentId) { // 实际项目中这里会访问数据库,例如: // SELECT course_name, score FROM t_score WHERE student_id = ? System.out.println("从数据库查询学号 " + targetStudentId + " 的成绩"); return List.of("高等数学: 90", "计算机基础: 95"); }}这里为了突出“代理职责”和“真实业务”的分离,真实主题不关心权限,只关心“给定学号,从数据库查成绩”。
3. 代理:权限校验和审计日志
Section titled “3. 代理:权限校验和审计日志”import java.time.LocalDateTime;import java.util.List;
/** * 代理:在调用真实服务前后增加权限校验和审计日志 */public class ScoreQueryServiceProxy implements ScoreQueryService {
private final ScoreQueryService target; private final RoleService roleService;
public ScoreQueryServiceProxy(ScoreQueryService target, RoleService roleService) { this.target = target; this.roleService = roleService; }
@Override public List<String> queryScores(String operatorId, String targetStudentId) { // 1. 权限校验 if (!roleService.canQueryScore(operatorId, targetStudentId)) { throw new SecurityException("当前用户无权查询该学生的成绩"); }
// 2. 记录审计日志 System.out.println(LocalDateTime.now() + " 操作人 " + operatorId + " 查询学生 " + targetStudentId + " 成绩");
// 3. 委托真实服务查询 List<String> scores = target.queryScores(operatorId, targetStudentId);
// 4. 如有需要,可在这里对结果做脱敏、过滤等处理 return scores; }}/** * 简化版角色服务,用于判断是否有权限查询 */public class RoleService {
public boolean canQueryScore(String operatorId, String targetStudentId) { // 真实场景中会根据角色、课程、班级等信息判断 // 这里只是示意:假设 operatorId 等于 targetStudentId 时认为是学生查自己 return operatorId.equals(targetStudentId); }}4. 客户端:只依赖统一接口
Section titled “4. 客户端:只依赖统一接口”import java.util.List;
public class ScoreClient {
public static void main(String[] args) { ScoreQueryService realService = new ScoreQueryServiceImpl(); RoleService roleService = new RoleService(); ScoreQueryService proxy = new ScoreQueryServiceProxy(realService, roleService);
String studentId = "20230001";
// 场景一:学生自己查询自己的成绩(通过) List<String> scores = proxy.queryScores(studentId, studentId); System.out.println(scores);
// 场景二:尝试用另一个学生账号查询(会抛异常) proxy.queryScores("20230002", studentId); }}在这个示例中:
- 客户端始终面对的是
ScoreQueryService接口。 - 是否走代理、代理里加了哪些逻辑,对客户端来说是透明的。
- 权限控制、审计日志都被集中放在了代理中,方便后续统一调整。
示例二:学生档案的懒加载代理
Section titled “示例二:学生档案的懒加载代理”在学生档案管理系统里,一份完整的学生档案可能包含:
- 基本信息(姓名、学号、学院等)。
- 较大体量的附件信息(入学材料、成绩单扫描件、奖惩记录等)。
很多场景下只需要展示基本信息,只有在进入某些详情页面时才需要加载完整档案。如果每次都把所有附件从数据库或对象存储中加载出来,会对性能和存储读取造成压力。
可以用代理模式在“第一次真正需要详情时”再去加载完整档案。
1. 主题接口:学生档案
Section titled “1. 主题接口:学生档案”/** * 主题接口:学生档案 */public interface StudentProfile {
String getBasicInfo();
String getFullProfile(); // 可能包含大字段、附件等}2. 真实主题:实际从数据库或存储加载档案
Section titled “2. 真实主题:实际从数据库或存储加载档案”/** * 真实主题:从存储加载完整档案 */public class StudentProfileImpl implements StudentProfile {
private final String studentId;
// 下面这些字段可能来自多张表或对象存储 private String basicInfo; private String fullProfile;
public StudentProfileImpl(String studentId) { this.studentId = studentId; loadFromStorage(); }
private void loadFromStorage() { // 模拟从数据库 / 对象存储加载完整档案 System.out.println("从存储加载学号 " + studentId + " 的完整档案"); this.basicInfo = "学号 " + studentId + ",姓名 张三"; this.fullProfile = "完整档案信息(包含基本信息、成绩记录、奖惩记录、附件链接等)"; }
@Override public String getBasicInfo() { return basicInfo; }
@Override public String getFullProfile() { return fullProfile; }}3. 代理:按需创建真实档案对象
Section titled “3. 代理:按需创建真实档案对象”/** * 懒加载代理:延迟创建真实的 StudentProfileImpl */public class LazyStudentProfileProxy implements StudentProfile {
private final String studentId; private StudentProfileImpl realProfile;
public LazyStudentProfileProxy(String studentId) { this.studentId = studentId; }
@Override public String getBasicInfo() { // 这里也可以选择提前加载,示例中直接返回占位信息 return "学号 " + studentId + " 的基本信息(可从轻量接口或缓存获取)"; }
@Override public String getFullProfile() { if (realProfile == null) { realProfile = new StudentProfileImpl(studentId); } return realProfile.getFullProfile(); }}4. 使用懒加载代理
Section titled “4. 使用懒加载代理”public class StudentProfileClient {
public static void main(String[] args) { StudentProfile profile = new LazyStudentProfileProxy("20230001");
// 场景一:列表页只展示简要信息,不触发完整加载 System.out.println(profile.getBasicInfo());
// 场景二:进入详情页时才需要完整档案,此时才真正加载 System.out.println(profile.getFullProfile()); }}在这个示例中:
- 代理对象先以极低成本提供基本信息展示。
- 当客户端真正调用
getFullProfile时,才会创建StudentProfileImpl并从存储加载完整档案。 - 对外暴露的仍然是统一的
StudentProfile接口,是否懒加载由代理内部控制。
- 权限代理:在访问敏感数据(如学生成绩、奖惩记录)前做统一权限校验和审计。
- 远程代理:本地代理远程服务(如教务系统对接外部数据平台),对客户端隐藏网络细节。
- 虚拟代理(懒加载):推迟访问大对象或大数据,如学生档案、历史成绩明细、附件等。
- 缓存代理:为某些读多写少的查询增加缓存层,例如常用的课程信息、学院列表。
优点与注意事项
Section titled “优点与注意事项”优点
- 解耦横切逻辑:日志、权限、缓存、懒加载等与业务本身无关的逻辑可以放在代理中,真实主题保持简洁。
- 客户端透明:客户端只认识接口,不需要关心背后是直连还是走代理。
- 扩展灵活:可以为同一个真实主题提供多种不同的代理,例如一个是权限代理,一个是缓存代理。
注意事项
- 代理层数过多会增加调用链复杂度,需要在设计上做好边界。
- 代理中的横切逻辑如果太复杂,可能需要进一步抽象成统一的拦截器或 AOP 机制。
- 代理模式通过“和真实对象实现同一接口的代理对象”,在不改变客户端使用方式的前提下,增加访问控制、日志、缓存、懒加载等能力。
- 在学校的成绩查询、学生档案、对接外部数据服务等场景中,代理模式都很适合承载权限控制和性能优化等横切关注点。