跳转到内容

代理 (Proxy)

Ken 码农
  • 设计模式
  • 结构型模式

代理 (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 表示要查询成绩的学生学号。

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");
}
}

这里为了突出“代理职责”和“真实业务”的分离,真实主题不关心权限,只关心“给定学号,从数据库查成绩”。


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);
}
}

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 “示例二:学生档案的懒加载代理”

在学生档案管理系统里,一份完整的学生档案可能包含:

  • 基本信息(姓名、学号、学院等)。
  • 较大体量的附件信息(入学材料、成绩单扫描件、奖惩记录等)。

很多场景下只需要展示基本信息,只有在进入某些详情页面时才需要加载完整档案。如果每次都把所有附件从数据库或对象存储中加载出来,会对性能和存储读取造成压力。

可以用代理模式在“第一次真正需要详情时”再去加载完整档案。


/**
* 主题接口:学生档案
*/
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();
}
}

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 接口,是否懒加载由代理内部控制。

  • 权限代理:在访问敏感数据(如学生成绩、奖惩记录)前做统一权限校验和审计。
  • 远程代理:本地代理远程服务(如教务系统对接外部数据平台),对客户端隐藏网络细节。
  • 虚拟代理(懒加载):推迟访问大对象或大数据,如学生档案、历史成绩明细、附件等。
  • 缓存代理:为某些读多写少的查询增加缓存层,例如常用的课程信息、学院列表。

优点

  • 解耦横切逻辑:日志、权限、缓存、懒加载等与业务本身无关的逻辑可以放在代理中,真实主题保持简洁。
  • 客户端透明:客户端只认识接口,不需要关心背后是直连还是走代理。
  • 扩展灵活:可以为同一个真实主题提供多种不同的代理,例如一个是权限代理,一个是缓存代理。

注意事项

  • 代理层数过多会增加调用链复杂度,需要在设计上做好边界。
  • 代理中的横切逻辑如果太复杂,可能需要进一步抽象成统一的拦截器或 AOP 机制。

  • 代理模式通过“和真实对象实现同一接口的代理对象”,在不改变客户端使用方式的前提下,增加访问控制、日志、缓存、懒加载等能力。
  • 在学校的成绩查询、学生档案、对接外部数据服务等场景中,代理模式都很适合承载权限控制和性能优化等横切关注点。