设计模式(十二)代理模式 — 用代理控制访问,实现延迟加载、权限控制等功能

在软件工程中,"间接性"往往是灵活性与可维护性的源泉。代理模式(Proxy Pattern)正是这一思想的经典体现。它通过引入一个"中间人"对象,在不改变原始接口的前提下,实现对目标对象的访问控制、功能增强或行为拦截。无论是图片懒加载、权限校验、远程调用,还是 Spring 的事务管理,背后都离不开代理的身影。

本文将系统性地展开代理模式的全貌,共分 14 个章节,层层递进,覆盖理论、分类、实现、对比、框架集成、性能分析与最佳实践,助你真正掌握这一核心设计模式。


第一章:代理模式的定义与核心思想

代理模式属于结构型设计模式,其官方定义为:"为其他对象提供一种代理以控制对这个对象的访问。"

其本质是通过一个中介对象(Proxy)来代表真实对象(RealSubject),客户端只与代理交互,而代理决定何时、如何、是否将请求转发给真实对象。

核心思想包括三点:

  1. 透明性:客户端无需知道代理的存在,调用方式与直接调用真实对象一致。
  2. 控制性:代理可在请求前后插入逻辑,如权限检查、日志记录、缓存等。
  3. 解耦性:将横切关注点(如安全、日志)从业务逻辑中剥离,符合单一职责原则。

例如,当你点击网页中的图片时,浏览器可能先显示占位图,等滚动到可视区域才加载真实图片------这就是虚拟代理的典型应用。


第二章:代理模式的角色与 UML 结构

代理模式包含四个核心角色:

  • Subject(抽象主题):定义代理与真实对象的公共接口,确保客户端可统一处理两者。
  • RealSubject(真实主题):实现 Subject 接口,包含核心业务逻辑。
  • Proxy(代理):也实现 Subject 接口,内部持有 RealSubject 引用,在转发请求前后执行附加操作。
  • Client(客户端):仅依赖 Subject 接口,完全 unaware 代理或真实对象的具体类型。

UML 类图如下:
聚合 <<interface>> Subject +request() RealSubject +request() Proxy -realSubject: RealSubject +request()

该结构保证了开闭原则:新增代理类型无需修改客户端代码。


第三章:虚拟代理(Virtual Proxy)------延迟加载重型资源

虚拟代理用于延迟创建开销大的对象,直到真正需要时才初始化。适用于图像、视频、数据库连接池、大型计算结果等场景。

典型实现:

  • 代理持有一个空引用;
  • 首次调用方法时,才创建真实对象;
  • 后续调用直接复用已创建实例。

优势:

  • 减少内存占用;
  • 加快系统启动速度;
  • 提升用户体验(如网页首屏渲染更快)。

💡 浏览器中的 <img loading="lazy"> 就是虚拟代理的 HTML 原生实现。


第四章:保护代理(Protection Proxy)------基于角色的访问控制

保护代理用于根据用户身份或权限决定是否允许访问某操作。常见于银行系统、后台管理、API 网关等。

实现要点:

  • 代理持有当前用户上下文(如角色、权限列表);
  • 在调用真实方法前进行鉴权;
  • 若无权限,抛出异常或返回错误信息。

示例:普通用户只能查看账户余额,管理员才能执行资金划转。通过保护代理,业务逻辑无需掺杂权限判断代码,保持纯净。


第五章:远程代理(Remote Proxy)------封装网络通信

远程代理为位于不同地址空间(如另一台机器)的对象提供本地代表,隐藏底层网络细节。

典型应用:

  • Java RMI(Remote Method Invocation)
  • gRPC 客户端 Stub
  • Web Service 的 SOAP/REST 客户端

客户端调用代理方法如同调用本地方法,而代理负责:

  • 序列化参数;
  • 发起网络请求;
  • 反序列化响应;
  • 处理超时、重试、熔断等。

这极大简化了分布式系统的开发复杂度。


第六章:智能引用代理(Smart Reference Proxy)------自动附加行为

智能引用代理在每次访问对象时自动触发额外动作,如:

  • 引用计数(用于资源回收)
  • 访问日志
  • 缓存命中统计
  • 对象状态监控

例如,一个文件句柄代理可在每次读取时记录访问次数,并在引用归零时自动关闭文件。这种模式常用于资源管理或调试工具中。


第七章:缓存代理(Cache Proxy)------提升系统性能

缓存代理对昂贵操作的结果进行缓存,避免重复计算或 I/O。

实现方式:

  • 以方法参数为 key,结果为 value 存入 Map 或 Redis;
  • 调用前先查缓存,命中则直接返回;
  • 未命中则委托真实对象执行,并写入缓存。

适用场景:

  • 数据库查询结果
  • 复杂数学计算
  • 第三方 API 调用

⚠️ 注意缓存一致性问题,需结合过期策略或主动失效机制。


第八章:日志与监控代理(Logging/Monitoring Proxy)

此类代理专注于非功能性需求,如:

  • 记录方法调用时间、参数、返回值;
  • 统计 QPS、错误率;
  • 上报指标至 Prometheus 或 ELK。

优势在于业务代码零侵入。例如,一个订单服务无需关心"谁在调用我",只需专注下单逻辑,日志由代理自动完成。


第九章:静态代理 vs 动态代理 ------ 手动编写 vs 运行时生成

维度 静态代理 动态代理
生成时机 编译期手动编写 运行期 JVM 动态生成
灵活性 低(每个接口需单独代理类) 高(通用 InvocationHandler)
性能 略高(无反射) 略低(依赖反射)
维护成本 高(代码冗余) 低(逻辑集中)

静态代理适合简单、明确的场景;动态代理适合框架级通用增强。


第十章:JDK 动态代理深度剖析

JDK 动态代理基于 java.lang.reflect.ProxyInvocationHandler仅支持接口代理

工作流程:

  1. 调用 Proxy.newProxyInstance()
  2. JVM 在运行时生成 $Proxy0 类(继承 Proxy,实现指定接口);
  3. 所有接口方法调用被转发至 InvocationHandler.invoke()
  4. Handler 决定是否调用 method.invoke(target, args)

局限性:

  • 无法代理没有接口的类;
  • 无法代理 final 方法;
  • 性能略低于直接调用(反射开销约 10--20%)。

第十一章:CGLIB 动态代理与字节码增强

CGLIB(Code Generation Library)通过继承目标类并重写方法 实现代理,支持类代理

原理:

  • 使用 ASM 操作字节码;
  • 生成子类(如 UserService$$EnhancerByCGLIB);
  • 在子类方法中插入回调逻辑。

优势:

  • 可代理无接口的类;
  • 性能优于 JDK 代理(尤其在高频调用下);
  • 支持方法过滤、回调链等高级特性。

Spring 默认优先使用 JDK 代理,若目标类无接口则自动切换至 CGLIB。


第十二章:Spring AOP 中的代理机制实战

Spring AOP 是代理模式最成功的工业级应用。其核心就是通过代理实现切面编程

关键点:

  • @EnableAspectJAutoProxy 开启自动代理;
  • 切面(Aspect)定义通知(Advice);
  • Spring 容器自动为目标 Bean 创建代理;
  • 调用被代理方法时,自动织入前置、后置、环绕通知。

示例:

java 复制代码
@Aspect
@Component
public class LogAspect {
    @Around("@annotation(Loggable)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        System.out.println("Method took: " + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

🔥 注意:只有通过 Spring 容器调用的方法才会被代理!内部方法调用(this.method())不会触发 AOP。


第十三章:Hibernate 与 MyBatis 中的代理应用

  • Hibernate :对 @OneToMany@ManyToOne 等关联属性使用虚拟代理 实现懒加载。当你访问 user.getOrders() 时,Hibernate 才发起 SQL 查询。
  • MyBatis :Mapper 接口无实现类,MyBatis 通过JDK 动态代理为每个接口方法绑定对应的 SQL 语句。

二者都利用代理实现了"声明式编程":开发者只需定义接口或注解,框架自动完成底层逻辑。


第十四章:代理模式的最佳实践与陷阱规避

✅ 最佳实践:

  1. 明确代理目的:是控制访问?还是增强功能?
  2. 优先使用动态代理:减少样板代码,提升可维护性;
  3. 避免代理链过深:多层代理会增加调试难度;
  4. 合理管理生命周期:虚拟代理注意内存泄漏;
  5. 结合工厂模式:统一创建和配置代理。

❌ 常见陷阱:

  • 循环依赖:代理持有真实对象,真实对象又持有代理;
  • 状态不一致:代理与真实对象各自维护状态;
  • 性能误判:在高频路径上滥用代理导致瓶颈;
  • 忽略异常传播:代理未正确处理或包装异常。

📊 适用场景总结:

  • 需要延迟加载 → 虚拟代理
  • 需要权限控制 → 保护代理
  • 需要远程调用 → 远程代理
  • 需要统一增强 → 动态代理 + AOP

结语:代理,让系统更优雅

代理模式不仅是设计模式,更是一种架构哲学 :通过引入可控的间接层,换取系统的安全性、灵活性与可扩展性。从简单的图片懒加载,到企业级微服务治理,代理无处不在。

掌握代理模式,意味着你已具备"在不侵入核心业务的前提下,优雅地扩展系统能力"的能力------这正是高级工程师的核心竞争力之一。

下次当你面对"如何在不改代码的情况下加日志?"、"如何实现懒加载?"、"如何做权限拦截?"等问题时,请记住:代理,就在你手中。

相关推荐
SakuraOnTheWay6 小时前
《深入设计模式》学习(1)—— 深入理解OOP中的6种对象关系
设计模式
q***71856 小时前
Java进阶-SpringCloud设计模式-工厂模式的设计与详解
java·spring cloud·设计模式
白衣鸽子6 小时前
告别参数地狱:业务代码中自定义Context的最佳实践
后端·设计模式·代码规范
帅中的小灰灰19 小时前
C++编程建造器设计模式
java·c++·设计模式
ZHE|张恒19 小时前
设计模式(十)外观模式 — 提供统一入口,简化复杂系统的使用
设计模式·外观模式
howcode1 天前
女友去玩,竟带回一道 “虐哭程序员” 的难题
后端·设计模式·程序员
程序员-周李斌1 天前
Java 代理模式详解
java·开发语言·系统安全·代理模式·开源软件
apigfly2 天前
深入Android系统(十三)Android的窗口系统
android·设计模式·源码
y***54882 天前
Java设计模式之观察者模式
观察者模式·设计模式