- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
代理模式(Proxy Pattern,又称 Surrogate)是 Gang of Four 提出的结构型设计模式之一,其核心是为另一个对象提供替身或占位符,从而在不改动真实对象代码的前提下控制对它的访问。本指南以当前仓库 java-design-patterns 中的 proxy 模块(原文档为 localization/es/proxy/README.md)为主线,结合模块源码与测试用例,完整讲解代理模式的意图、类结构、可复现的代码示例,以及远程代理、虚拟代理、保护代理三种典型变体的适用场景。读完本文,你将能够在自己的 Java 项目中实现一个带访问控制、懒加载或日志增强的代理层,并理解它与 Adapter、Decorator、Facade、Ambassador 等模式的边界。
模式的意图:为什么需要代理
按照原文档的定义,代理模式的**目的(Propósito)**是:
提供另一个对象的替代品(sustituto)或占位符(marcador de posición),以控制对该对象的访问。
用一句简单的话概括就是:用一个类来代表另一个类的功能,客户端与真实对象之间多了一层可控的中介。维基百科对代理的经典描述(原文档引用)是:
代理,在其最一般的形式上,是一个充当某事物接口的类。代理是一个包装对象或代理对象,由客户端调用它以访问幕后的真实服务对象。代理的用途可以仅仅是转发到真实对象,也可以提供额外的逻辑——例如在真实对象上的操作非常消耗资源时提供缓存,或在真实对象上的操作被调用之前检查前置条件。
这恰好点出了代理模式的两大价值:透明地转发,以及在不修改真实对象代码的前提下附加横切逻辑。
真实世界案例:象牙塔的守门人
原文档给出了一个生动的现实隐喻:
想象一座塔,当地的巫师们前来学习咒语。象牙塔只能通过一个代理进入,该代理确保只有前三位巫师能够入塔。在这里,代理代表了塔的功能,并为其附加了访问控制。
类似的现实对应物还包括小区门卫:访客到来时,门卫先核验其凭证与权限,再决定是否放行。无论场景如何,本质都一样——对真实对象(居民/塔)的访问必须经过一个检查关卡(门卫/代理)。
模式结构:四个参与者的职责
从 proxy/etc/proxy.urm.puml 的类图定义和 proxy/src/main/java/com/iluwatar/proxy 的源码可以看出,本示例由四个参与者构成:
| 参与者 | 本示例中的类 | 职责 |
|---|---|---|
| 抽象主题(Subject) | WizardTower | 定义真实对象与代理共同实现的业务接口 |
| 真实主题(RealSubject) | IvoryTower | 真正执行业务逻辑的对象,即“被代理的对象” |
| 代理(Proxy) | WizardTowerProxy | 持有真实主题的引用,在调用前后附加控制逻辑 |
| 客户端(Client) | App | 只面向抽象主题编程,感知不到代理的存在 |
类图中明确标示了三个关键关系:WizardTowerProxy聚合(-->)持有WizardTower类型的tower引用;IvoryTower实现(..|>)WizardTower;WizardTowerProxy同样实现WizardTower。正是因为代理与真实对象实现同一接口,客户端才能无差别地调用二者。
程序化示例:完整可运行的代码
以下代码均可在 proxy/src/main/java/com/iluwatar/proxy 中找到原文实现。
第一步:定义抽象主题接口
WizardTower.java 定义了业务契约:
public interface WizardTower { void enter(Wizard wizard); }第二步:实现真实主题 IvoryTower
IvoryTower.java 使用 Lombok 的@Slf4j注解生成日志器,真实对象本身不包含任何访问控制逻辑,它只负责“放行并记录”:
@Slf4j public class IvoryTower implements WizardTower { public void enter(Wizard wizard) { LOGGER.info("{} enters the tower.", wizard); } }第三步:定义被传入的对象 Wizard
Wizard.java 是一个不可变的简单数据类,通过@RequiredArgsConstructor自动生成以name为参数的构造器,并重写toString()以输出巫师姓名:
@RequiredArgsConstructor public class Wizard { private final String name; @Override public String toString() { return name; } }第四步:实现代理 WizardTowerProxy
WizardTowerProxy.java 是本模式的核心。它在构造时接收一个WizardTower真实对象,并在enter方法中附加访问控制:只有前NUM_WIZARDS_ALLOWED(常量值 3)位巫师可以入塔,其余被拒绝:
@Slf4j public class WizardTowerProxy implements WizardTower { private static final int NUM_WIZARDS_ALLOWED = 3; private int numWizards; private final WizardTower tower; public WizardTowerProxy(WizardTower tower) { this.tower = tower; } @Override public void enter(Wizard wizard) { if (numWizards < NUM_WIZARDS_ALLOWED) { tower.enter(wizard); numWizards++; } else { LOGGER.info("{} is not allowed to enter!", wizard); } } }注意这里的关键设计:代理与真实对象实现同一接口,代理内部保存接口类型的tower引用(而非具体类),因此代理可以在不改动IvoryTower任何代码的情况下,对任意实现WizardTower的对象施加控制,这正是“开闭原则”的体现。
第五步:客户端调用场景
App.java 的main方法演示了完整的入塔场景——客户端通过代理(而非直接使用IvoryTower)依次让五位巫师尝试入塔:
public static void main(String[] args) { var proxy = new WizardTowerProxy(new IvoryTower()); proxy.enter(new Wizard("Red wizard")); proxy.enter(new Wizard("White wizard")); proxy.enter(new Wizard("Black wizard")); proxy.enter(new Wizard("Green wizard")); proxy.enter(new Wizard("Brown wizard")); }程序输出
运行 App 后,日志输出如下(时间戳与日志器名按实际运行环境略有差异):
Red wizard enters the tower. White wizard enters the tower. Black wizard enters the tower. Green wizard is not allowed to enter! Brown wizard is not allowed to enter!前三位巫师顺利入塔,第四、第五位被代理拦截——真实对象IvoryTower从未被修改,但访问行为已被代理完全控制。
源码级佐证:测试如何验证代理行为
仓库为代理模块提供了完整的单元测试,可作为行为契约的权威证据:
- WizardTowerProxyTest.java 验证代理的访问控制:让 Gandalf、Dumbledore、Oz、Merlin 四位巫师依次入塔,断言前三位产生
"enters the tower."日志,Merlin 产生"is not allowed to enter!"日志,且日志总数为 4。这直接印证了NUM_WIZARDS_ALLOWED = 3的计数逻辑。 - IvoryTowerTest.java 验证真实对象不设防:同样四位巫师直接调用
IvoryTower.enter,四人全部产生入塔日志——两相对照,可以直观看出“限制”完全来自代理层。 - AppTest.java 验证入口程序可无异常执行。
- 测试基础设施 InMemoryAppender.java 是一个基于 Logback
AppenderBase的内存日志收集器,将日志事件存入LinkedList,为上述断言提供支撑。
适用场景:何时使用代理模式
原文档明确:只要需要比简单指针更灵活、更复杂的对象引用,代理模式就适用。典型场景包括:
- 远程代理(Remote Proxy):为位于不同地址空间的对象提供本地代表(如 Java RMI)。
- 虚拟代理(Virtual Proxy):按需创建开销昂贵的对象(如大图加载、复杂计算结果的延迟实例化)。
- 保护代理(Protection Proxy):控制对原始对象的访问,当不同对象需要不同访问权限时尤其有用。
此外,代理模式通常还被用于:
- 控制对另一个对象的访问
- 延迟初始化(Lazy Initialization)
- 实现日志记录(Logging)
- 简化网络连接
- 统计对象引用次数
优点与权衡
优点:
- 受控访问:代理可以在调用真实对象之前执行检查、日志或其他附加操作,而无需侵入真实对象的代码。
- 延迟初始化:代理可将资源密集型对象的创建推迟到真正需要时,改善启动性能与内存占用。
- 远程透明:代理封装网络通信细节,让客户端像调用本地对象一样操作远程对象。
权衡:
- 额外开销:增加一层代理会引入额外的间接层,可能带来微小的性能开销。
- 复杂度上升:系统中新增类会提高结构复杂度,需要权衡是否确有引入代理的必要。
与相关模式的边界
- Ambassador:与代理相似,同为客户端与真实服务之间的中介,特别适用于远程通信场景,并会附加访问控制与监控能力。
- Adapter:Adapter 改变现有对象的接口(接口转换),而 Proxy 与真实对象保持同一接口(接口不变,只控制访问)。
- Decorator:两者都提供间接层,但 Decorator 是动态地为对象添加职责(增强功能),Proxy 则是控制访问。
- Facade:Facade 为复杂子系统提供简化接口,Proxy 则针对单个对象控制访问。
现实世界中的代理应用
从本仓库文档可以梳理出代理模式在业界与 JDK 中的常见落点(均为原文档列举,此处仅列名称便于本地检索):Java 标准库的动态代理工具 java.lang.reflect.Proxy(JDK 反射代理)、Apache Commons Proxy 项目,以及 Mockito、PowerMock、EasyMock 等 Mock 框架——它们正是利用代理机制为测试对象生成替身;iOS 的 UIAppearance 也运用了类似思想。理解手写代理的实现原理,是理解这些框架内部机制的基石。
如何在本地运行本示例
- 进入仓库根目录(本模块位于
proxy/,其构建配置见 proxy/pom.xml)。 - 该模块的
pom.xml声明了slf4j-api、logback-classic运行时依赖,以及junit-jupiter-engine、mockito-core测试依赖;并通过maven-assembly-plugin将主类配置为com.iluwatar.proxy.App,便于打包运行。 - 在仓库根目录执行测试:
./mvnw -pl proxy test(需要本机已安装 JDK 与 Maven,且具备网络以拉取依赖)。 - 执行示例入口:
./mvnw -pl proxy exec:java或在 IDE 中直接运行com.iluwatar.proxy.App,即可看到前三位巫师入塔、后两位被拒绝的日志输出。
小结
代理模式通过一个“同接口的替身对象”在客户端与真实对象之间建立可控的访问通道。本文以 java-design-patterns 仓库的proxy模块为完整蓝本,从接口、真实对象、代理到客户端调用给出了可直接运行的代码,并用测试用例验证了行为契约。掌握代理模式后,你可以将其用于权限控制、懒加载、日志审计、远程调用封装等场景,同时也为理解 JDK 动态代理与 Mock 框架的底层机制打下基础。若需进一步了解模式间的组合使用,可继续阅读仓库中的 Ambassador、Adapter 与 Decorator 模块文档。
- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
相关推荐
Amlogic S9xxx 电视盒子安装 Armbian 完整指南:新手从 0 到 Linux 服务器,附避坑命令
Amlogic S9xxx 电视盒子安装 Armbian 完整指南:新手从 0 到 Linux 服务器,附避坑命令 抽屉里吃灰的电视盒子,其实可以 7×24 小
示例工程教程Go Patterns 代理模式(Proxy Pattern)实战:用拦截与受控访问包装昂贵资源
Go Patterns 代理模式(Proxy Pattern)实战:用拦截与受控访问包装昂贵资源 代理模式(Proxy Pattern)是 Go Pattern
示例工程教程文档Java 设计模式:Acyclic Visitor(无环访问者)模式解析与源码实战 —— 基于 java-design-patterns 仓库 acyclic-visitor 模块
Java 设计模式:Acyclic Visitor(无环访问者)模式解析与源码实战 —— 基于 java design patterns 仓库 acyclic
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考