什么是代理模式?它解决什么问题?
简化版
代理模式是给目标对象提供一个代理对象,由代理对象控制对目标对象的访问,并在调用前后增加额外逻辑。它主要解决直接访问目标对象不方便、不安全、成本高,或者需要统一增强的问题。
详细版
代理模式的核心结构一般包括:
Subject:抽象主题,定义目标对象和代理对象共同的接口;RealSubject:真实主题,真正执行业务逻辑;Proxy:代理对象,持有真实对象引用,并在调用前后做增强。
典型代码:
interface UserService {
void createUser(String name);
}
class UserServiceImpl implements UserService {
public void createUser(String name) {
System.out.println("创建用户:" + name);
}
}
class UserServiceProxy implements UserService {
private final UserService target;
UserServiceProxy(UserService target) {
this.target = target;
}
public void createUser(String name) {
System.out.println("权限校验");
target.createUser(name);
System.out.println("记录日志");
}
}
调用方使用代理:
UserService service = new UserServiceProxy(new UserServiceImpl());
service.createUser("Tom");
常见应用场景有:
- 权限校验;
- 日志记录;
- 事务控制;
- 缓存;
- 延迟加载;
- 远程调用;
- 监控埋点;
- Spring AOP。
面试时要强调:代理模式的重点不是“多包一层”,而是在不修改目标类的前提下控制访问、添加增强、隔离复杂逻辑。
完整版教学
一、为什么需要代理模式
假设业务中有一个用户服务:
userService.createUser("Tom");
一开始它只负责创建用户。后来需求变多了:
- 创建前要鉴权;
- 创建后要记录操作日志;
- 方法耗时要上报监控;
- 异常要统一转换;
- 某些场景要走缓存;
- 远程调用失败要重试。
如果把这些逻辑全部塞进 UserServiceImpl,业务类会越来越臃肿。它不再只关心“创建用户”,还要关心权限、日志、监控、异常、缓存。
代理模式的思路是:业务逻辑仍然放在真实对象里,访问控制和横切增强放在代理对象里。
这就是代理模式的第一层价值:让目标类专注业务,让代理类负责访问过程。
二、代理模式的核心思想
代理模式不是替代真实对象,而是“代替调用方去访问真实对象”。
调用链可以理解为:
客户端 -> 代理对象 -> 真实对象
客户端以为自己在调用 UserService,实际上调用的是代理对象。代理对象可以选择:
- 是否允许访问;
- 何时访问;
- 调用前做什么;
- 调用后做什么;
- 异常时怎么处理;
- 是否直接返回缓存结果;
- 是否把请求转发到远程机器。
因此代理模式非常适合处理“访问过程”相关的问题。
三、代理模式和直接调用的区别
直接调用:
target.createUser("Tom");
代理调用:
proxy.createUser("Tom");
看起来只是多了一层,但设计意义完全不同。
直接调用时,客户端和真实对象强耦合。所有增强逻辑要么写在客户端,要么写在真实对象里。
代理调用时,客户端依赖的是统一接口,真实对象藏在代理后面。代理可以在不改客户端和真实对象主体逻辑的情况下扩展访问行为。
这也是 Spring AOP、MyBatis Mapper、RPC 框架大量使用代理思想的原因。
四、代理模式的常见类型
代理模式可以按目的分成很多类:
| 类型 | 作用 |
|---|---|
| 静态代理 | 手写代理类,结构清楚但类数量多 |
| 动态代理 | 运行期生成代理对象,减少手写代理类 |
| 远程代理 | 把本地方法调用转成远程网络调用 |
| 虚拟代理 | 延迟创建成本高的对象 |
| 保护代理 | 做权限控制 |
| 缓存代理 | 避免重复访问目标对象 |
| 智能引用代理 | 增加计数、监控、日志等 |
面试中最常考的是静态代理、JDK 动态代理、CGLIB 动态代理和 Spring AOP。
五、代理模式的边界
代理模式适合“访问控制和增强”,但不适合把业务流程都塞进代理。
如果代理对象里写了大量业务分支:
if (vip) {
// 一套业务流程
} else {
// 另一套业务流程
}
那可能不是代理模式要解决的问题,而是策略、模板方法、责任链等模式更合适。
代理应该增强访问过程,而不是取代真实对象成为新的业务大杂烩。
六、访问链是否真正可控
一次创建用户耗时 80 ms,鉴权 2 ms、日志 3 ms,代理后的同步延迟约为 85 ms;这 5 ms 是访问治理的显式成本。
Client -> Proxy(鉴权 2ms) -> RealSubject(80ms) -> Proxy(日志 3ms)
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 把真实对象藏在代理后面,不等于可以忽略增强带来的延迟和失败语义。 |
| 适用边界 | 代理应控制访问过程,若大量核心业务分支进入代理,应转向策略、模板方法或责任链。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:把真实对象藏在代理后面,不等于可以忽略增强带来的延迟和失败语义。
八、常见误区与追问
- 误区:代理对象会取代真实对象执行业务。 代理只决定是否、何时以及怎样转发,核心业务仍由 RealSubject 完成。
- 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:代理模式最稳定的判断标准是什么? 看它是否为真实对象提供替身并控制访问,而不是仅看是否出现包装结构。
- 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
代理模式的记忆锚点是:客户端不直接碰真实对象,而是先经过代理。代理负责权限、日志、事务、缓存、远程调用等访问过程,真实对象继续专心做业务。