深入浅出设计模式【十二、代理模式】
一、代理模式介绍
在某些场景下,客户端直接访问目标对象可能不合适或存在困难。原因可能是:目标对象位于远程服务器上(网络开销大)、目标对象创建开销巨大需要延迟加载、需要控制对目标对象的访问权限、或者需要在访问前后添加额外操作(如日志、监控)。
代理模式正是为解决这些问题而生。它在客户端和目标对象之间引入一个代理对象。这个代理对象与目标对象(或称真实对象)实现相同的接口,因此客户端通常无法察觉(或不关心)使用的是代理还是真实对象。代理对象负责在需要时将客户端请求转发给真实对象,并可在转发前后执行附加操作,从而实现了访问控制、功能增强等目的。
二、核心概念与意图
-
核心概念:
- 主题/抽象接口 (Subject): 声明了真实对象和代理对象共同实现的接口。这使得任何使用真实对象的地方都能透明地使用代理对象。
- 真实主题/真实对象 (Real Subject): 定义了代理所代表的真实对象。它包含了真正的业务逻辑,是客户端最终想要访问的对象。
- 代理 (Proxy): 模式的核心。
- 持有一个对真实主题的引用(或能访问真实主题)。
- 实现了与真实主题相同的接口 (
Subject)。 - 客户端访问代理对象。
- 代理对象可以控制对真实对象的访问,并且在访问真实对象前后可以执行附加操作。
- 负责在必要时创建和销毁真实对象(如在虚拟代理中)。
-
意图:
- 为其他对象提供一种代理以控制对这个对象的访问。
- 在访问一个对象时引入一定程度的间接性,以便根据情况决定直接访问、延迟创建、控制访问或添加增强功能。
- 实现职责分离,将核心功能保留在真实对象中,将横切关注点(如日志、安全、事务)放在代理中。
三、适用场景剖析
代理模式在以下场景中非常有效:
- 远程代理 (Remote Proxy): 为一个位于不同地址空间(如不同JVM、不同服务器)的对象提供本地代表。它将远程调用细节(如网络通信、序列化)封装在代理内部,使客户端感觉像是在调用本地对象(如RMI Stub)。
- 虚拟代理 (Virtual Proxy): 用于延迟创建开销大的真实对象。代理会先处理轻量级操作,或者显示一个占位符(如Loading图片),直到真正需要时才创建或加载真实对象(如图片的懒加载、大型文档的分页加载)。
- 保护代理 (Protection Proxy): 控制对原始对象的访问权限。代理在访问真实对象前,会检查客户端的访问权限,只有拥有相应权限的请求才会被转发(如权限控制层)。
- 缓存代理 (Cache Proxy): 为开销大的操作提供临时存储(缓存)。当客户端请求时,代理首先检查缓存中是否存在有效结果。如果存在,直接返回缓存结果;如果不存在,才调用真实对象的方法,并将结果放入缓存后返回(如数据库查询结果缓存)。
- 智能引用代理 (Smart Reference Proxy): 在访问真实对象时执行附加的智能操作。例如:
- 引用计数:自动释放不再使用的昂贵对象。
- 首次引用时加载对象。
- 访问真实对象前加锁,确保线程安全(同步代理)。
- 日志记录代理 (Logging Proxy): 记录方法调用的参数、返回值、执行时间等信息,用于调试、监控或审计。
- 防火墙代理 (Firewall Proxy): 保护网络免受恶意请求的侵害,通常用于内部网络和外部网络(如Internet)之间的缓冲区域。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了代理模式的结构和角色间的关系:
Subject(抽象主题/接口): 定义了RealSubject和Proxy的公共接口。客户端依赖于这个接口。RealSubject(真实主题/真实对象): 定义并实现了核心的业务逻辑,提供了request()方法的具体功能。Proxy(代理):- 维护一个对
RealSubject的引用 (-realSubject: RealSubject)。这个引用可以在代理构造时创建,也可以延迟到真正需要时才创建(虚拟代理)。 - 实现了
Subject接口,因此其request()方法具有与RealSubject.request()相同的方法签名。 - 在其
request()方法实现中:- 执行前置操作 (如权限检查、日志记录、参数校验)。
- 访问真实对象 (调用
realSubject.request())。代理可以决定是否转发请求、如何转发请求、甚至转发给哪个对象(可能在多个实现中选择)。 - 执行后置操作 (如缓存结果、执行清理、记录执行时间)。
- 负责管理真实对象的生命周期 (如创建、销毁),对客户端透明。
- 维护一个对
Client(客户端): 只与Subject接口交互。客户端通过Subject对象(实际上是一个Proxy实例)来调用request()方法,完全不知道(或不关心)背后是代理还是真实对象在工作。
五、各种实现方式及其优缺点
代理模式的实现方式多样,取决于应用场景和具体技术栈。主要分为两大类:静态代理和动态代理。
1. 静态代理 (Static Proxy)
- 描述: 在编译前,代理类 (
Proxy) 和真实类 (RealSubject) 的代码都是明确编写好的。代理类直接引用具体的真实类。这对应于上述UML图的直接实现。 - 代码示例(简化版):
// Subject Interface public interface Image { void display(); } // RealSubject public class RealImage implements Image { private String filename; public RealImage(String filename) { this.filename = filename; loadFromDisk(); // Heavy operation } private void loadFromDisk() { System.out.println("Loading image: " + filename); } @Override public void display() { System.out.println("Displaying image: " + filename); } } // Proxy (Virtual Proxy - Lazy Loading) public class ProxyImage implements Image { private String filename; private RealImage realImage; // Reference to real subject public ProxyImage(String filename) { this.filename = filename; } @Override public void display() { if (realImage == null) { // Lazy initialization realImage = new RealImage(filename); } realImage.display(); // Delegate to the real image } } // Client public class Client { public static void main(String[] args) { Image image = new ProxyImage("large_image.jpg"); // RealImage is NOT loaded yet System.out.println("Image will be displayed now..."); image.display(); // RealImage is loaded and displayed here // Subsequent calls use the already loaded image image.display(); } } - 优点:
- 简单直观,易于理解和实现。
- 在编译时就能确定代理关系,类型安全。
- 缺点:
- 代码冗余:如果
Subject接口方法众多,代理类需要为每个方法编写转发逻辑(即使只是简单调用realSubject.method())。 - 灵活性差:一个代理类只能服务一种类型的
RealSubject(或一个接口)。如果系统中有大量需要代理的类,需要编写大量几乎相同的代理类,导致类爆炸。
- 代码冗余:如果
2. 动态代理 (Dynamic Proxy)
-
描述: 在运行时动态生成代理类。开发者无需手动编写代理类的源码。Java提供了原生支持(
java.lang.reflect.Proxy)和第三方库(如CGLIB)来实现动态代理。 -
JDK 动态代理 (Interface-based):
- 机制: 基于
InvocationHandler接口和Proxy类。代理对象在运行时动态创建,实现指定的接口,并将所有方法调用分派给一个InvocationHandler对象处理。 - 代码示例(日志代理):
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; // Subject Interface public interface UserService { void addUser(String name); void deleteUser(String name); } // RealSubject public class UserServiceImpl implements UserService { @Override public void addUser(String name) { System.out.println("Adding user: " + name); // Core logic } @Override public void deleteUser(String name) { System.out.println("Deleting user: " + name); // Core logic } } // InvocationHandler (Common Logic: Logging) public class LoggingInvocationHandler implements InvocationHandler { private final Object target; // Real subject object public LoggingInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Pre-operation: Log before method call System.out.println("[Log] Calling method: " + method.getName() + " with args: " + (args != null ? String.join(", ", (String[]) args) : "")); // Call the real method on the target Object result = method.invoke(target, args); // Post-operation: Log after method call (optional: can log result) System.out.println("[Log] Method: " + method.getName() + " executed."); return result; } } // Client public class Client { public static void main(String[] args) { // 1. Create real subject UserService realService = new UserServiceImpl(); // 2. Create InvocationHandler, wrapping the real subject InvocationHandler handler = new LoggingInvocationHandler(realService); // 3. Dynamically create the proxy object UserService proxy = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, handler); // 4. Client uses the proxy proxy.addUser("Alice"); // Logs will be printed proxy.deleteUser("Bob"); // Logs will be printed } } - 优点:
- 减少代码冗余:
InvocationHandler只需编写一次,就能代理UserService接口的所有方法。 - 通用性强: 同一个
InvocationHandler可以代理多个不同对象(只要它们实现了相同接口)。 - 灵活性高: 运行时决定代理关系,易于扩展。
- 减少代码冗余:
- 缺点:
- 只能代理接口: JDK动态代理要求真实对象必须实现一个或多个接口。对于没有实现接口的类无能为力。
- 性能开销: 反射调用 (
method.invoke()) 比直接的方法调用稍慢(虽然现代JVM优化了很多)。 - 类型转换注意: 需要类型转换获取代理对象。
- 目标对象接口变化时要注意同步:如果接口方法改变,InvocationHandler 代码可能需要调整。
- 机制: 基于
-
CGLIB 动态代理 (Class-based):
- 机制: 基于字节码操作(通常使用ASM库),在运行时动态生成被代理类的子类作为代理类。这个子类会重写父类(即被代理类)的非final方法,并将方法调用转发给自定义的
MethodInterceptor。 - 优点:
- 可以代理类: 即使目标类没有实现任何接口,CGLIB也能代理它。这使得它的适用范围比JDK代理更广。
- 性能可能更好: 在某些场景和JVM下,生成的代理类方法调用可能比反射略快(CGLIB 3.x进行了大量优化)。
- 缺点:
- 不能代理final方法和类: 因为代理是基于继承的。
- 依赖外部库: 需要引入CGLIB库 (
cglib:cglib或org.springframework:spring-core通常包含它)。 - 生成过程更复杂: 生成子类字节码的过程可能比JDK动态代理创建接口实现类略慢(尤其在首次加载时)。
- 常用场景: Spring AOP默认对实现了接口的类使用JDK动态代理,对没有实现接口的类使用CGLIB代理。可以通过配置强制使用CGLIB。
- 机制: 基于字节码操作(通常使用ASM库),在运行时动态生成被代理类的子类作为代理类。这个子类会重写父类(即被代理类)的非final方法,并将方法调用转发给自定义的
六、最佳实践
- 优先选择动态代理: 除非代理逻辑极其简单或需要精确控制,否则动态代理(JDK或CGLIB)通常是更优选择,尤其当需要代理多个类或接口方法众多时,它能显著减少样板代码。
- 按需选择代理类型:
- 被代理对象有接口? → JDK动态代理。
- 被代理对象无接口或需要代理类? → CGLIB动态代理。
- 清晰定义抽象接口 (
Subject): 一个良好设计的接口是代理模式成功应用的基础,它清晰地定义了代理和被代理对象的契约。 - 遵循单一职责原则: 让代理专注于访问控制、增强功能(日志、缓存等),而让真实对象专注于核心业务逻辑。避免让代理承担核心业务。
- 理解代理的局限性和开销: 尤其是动态代理,了解其反射开销以及在特定性能要求高的场景下的权衡。
- 与装饰器模式 (Decorator) 区分:
- 意图: 代理的目的是控制访问(提供替代品/占位符)。装饰器的目的是动态添加额外职责(增强功能)。虽然结构相似,但意图是核心区别。
- 关注点: 代理通常与对象的生命周期、访问权限、位置(远程)相关。装饰器则与添加额外的行为(如边框、滚动条)相关。
- 创建: 代理通常在编译时或在特定规则下(如访问权限)由框架或工厂决定。装饰器通常由客户端按需组合添加。
七、在开发中的演变和应用
代理模式是现代软件开发,特别是分布式系统、面向切面编程 (AOP) 和服务治理的核心技术:
- 远程过程调用 (RPC): RPC Stub/Skeleton 本质上是远程代理的自动化实现。它们封装了网络通信、序列化/反序列化等细节,使得客户端可以像调用本地方法一样调用远程服务。代表框架有:gRPC, Apache Thrift, Dubbo。
- 面向切面编程 (AOP): AOP框架(如AspectJ, Spring AOP)的基础就是代理模式。它们通过编译时/运行时生成代理(JDK动态代理或CGLIB),为连接点(方法调用)织入横切关注点(如事务
@Transactional, 日志@Loggable, 安全@Secured)。AspectJ甚至能在编译时修改字节码(ajc)。 - 微服务网关 / API 网关: API网关是代理模式的宏观应用实例。它可以提供路由、负载均衡、认证/授权(保护代理)、限流、熔断、日志/监控(日志代理/智能引用代理)、请求/响应转换(类似适配+代理)等功能,是外部客户端访问内部微服务群的统一入口和控制点。
- 服务网格 (Service Mesh): 在
Sidecar模式(如Istio的Envoy)中,Sidecar代理就是每个服务的智能代理。它处理服务间的通信(如服务发现、负载均衡、重试、熔断、加密、指标收集),对服务本身透明。这是代理模式在分布式系统基础设施层面的典型展现。 - ORM 框架:
- 延迟加载 (Lazy Loading): Hibernate、MyBatis等ORM框架大量使用虚拟代理。当加载一个实体对象时,其关联的对象(如
User的orders集合)通常不会立即加载。框架返回一个代理对象,当客户端代码首次访问这个关联对象(如user.getOrders().size())时,代理才会触发数据库查询去加载真实的关联数据。这提升了首次加载性能和内存使用。 - MyBatis Mapper 接口: MyBatis动态生成
Mapper接口的代理实现类(通常使用JDK动态代理或CGLIB),代理内部实现了SQL语句绑定、参数映射、结果映射、事务管理等。
- 延迟加载 (Lazy Loading): Hibernate、MyBatis等ORM框架大量使用虚拟代理。当加载一个实体对象时,其关联的对象(如
- Mock 测试: Mock框架(如Mockito, EasyMock)利用动态代理技术创建被测对象的“模拟”对象(代理),用于在单元测试中隔离依赖并验证交互行为。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java RMI (Remote Method Invocation):
- 最经典的远程代理实现。
rmic编译器生成 Stub(客户端代理)和 Skeleton(服务端代理)类。客户端通过调用Stub(实现了远程接口Subject) 的本地方法,Stub负责序列化参数、通过网络发送请求给远端的Skeleton,Skeleton反序列化参数、调用真正的远程对象 (RealSubject) 的方法,再序列化结果返回给Stub,最终由Stub反序列化并返回给客户端。Registry(RMI注册表) 充当了代理工厂的角色。
- 最经典的远程代理实现。
-
Spring Framework AOP:
- 核心机制: 当开启AOP支持时,Spring容器在初始化Bean时,会根据AOP配置(通过
@Aspect、@EnableAspectJAutoProxy、XML等)为符合条件的Bean创建代理对象并放入容器,替代原有的Bean。默认策略:- 如果Bean实现了接口 → JDK动态代理。
- 如果Bean没有实现接口 → CGLIB动态代理。
- 核心组件:
AbstractAutoProxyCreator: 自动代理创建器的抽象基类。JdkDynamicAopProxy: JDK动态代理的具体实现。ObjenesisCglibAopProxy/CglibAopProxy: CGLIB动态代理的具体实现。Advisor/Advice: 定义了在何处(Pointcut)执行什么增强逻辑(Advice,如@Before,@After,@Around)。代理对象会执行这些Advice的逻辑(由AOP框架组织的MethodInterceptor链)。
- 效果: 开发者定义的
@Transactional,@Cacheable,@Async等注解通过代理机制生效。
- 核心机制: 当开启AOP支持时,Spring容器在初始化Bean时,会根据AOP配置(通过
-
Spring Cloud OpenFeign:
- 用于声明式REST客户端。开发者定义一个接口(
Subject),并用@FeignClient注解标记。Spring在运行时动态生成该接口的代理实现类(通常使用JDK动态代理或通过工厂创建HttpURLConnection/RestTemplate/WebClient实例)。当客户端调用代理接口的方法时,代理会将方法签名和参数转换为HTTP请求(基于注解如@GetMapping,@PostMapping,@PathVariable,@RequestParam),发送到远程服务,并处理响应和错误。
- 用于声明式REST客户端。开发者定义一个接口(
-
Java 标准库中的
java.net.URL和java.net.URLConnection(概念上):- 在实际进行网络操作前,可以进行一些配置(如超时设置)。虽然内部实现复杂,但其理念与代理类似,封装了底层的Socket通信细节。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 结构型设计模式 |
| 核心意图 | 为其他对象提供一种代理以控制对该对象的访问。引入间接层实现访问控制、功能增强、位置透明等。 |
| 关键角色 | 抽象主题(Subject), 真实主题(RealSubject), 代理(Proxy), 客户端(Client) |
| 核心机制 | 实现相同接口 + 持有真实对象引用 + 委托与增强。代理与真实对象遵循相同的契约 (Subject)。代理管理对真实对象的访问,可添加附加操作。 |
| 主要实现 | 静态代理: 手动编码代理类,简单但冗余。 动态代理: 运行时生成代理。JDK动态代理 (基于接口, 反射, InvocationHandler)。CGLIB动态代理 (基于继承,字节码生成, MethodInterceptor)。 |
| 主要优点 | 1. 职责清晰: 代理处理横切关注点(访问控制、增强),真实对象专注业务。 2. 扩展性强: 动态代理尤其强大灵活。 3. 访问控制: 保护代理实现安全。 4. 优化性能: 虚拟代理延迟加载,缓存代理减少开销。 |
| 主要缺点 | 1. 间接性增加复杂度: 理解、调试可能稍难。 2. 性能开销: 代理调用(尤其动态代理反射)比直接调用稍慢。 3. 潜在复杂性: 动态代理需要理解技术细节(反射、字节码)。 |
| 核心应用场景 | 远程代理 (RPC),虚拟代理 (延迟加载),保护代理 (权限控制),缓存代理,智能引用 (日志、同步),防火墙代理。 |
| 与装饰器模式区别 | 意图不同: 代理目的是访问控制(替代/占位),装饰器目的是动态增强职责(添加功能)。结构相似,关注点不同。 |
| 现代演进与应用 | RPC/微服务核心 (Stub/Skeleton),AOP基石 (Spring AOP),API网关/服务网格,ORM框架延迟加载,Mock测试。 |
| 工业级案例 | Java RMI (经典远程代理),Spring AOP / @Transactional (AOP动态代理),Spring Cloud OpenFeign (声明式HTTP客户端代理),Hibernate 延迟加载 (虚拟代理)。 |
代理模式是软件设计中解决访问控制、间接引用、横切关注点分离等问题的关键技术。从微观层面的对象访问控制(如日志、权限),到宏观层面的分布式通信(RPC)、服务治理(API网关、服务网格),其思想无处不在。深刻理解静态代理和动态代理(JDK, CGLIB)的原理、优缺点及适用场景,特别是其在Spring等主流框架中的核心应用,是成为高级后端架构师不可或缺的能力。它完美诠释了“间接性”带来的强大力量:解耦、控制、扩展。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120422

浙公网安备 33010602011771号