走进 Gang of Four 设计模式:代理模式
说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"
前置知识:Java 面向对象基础(接口、多态)、静态代理与动态代理概念
📊 设计模式分类总览
GoF 23 种设计模式可按两个维度交叉分类:类/对象 (处理方式) × 创建型/结构型/行为型(目的)。
| 维度 | 创建型(Creational) | 结构型(Structural) | 行为型(Behavioral) |
|---|---|---|---|
| 类(Class) (通过继承复用) | Factory Method 工厂方法 | Adapter(类适配器) | Interpreter(解释器) Template Method(模板方法) |
| 对象(Object) (通过组合/聚合复用) | Abstract Factory(抽象工厂) Builder(建造者) Prototype(原型) Singleton(单例) | Adapter(对象适配器) Bridge(桥接) Composite(组合) Decorator(装饰) Facade(外观) Flyweight(享元) Proxy(代理) | Chain of Resp.(责任链) Command(命令) Iterator(迭代器) Mediator(中介者) Memento(备忘录) Observer(观察者) State(状态) Strategy(策略) Visitor(访问者) |
本文档聚焦 结构型-对象模式 中的 Proxy(代理) ,核心思想是通过引入一个中间层(Proxy)控制对真实对象的访问,在不修改原始类的前提下添加额外逻辑。
📑 目录
- [模式概述 · 15 问深度分析](#模式概述 · 15 问深度分析)
- [Q1: 为什么需要?解决了什么问题?](#Q1: 为什么需要?解决了什么问题?)
- [Q2: 不用会有什么缺陷?](#Q2: 不用会有什么缺陷?)
- [Q3: 核心思想一句话](#Q3: 核心思想一句话)
- [Q4: 包含哪些角色?](#Q4: 包含哪些角色?)
- [Q5: 调用流程是怎样的?](#Q5: 调用流程是怎样的?)
- [Q6: 为什么这样设计?](#Q6: 为什么这样设计?)
- [Q7: 为什么用接口/抽象类/组合?](#Q7: 为什么用接口/抽象类/组合?)
- [Q8: 符合哪些面向对象原则?](#Q8: 符合哪些面向对象原则?)
- [Q9: 优缺点与额外成本](#Q9: 优缺点与额外成本)
- [Q10: 适合/不该用的场景](#Q10: 适合/不该用的场景)
- [Q11: 与其他模式的区别](#Q11: 与其他模式的区别)
- [Q12: 框架中的典型应用](#Q12: 框架中的典型应用)
- [Q13: 最小可运行版本](#Q13: 最小可运行版本)
- [Q14: 识别重构信号](#Q14: 识别重构信号)
- [Q15: 现代改进方案](#Q15: 现代改进方案)
- 框架源码实战分析
- [Spring AOP 代理机制](#Spring AOP 代理机制)
- [MyBatis Mapper 动态代理](#MyBatis Mapper 动态代理)
- [Tomcat RequestFacade 安全代理](#Tomcat RequestFacade 安全代理)
- [深度追问:Why / How / Trade-off / Evolution / Modern Practice](#深度追问:Why / How / Trade-off / Evolution / Modern Practice)
- 总结
- 参考文献
1. 模式概述 · 15 问深度分析
Q1: 为什么需要这个模式?它解决了什么问题?
在软件开发中,直接访问一个对象有时会带来问题。例如:对象创建开销过大、需要进行权限控制、或者对象存在于远程机器上。
代理模式解决的核心问题是:在不改变原始类代码的前提下,控制对该对象的访问,并在访问前后添加额外的间接层(控制逻辑)。
Q2: 如果不用这个模式,会有什么缺陷?
不用代理模式,你只能将非业务核心逻辑(如日志记录、权限校验、事务管理、缓存等)硬编码进业务类中。这会导致:
- 代码臃肿与耦合: 核心业务代码充斥着大量与业务无关的周边逻辑。
- 违反单一职责原则: 一个类既要负责核心业务,又要负责安全、日志和事务。
- 难以扩展和复用: 每次你想给不同类加上相同的安全校验,都得复制一遍代码。
java
// ❌ 业务代码中混入了横切逻辑
public class UserService {
public void createUser(User user) {
// 权限校验
if (!SecurityContext.hasRole("ADMIN")) {
throw new SecurityException("无权限");
}
// 日志记录
log.info("开始创建用户...");
// 事务开启
TransactionManager.begin();
try {
// 核心业务逻辑
userDao.insert(user);
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
throw e;
}
log.info("用户创建成功");
}
}
Q3: 核心思想是什么?一句话如何概括?
- 核心思想: 通过引入一个代理对象来间接访问目标对象,从而在不修改目标对象的前提下,控制访问并为其扩展功能。
- 一句话概括: "房东只管签合同,出租找中介" ------ 用一个替身来控制和增强对原对象的访问。
Q4: 它包含哪些角色?每个角色的职责是什么?
代理模式通常包含以下 3 个核心角色:
- 抽象主题(Subject): 定义了真实主题和代理主题的共同接口。这样在任何使用真实主题的地方,都可以用代理主题代替。
- 真实主题(Real Subject): 真正执行核心业务逻辑的对象,是代理对象所代表的幕后真实对象。
- 代理(Proxy): 内部持有对真实主题的引用。它与真实主题实现了相同的接口,因此可以替代真实主题。它负责在调用真实主题前后,执行预处理和善后工作(如权限判定、延迟加载、日志记录)。
Q5: 它们之间如何协作?调用流程是怎样的?
调用流程如下:
- 客户端(Client) 拿着抽象主题(Subject)的引用,调用其方法。此时客户端实际调用的是代理(Proxy)。
- 代理(Proxy) 接收到请求,先执行前置处理(例如:检查用户是否有权限,或者判断真实对象是否需要初始化)。
- 代理(Proxy) 调用**真实主题(Real Subject)**的对应方法,执行核心业务。
- 代理(Proxy) 接收到真实主题的返回值,执行后置处理(例如:记录日志、提交事务、缓存结果)。
- 代理(Proxy) 将最终结果返回给客户端。
Q6: 为什么要这样设计?每个角色存在的意义是什么?
- 抽象主题(Subject)存在的意义: 保证了"透明性"。对客户端而言,它不知道也不关心自己拿到的是真身还是代理,两者的契约(接口)完全一致,这极大地降低了系统的耦合度。
- 真实主题(Real Subject)存在的意义: 专注核心业务逻辑。它不需要感知任何诸如网络传输、权限控制或日志统计的外部环境,保持纯粹。
- 代理(Proxy)存在的意义: 充当"挡箭牌"和"打杂工"。它屏蔽了直接访问真身带来的副作用,承接了所有横切关注点(Cross-cutting Concerns)。
Q7: 为什么使用接口、抽象类、组合,而不是其他方式?
- 为什么用接口/抽象类(继承契约): 为了实现多态。只有当代理和真实对象拥有相同的"外壳"(接口)时,客户端才能无感知地进行切换。
- 为什么用组合(内部持有真实对象),而不是让代理类直接继承真实类:
- 继承是静态的(编译期决定),而组合是动态的(运行期决定)。
- 如果使用继承,每个真实类都需要写一个继承它的代理类,会导致类爆炸。
- 通过组合,一个代理类甚至可以代理一类实现了相同接口的不同真实对象(尤其是动态代理)。
Q8: 这种设计符合哪些面向对象原则?
- 开闭原则(OCP): 当你需要增加新的控制逻辑(如新增缓存功能)时,不需要修改真实主题的代码,只需要新建一个代理类或者扩展代理逻辑即可。
- 单一职责原则(SRP): 真实主题只负责核心业务,代理类负责控制和周边外围逻辑,各自职责单一。
- 依赖倒置原则(DIP): 客户端和代理类都依赖于抽象接口(Subject),而不是依赖于具体的实现类(Real Subject)。
Q9: 它有哪些优点和局限性?会带来哪些额外成本?
优点:
- 能协调调用者和被调用者,在一定程度上降低了系统的耦合度。
- 可以在不修改原始代码的情况下,动态地切入新功能。
- 远程代理可以隐藏对象存在于不同地址空间的事实;虚拟代理可以优化性能(延迟加载)。
局限性/额外成本:
- 性能损耗: 增加了一层间接调用,请求处理速度可能会稍微变慢。
- 复杂度增加: 增加了类的数量(静态代理),或者让代码的调用链路变长,增加了调试和理解的成本(尤其是多层动态代理)。
Q10: 它最适合哪些场景?哪些场景不应该使用?
适合使用的场景:
- 虚拟代理(Virtual Proxy): 用于大对象的延迟加载(Lazy Loading),比如网页中先用一张轻量级占位图代替未加载完的大图。
- 保护代理(Protection Proxy): 控制不同用户对真实对象的访问权限。
- 远程代理(Remote Proxy): 为一个位于不同地址空间(如微服务、RPC 调用)的对象提供局部代表(如 Dubbo 的 Stub)。
- 智能代理(Smart Proxy): 在方法前后统一加日志、统计耗时或开启/提交事务。
不应该使用的场景:
- 如果系统调用链路已经非常冗长且性能敏感,不应滥用代理。
- 如果扩展功能只需要简单地在真身内部改动一行代码,且该类未来几乎不会再扩展其他外围功能,引入代理属于过度设计。
Q11: 它与容易混淆的其他设计模式有什么区别和联系?
- 装饰器模式(Decorator) vs 代理模式:
- 目的不同: 装饰器关注于增强功能 (动态地为对象添加职责);代理关注于控制访问,它往往对客户端隐瞒真实对象的细节。
- 适配器模式(Adapter) vs 代理模式:
- 接口不同: 适配器会改变接口 (把 A 接口转成 B 接口);代理模式严格保持接口一致。
- 外观模式(Facade) vs 代理模式:
- 粒度不同: 外观模式为一组子系统 定义一个高层组合接口(一对多);代理模式代表的是单个对象(一对一)。
Q12: Java/JDK、Spring、MyBatis、Tomcat 等框架中有哪些典型应用?
- JDK:
java.lang.reflect.Proxy提供了动态代理的底层实现;Collections.unmodifiableList()返回一个只读的代理列表,当你调用add()时会抛出异常(保护代理)。 - Spring: Spring AOP(面向切面编程)的基石就是代理模式。 它通过 JDK 动态代理或 CGLIB 动态代理,实现了
@Transactional事务控制和日志切面。 - MyBatis: Mapper 接口(如
UserMapper)并没有实现类,MyBatis 内部使用 JDK 动态代理为这些接口生成了代理对象,拦截方法调用并将其转化为执行 SQL。 - Tomcat: Request 和 Response 对象在传入 Servlet 时,往往会被包装成
RequestFacade和ResponseFacade(外观/代理结合体),防止开发者通过强转接触到 Tomcat 容器底层的核心Request对象,确保安全。
Q13: 如何从零实现一个最小可运行版本?
以图片懒加载为例:
java
// Subject
public interface Image {
void display();
}
// RealSubject
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk(fileName);
}
private void loadFromDisk(String fileName) {
System.out.println("Loading " + fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
}
// Proxy
public class ProxyImage implements Image {
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
// 注意:构造时不加载图片
}
@Override
public void display() {
// 延迟加载:只在第一次调用 display() 时创建 RealImage
if (realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
// Client
public class ProxyPatternDemo {
public static void main(String[] args) {
Image image = new ProxyImage("test_10mb.jpg");
System.out.println("Image created, but not yet loaded.");
// 第一次调用 display(),才真正加载
image.display();
System.out.println("");
// 第二次调用,不需要再加载,直接显示
image.display();
}
}
Q14: 如何在现有项目中识别可以使用该模式的地方?
如果代码中频繁出现"在执行核心逻辑之前,先做某件事"的模式(如先检查权限再执行、先加载缓存再执行、先记录日志再执行),说明需要引入代理模式将这些横切关注点剥离。
典型信号:
- 业务类中充斥着
log.info("start")→ 核心逻辑 →log.info("end")的三段式代码。 - 业务类中每次方法调用前都有
if (!hasPermission(...)) throw ...。 - 业务类的方法中包含
TransactionManager.begin()和TransactionManager.commit()。
Q15: 如果重新设计这个模式,在今天会有哪些改进?
静态代理 → 动态代理: 静态代理中,代理类在编译期就已经写好,每增加一个真实对象就需要手动编写一个代理类,代码量翻倍。动态代理(JDK 动态代理或 CGLIB)允许在运行时动态生成代理对象,消除了代理类的数量问题。
Spring AOP 让代理变得透明: 开发不再需要手动编写代理类,只需声明 @Transactional、@Cacheable 等注解,Spring 自动为 Bean 生成代理。
java
// 使用注解即可,无需手动编写代理
@Service
public class UserService {
@Transactional
public void createUser(User user) {
// 只写核心业务逻辑,事务由代理自动处理
userDao.insert(user);
}
}
Sidecar 模式(云原生): 在 Kubernetes + Istio 架构中,每个微服务旁边部署一个代理进程(Envoy),负责处理所有进出流量、服务发现、熔断限流。这是代理模式从代码层面到基础设施层面的升华。
2. 框架源码实战分析
2.1 Spring AOP 代理机制
Spring AOP 是代理模式最经典的工业级应用。它的核心是:在运行时为目标对象生成代理,在代理中织入切面逻辑(事务、安全、日志等)。
JDK 动态代理 vs CGLIB 代理:
| 代理方式 | 适用条件 | 原理 |
|---|---|---|
| JDK 动态代理 | 目标类实现了接口 | 通过 Proxy.newProxyInstance() 生成实现了相同接口的代理类 |
| CGLIB 代理 | 目标类未实现接口 | 通过生成目标类的子类作为代理(EmitSubclass) |
Spring 源码核心逻辑(简化):
java
// Spring AOP 的核心------代理创建
public class DefaultAopProxyFactory implements AopProxyFactory {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 判断使用 JDK 动态代理还是 CGLIB
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
// 没有接口 → 使用 CGLIB
return new ObjenesisCglibAopProxy(config);
} else {
// 有接口 → 使用 JDK 动态代理
return new JdkDynamicAopProxy(config);
}
}
}
// JDK 动态代理的调用处理程序
public class JdkDynamicAopProxy implements AopProxy, InvocationHandler {
// 持有目标对象的引用
private final AdvisedSupport advised;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, ...);
// 1. 执行前置通知(@Before)
// 2. 执行目标方法
Object retVal = invocation.proceed();
// 3. 执行后置通知(@AfterReturning / @After)
// 4. 返回结果
return retVal;
}
}
运行时调用链:
当你调用 userService.createUser(user) 时:
- 实际上调用的是 Spring 生成的代理对象的
createUser()方法。 - 代理对象进入
JdkDynamicAopProxy.invoke()。 - invoke 方法获取该方法对应的拦截器链(包括事务拦截器、日志拦截器等)。
- 逐个执行拦截器:先开启事务,然后调用目标方法,最后提交事务或回滚。
- 整个过程中,
UserService完全不需要知道事务的存在。
2.2 MyBatis Mapper 动态代理
MyBatis 中,我们只需要定义 Mapper 接口而无需编写实现类:
java
// 定义接口 ------ 不需要写实现类
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
}
MyBatis 使用 JDK 动态代理为每个 Mapper 接口生成代理对象:
java
// MyBatis 核心------Mapper 代理
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用 JDK 动态代理生成 Mapper 接口的代理对象
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy // 调用处理程序
);
}
}
// MapperProxy------InvocationHandler
public class MapperProxy<T> implements InvocationHandler, Serializable {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 如果是 Object 的方法(toString, hashCode 等),直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 根据 Method 从配置中查找对应的 SQL 语句(MappedStatement)
MappedStatement ms = configuration.getMappedStatement(method.getName());
// 3. 执行 SQL(代理的核心职责)
return sqlSession.selectOne(ms, args);
}
}
运行时调用链: 当你调用 userMapper.selectById(1) 时,实际调用的是 MapperProxy.invoke() → 获取 SQL 语句 → 创建 SqlSession → 执行 JDBC 操作 → 映射结果集 → 返回 User 对象。对业务代码来说,感觉就像在调用一个本地方法。
2.3 Tomcat RequestFacade 安全代理
Tomcat 在处理 HTTP 请求时,内部使用了 org.apache.catalina.connector.Request 对象来处理底层 socket 数据。但这个对象暴露了太多内部 API(如 getCoyoteRequest()、getConnector() 等),如果直接传给 Servlet 容器,开发者可以强转后访问 Tomcat 的内部实现,存在安全隐患。
解决方案:使用 RequestFacade 作为代理:
java
// Tomcat 内部的核心 Request 对象(包含大量的内部方法)
public class Request implements HttpServletRequest {
// 内部方法------不希望暴露给 Servlet 开发者
public Connector getConnector() { ... }
public CoyoteRequest getCoyoteRequest() { ... }
// 标准 Servlet 方法
@Override
public String getParameter(String name) { ... }
@Override
public String getRequestURI() { ... }
}
// RequestFacade------代理(只暴露标准 Servlet 接口)
public class RequestFacade implements HttpServletRequest {
// 持有内部 Request 对象的引用(组合)
private final Request request;
public RequestFacade(Request request) {
this.request = request;
}
// 只代理标准方法
@Override
public String getParameter(String name) {
return request.getParameter(name);
}
@Override
public String getRequestURI() {
return request.getRequestURI();
}
// 注意:没有代理 getConnector()、getCoyoteRequest() 等内部方法
// Servlet 开发者无法通过 RequestFacade 访问 Tomcat 内部实现
}
为什么这样设计? 这是保护代理 的典型应用。Tomcat 通过 RequestFacade 控制了对底层 Request 对象的访问权限------只允许开发者调用标准 Servlet API,屏蔽了内部实现细节。这既保证了接口的兼容性,又保护了容器内部的安全。
3. 深度追问
3.1 Why(为什么):解决哪个根本矛盾?
代理模式解决的是**"核心业务职责的纯粹性"与"系统横切关注点(Cross-cutting Concerns)的侵入性"之间的根本矛盾。**
安全、事务、日志、缓存、延迟加载等横切逻辑与核心业务逻辑本质上是正交的,但传统的 OOP 缺乏有效的分离机制,导致这些关注点散落在各个业务类中,造成代码的混乱和重复。代理模式通过创建一个中间层,将这些横切关注点从核心业务中剥离出来,实现了关注点分离。
3.2 How(怎么做):对象关系与交互机制
代理模式的实现有三个层级,逐步递进:
静态代理: 手动编写代理类,编译期就已确定。优点是简单直接;缺点是一个代理类只能服务一个接口,真实对象增加时代码量翻倍。
JDK 动态代理: 运行时通过 Proxy.newProxyInstance() 为接口生成代理对象,使用 InvocationHandler 拦截方法调用。优点是无需手动编写代理类;缺点是只能代理实现了接口的类。
CGLIB 代理: 运行时通过字节码生成技术(ASM)创建目标类的子类作为代理。优点是可以代理没有接口的普通类;缺点是 final 方法无法被代理,且生成速度比 JDK 代理慢。
三种方式的共同核心:代理与真实对象实现相同的接口(或继承相同的基类),客户端面向接口编程,对代理的存在无感知。
3.3 Trade-off(代价):牺牲了什么换取什么?
它牺牲了(付出代价):
- 性能开销: 每次方法调用都多了一层间接调用,在高频调用场景下性能损耗不可忽略(尤其是动态代理的反射调用)。
- 调试复杂度: 代理在 IDE 中断点调试时,调用栈比直接调用深 2-3 层。Spring AOP 的代理 + AOP 拦截器链可能让调用栈深达 5-8 层。
- 启动时间: 动态代理在启动时需要生成字节码、加载类,会延长应用的启动时间。
它换取了(获得收益):
- 完美的关注点分离: 核心业务与横切逻辑完全解耦。
- 极高的可扩展性: 新增横切逻辑只需增加新的代理或切面,不改现有业务逻辑。
- 对客户端的完全透明: 客户端感知不到代理的存在,面向接口编程即可。
3.4 Evolution(演化):它是如何从简单设计逐步演化而来的?
-
硬编码横切逻辑(起点): 在业务代码中直接写日志、事务、权限检查。最直接但也最混乱。
-
静态代理(抽取): 为每个业务类编写对应的代理类,将横切逻辑抽取到代理中。但代码量翻倍,维护成本高。
-
JDK 动态代理(动态化): 运行时根据接口动态生成代理对象,消除了代理类的数量问题。但只能代理接口。
-
CGLIB 代理(突破限制): 通过生成子类的方式代理普通类,突破了 JDK 代理只能代理接口的限制。
-
AOP 框架(声明式): Spring AOP 结合 IoC 容器,让开发者只需要声明
@Transactional、@Cacheable等注解,Spring 自动在运行时为目标 Bean 生成合适的代理。开发者几乎感知不到代理的存在。
3.5 Modern Practice(现代实践):它在今天被取代了吗?
结论:代理模式不仅没有被取代,反而在现代框架中成为了基础设施级的核心机制。
- Spring AOP 是代理模式最成功的工业应用,
@Transactional、@Cacheable、@Async等注解全部依赖代理实现。 - MyBatis Mapper 动态代理 让开发者不需要编写 DAO 实现类。
- Dubbo / gRPC 的远程代理 让远程调用像本地调用一样简单。
Sidecar 模式------代理模式在基础设施层面的升华: 在云原生架构(Kubernetes + Istio)中,每个微服务旁边部署一个代理进程(Envoy),负责处理所有进出流量、服务发现、熔断限流、可观测性。业务代码完全不需要关心这些基础设施能力------这正是代理模式"透明控制"思想的终极体现。
4. 总结
核心要点
- 代理模式为真实对象提供一个替身,通过中间层控制访问并添加额外逻辑
- 三种实现层级:静态代理 → JDK 动态代理 → CGLIB 代理
- 与装饰器的区别:代理控制访问 ,装饰器增强功能
- Spring AOP 是代理模式的核心应用------通过
@Transactional等注解声明式管理横切逻辑 - 在云原生架构中,代理模式升级为 Sidecar 模式,成为基础设施的一等公民
一句话
代理模式就是"中间人"------你找明星演出,通过经纪人(代理)谈价格、排档期,明星只负责登台表演。
5. 参考文献
1 GAMMA E, HELM R, JOHNSON R, et al. Design Patterns: Elements of Reusable Object-Oriented SoftwareM. Boston: Addison-Wesley, 1994.
2 SPRING. Spring AOP DocumentationEB/OL. https://docs.spring.io/spring-framework/reference/core/aop.html, 2024.
3 MyBatis. MyBatis 3 Source CodeEB/OL. https://github.com/mybatis/mybatis-3, 2024.
4 Apache Tomcat. Tomcat Source CodeEB/OL. https://tomcat.apache.org/, 2024.