为什么需要代理模式?
有时候我们不能或不想直接访问某个对象。比如对象创建开销很大需要延迟加载,或者需要在访问前做权限检查,或者需要记录访问日志。直接在业务代码中掺杂这些逻辑会让代码臃肿且难以维护。
代理模式通过引入一个中间层,将这些控制逻辑从业务代码中分离出来。客户端代码不需要知道它是在和代理交互还是真实对象交互,两者可以透明替换。
概念
代理模式Proxy Pattern是一种结构型设计模式,核心思想是为一个对象提供一个替身(代理),以控制对这个对象的访问。
代理对象和真实对象实现相同的接口,客户端通过代理间接访问真实对象,代理可以在调用前后添加额外的控制逻辑。
代理模式的主要角色有:
- Subject(抽象主题):声明真实主题和代理共同实现的业务方法,客户端面向该接口编程
- RealSubject(真实主题):定义代理所代表的真实对象,是客户端最终要访问的对象
- Proxy(代理):持有对真实主题的引用,实现与真实主题相同的接口,在调用真实主题前后添加控制逻辑
类图展示了静态结构,但对代理模式这种"调用拦截"的场景,时序图更能体现动态调用流程:
RealSubjectProxyClientRealSubjectProxyClientrequest()前置处理(权限/日志/延迟加载)request()返回结果后置处理(日志/缓存)返回结果
可以把代理理解为私人助理:老板(客户端)有事找某人(真实对象),先通过助理(代理)。助理可以在前面挡掉不重要的打扰(权限控制),也可以在事后记录行程(日志),老板全程不需要直接接触对方。这个比喻贯穿后面的实现章节,方便对照理解。
实现
代理模式的基本实现分为以下几个步骤:
- 定义抽象主题,一般是接口或抽象类,声明真实主题和代理对象实现的业务方法
- 定义真实主题,实现抽象主题中的具体业务
- 定义代理类,包含对
RealSubject的引用,提供和真实主题相同的接口,在调用前后添加控制逻辑 - 客户端使用代理
// 抽象主题 interface Subject { // 声明同业务对象同名的方法 public void request(); } // 真实主题 class RealSubject implements Subject { public void request() { System.out.println("RealSubject request"); } } // 代理类 class Proxy implements Subject { private RealSubject realSubject; @Override public void request() { // 访问真实主题之前:延迟加载 if (realSubject == null) { realSubject = new RealSubject(); } // 调用真实主题的方法 realSubject.request(); // 访问真实主题之后:可添加日志等逻辑 } }总结
代理模式本质上是一层"中间人"——为真实对象提供一个替身,在调用前后添加控制逻辑。
什么时候用:
- 想控制对某个对象的访问(权限、延迟加载、缓存、日志)
- 需要为远程对象提供本地代表
- 引入第三方库或遗留代码,需要统一调用方式
什么时候不用:
- 接口差异巨大,代理会变得臃肿
- 能修改真实对象源码且代价不大,直接修改更简单
- 系统设计阶段就能定义接口规范,从源头统一即可
简单记忆:
代理解决"控制访问"的问题,是给真实对象"加一层控制"。能改源码就改,改不了才用代理。
代理 vs 装饰器 vs 适配器 vs 外观:四个结构型模式都"包了一层对象",结构相似但意图不同:
| 模式 | 接口关系 | 核心意图 |
|---|---|---|
| 代理 | 目标接口 = 被包装对象接口 | 控制访问,附加访问前后逻辑 |
| 装饰器 | 目标接口 = 被包装对象接口 | 增强功能,接口不变 |
| 适配器 | 目标接口 ≠ 被包装对象接口 | 转换接口,让不兼容的类协同 |
| 外观 | 目标接口是新设计的 | 简化复杂子系统的调用 |
口诀对比:代理控访问,装饰增功能,适配改接口,外观简调用。
代理模式 vs 中介者模式
两者都引入"中间层",结构相似,但意图完全不同:
| 维度 | 代理模式 | 中介者模式 |
|---|---|---|
| 核心意图 | 控制对单个对象的访问 | 协调多个对象之间的交互 |
| 对象关系 | 客户端 → 代理 → 真实对象(单向委托) | 多个同事对象 ↔ 中介者 ↔ 多个同事对象(多向协调) |
| 封装内容 | 访问控制逻辑(权限、延迟加载、缓存) | 对象间的交互规则、通信协议 |
| 客户端感知 | 客户端不知道真实对象存在 | 各同事对象知道中介者存在,但不直接知道其他同事 |
| 应用场景 | Spring AOP、MyBatis Mapper、远程调用 | MVC 框架的 Controller、聊天室服务器、GUI 事件分发 |
用例子说明:
- 代理模式:你要见 CEO,先通过秘书(代理)。秘书控制访问——过滤不重要的人、安排时间。你只和秘书打交道,CEO 对你透明。
- 中介者模式:公司的各部门(销售、研发、财务)不直接相互沟通,所有协调通过行政部(中介者)。销售要研发资源,找行政部安排;财务要销售数据,找行政部转发。各部门知道行政部,但不直接依赖其他部门。
简单记忆:
代理管"谁能动",中介者管"怎么联动"。代理是单对象的门禁,中介者是多对象的调度中心。
常见误区:
- 误区:代理模式 = 装饰器模式 → 意图不同:一个控访问,一个增功能
- 误区:代理必须和真实对象同接口 → 对,这是代理的基本要求,否则就不是代理了
- 误区:Spring
@Transactional在同一个类内部调用不生效 → 这是自调用绕过代理的经典坑,AOP 代理需要外部调用才能触发拦截逻辑
练习题目
门禁系统权限控制
题目描述:某科技园区的门禁系统管理着多个房间,每个房间有名称和最低访问权限等级。用户通过门禁终端访问房间,门禁终端作为代理,会检查用户的权限等级:
- 用户权限 ≥ 房间要求的等级 → 放行,房间显示欢迎信息
- 用户权限 < 房间要求的等级 → 拒绝,房间不会响应
请使用代理模式实现该门禁系统。其中:
- Room(真实对象)拥有 enter() 方法,输出欢迎信息
- RoomProxy(代理)在调用 enter() 前进行权限检查,只有通过才委托给真实对象
输入描述:第一行输入一个整数,表示用户的权限等级。第二行输入一个整数 N(1 ≤ N ≤ 20),表示要访问的房间数量。接下来 N 行,每行包含房间名称和该房间要求的最低权限等级,用空格分隔。
输出描述:对每个房间,通过代理访问后输出:
- 权限足够:
欢迎进入[房间名] - 权限不足:
权限不足,无法进入[房间名]
输入示例:
2 3 MeetingRoom 1 Lab 3 Office 2输出示例:
欢迎进入MeetingRoom 权限不足,无法进入Lab 欢迎进入Office解题思路:代理类RoomProxy实现Room接口,内部持有用户权限。在enter()方法中先检查用户权限是否满足房间要求,满足则委托给RealRoom.enter(),否则直接拒绝。这体现了代理模式的核心——代理在调用真实对象之前增加控制逻辑(权限校验),对客户端透明。
import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int qx = sc.nextInt(); int n = sc.nextInt(); RoomProxy proxy = new RoomProxy(qx); while (n-- > 0) { String name = sc.next(); int roomQx = sc.nextInt(); RealRoom room = new RealRoom(name, roomQx); proxy.setRoom(room); proxy.enter(); } } } interface Room { public void enter(); } class RealRoom implements Room { private String name; private int qx; public RealRoom(String name, int qx) { this.name = name; this.qx = qx; } public void enter() { System.out.println("欢迎进入" + this.name); } public String getName() { return this.name; } public int getQx() { return this.qx; } } class RoomProxy implements Room { private int qx; private RealRoom room; public RoomProxy(int qx) { this.qx = qx; } public void setRoom(RealRoom room) { this.room = room; } public void enter() { if (this.qx >= room.getQx()) { room.enter(); } else { System.out.println("权限不足,无法进入" + room.getName()); } } }扩展:实际项目中的代理模式
Spring AOP 的动态代理
Spring AOP 是代理模式最经典的应用。当 Bean 被 AOP 增强时,Spring 不会返回原始对象,而是返回一个代理对象。代理在方法调用前后插入切面逻辑(事务、日志、权限校验等),对调用方完全透明。
// 业务代码:完全感知不到代理的存在 @Service public class OrderService { @Transactional // 事务由代理自动管理 public void createOrder(Order order) { orderDao.save(order); inventoryService.deduct(order.getProductId(), order.getQty()); } } // Spring 内部:创建代理对象(JDK 动态代理或 CGLIB) // 代理在 createOrder 前后自动开启/提交/回滚事务关键点:Spring 默认对实现了接口的 Bean 使用 JDK 动态代理,对没有实现接口的 Bean 使用 CGLIB 代理。开发者只需写@Transactional、@Cacheable等注解,代理负责增强逻辑的织入。
JDK 动态代理 vs CGLIB 对比: