文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

深入浅出设计模式【十二、代理模式】

一、代理模式介绍

在某些场景下,客户端直接访问目标对象可能不合适或存在困难。原因可能是:目标对象位于远程服务器上(网络开销大)、目标对象创建开销巨大需要延迟加载、需要控制对目标对象的访问权限、或者需要在访问前后添加额外操作(如日志、监控)。

代理模式正是为解决这些问题而生。它在客户端和目标对象之间引入一个代理对象。这个代理对象与目标对象(或称真实对象)实现相同的接口,因此客户端通常无法察觉(或不关心)使用的是代理还是真实对象。代理对象负责在需要时将客户端请求转发给真实对象,并可在转发前后执行附加操作,从而实现了访问控制、功能增强等目的。

二、核心概念与意图

  1. 核心概念

    • 主题/抽象接口 (Subject): 声明了真实对象和代理对象共同实现的接口。这使得任何使用真实对象的地方都能透明地使用代理对象。
    • 真实主题/真实对象 (Real Subject): 定义了代理所代表的真实对象。它包含了真正的业务逻辑,是客户端最终想要访问的对象。
    • 代理 (Proxy)模式的核心
      • 持有一个对真实主题的引用(或能访问真实主题)。
      • 实现了与真实主题相同的接口 (Subject)。
      • 客户端访问代理对象。
      • 代理对象可以控制对真实对象的访问,并且在访问真实对象前后可以执行附加操作。
      • 负责在必要时创建和销毁真实对象(如在虚拟代理中)。
  2. 意图

    • 为其他对象提供一种代理以控制对这个对象的访问
    • 在访问一个对象时引入一定程度的间接性,以便根据情况决定直接访问、延迟创建、控制访问或添加增强功能。
    • 实现职责分离,将核心功能保留在真实对象中,将横切关注点(如日志、安全、事务)放在代理中。

三、适用场景剖析

代理模式在以下场景中非常有效:

  1. 远程代理 (Remote Proxy): 为一个位于不同地址空间(如不同JVM、不同服务器)的对象提供本地代表。它将远程调用细节(如网络通信、序列化)封装在代理内部,使客户端感觉像是在调用本地对象(如RMI Stub)。
  2. 虚拟代理 (Virtual Proxy): 用于延迟创建开销大的真实对象。代理会先处理轻量级操作,或者显示一个占位符(如Loading图片),直到真正需要时才创建或加载真实对象(如图片的懒加载、大型文档的分页加载)。
  3. 保护代理 (Protection Proxy): 控制对原始对象的访问权限。代理在访问真实对象前,会检查客户端的访问权限,只有拥有相应权限的请求才会被转发(如权限控制层)。
  4. 缓存代理 (Cache Proxy): 为开销大的操作提供临时存储(缓存)。当客户端请求时,代理首先检查缓存中是否存在有效结果。如果存在,直接返回缓存结果;如果不存在,才调用真实对象的方法,并将结果放入缓存后返回(如数据库查询结果缓存)。
  5. 智能引用代理 (Smart Reference Proxy): 在访问真实对象时执行附加的智能操作。例如:
    • 引用计数:自动释放不再使用的昂贵对象。
    • 首次引用时加载对象。
    • 访问真实对象前加锁,确保线程安全(同步代理)。
  6. 日志记录代理 (Logging Proxy): 记录方法调用的参数、返回值、执行时间等信息,用于调试、监控或审计。
  7. 防火墙代理 (Firewall Proxy): 保护网络免受恶意请求的侵害,通常用于内部网络和外部网络(如Internet)之间的缓冲区域。

四、UML 类图解析(Mermaid)

以下UML类图清晰地展示了代理模式的结构和角色间的关系:

uses (may create & manage)
Client
«interface»
Subject
+request()
RealSubject
+request()
Proxy
-realSubject: RealSubject
+request()
  • Subject (抽象主题/接口): 定义了 RealSubjectProxy 的公共接口。客户端依赖于这个接口。
  • 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:cgliborg.springframework:spring-core 通常包含它)。
      • 生成过程更复杂: 生成子类字节码的过程可能比JDK动态代理创建接口实现类略慢(尤其在首次加载时)。
    • 常用场景: Spring AOP默认对实现了接口的类使用JDK动态代理,对没有实现接口的类使用CGLIB代理。可以通过配置强制使用CGLIB。

六、最佳实践

  1. 优先选择动态代理: 除非代理逻辑极其简单或需要精确控制,否则动态代理(JDK或CGLIB)通常是更优选择,尤其当需要代理多个类或接口方法众多时,它能显著减少样板代码。
  2. 按需选择代理类型
    • 被代理对象有接口? → JDK动态代理
    • 被代理对象无接口或需要代理类? → CGLIB动态代理
  3. 清晰定义抽象接口 (Subject): 一个良好设计的接口是代理模式成功应用的基础,它清晰地定义了代理和被代理对象的契约。
  4. 遵循单一职责原则: 让代理专注于访问控制、增强功能(日志、缓存等),而让真实对象专注于核心业务逻辑。避免让代理承担核心业务。
  5. 理解代理的局限性和开销: 尤其是动态代理,了解其反射开销以及在特定性能要求高的场景下的权衡。
  6. 与装饰器模式 (Decorator) 区分
    • 意图代理的目的是控制访问(提供替代品/占位符)。装饰器的目的是动态添加额外职责(增强功能)。虽然结构相似,但意图是核心区别。
    • 关注点: 代理通常与对象的生命周期、访问权限、位置(远程)相关。装饰器则与添加额外的行为(如边框、滚动条)相关。
    • 创建: 代理通常在编译时或在特定规则下(如访问权限)由框架或工厂决定。装饰器通常由客户端按需组合添加。

七、在开发中的演变和应用

代理模式是现代软件开发,特别是分布式系统面向切面编程 (AOP)服务治理的核心技术:

  1. 远程过程调用 (RPC)RPC Stub/Skeleton 本质上是远程代理的自动化实现。它们封装了网络通信、序列化/反序列化等细节,使得客户端可以像调用本地方法一样调用远程服务。代表框架有:gRPC, Apache Thrift, Dubbo。
  2. 面向切面编程 (AOP): AOP框架(如AspectJ, Spring AOP)的基础就是代理模式。它们通过编译时/运行时生成代理(JDK动态代理或CGLIB),为连接点(方法调用)织入横切关注点(如事务 @Transactional, 日志 @Loggable, 安全 @Secured)。AspectJ 甚至能在编译时修改字节码(ajc)。
  3. 微服务网关 / API 网关: API网关是代理模式的宏观应用实例。它可以提供路由、负载均衡、认证/授权(保护代理)、限流、熔断、日志/监控(日志代理/智能引用代理)、请求/响应转换(类似适配+代理)等功能,是外部客户端访问内部微服务群的统一入口和控制点。
  4. 服务网格 (Service Mesh): 在 Sidecar 模式(如Istio的Envoy)中,Sidecar代理就是每个服务的智能代理。它处理服务间的通信(如服务发现、负载均衡、重试、熔断、加密、指标收集),对服务本身透明。这是代理模式在分布式系统基础设施层面的典型展现。
  5. ORM 框架
    • 延迟加载 (Lazy Loading): Hibernate、MyBatis等ORM框架大量使用虚拟代理。当加载一个实体对象时,其关联的对象(如Userorders集合)通常不会立即加载。框架返回一个代理对象,当客户端代码首次访问这个关联对象(如user.getOrders().size())时,代理才会触发数据库查询去加载真实的关联数据。这提升了首次加载性能和内存使用。
    • MyBatis Mapper 接口: MyBatis动态生成 Mapper 接口的代理实现类(通常使用JDK动态代理或CGLIB),代理内部实现了SQL语句绑定、参数映射、结果映射、事务管理等。
  6. Mock 测试: Mock框架(如Mockito, EasyMock)利用动态代理技术创建被测对象的“模拟”对象(代理),用于在单元测试中隔离依赖并验证交互行为。

八、真实开发案例(Java语言内部、知名开源框架、工具)

  1. Java RMI (Remote Method Invocation)

    • 最经典的远程代理实现。rmic 编译器生成 Stub(客户端代理)和 Skeleton(服务端代理)类。客户端通过调用 Stub (实现了远程接口 Subject) 的本地方法,Stub 负责序列化参数、通过网络发送请求给远端的 SkeletonSkeleton 反序列化参数、调用真正的远程对象 (RealSubject) 的方法,再序列化结果返回给 Stub,最终由 Stub 反序列化并返回给客户端。Registry (RMI注册表) 充当了代理工厂的角色。
  2. 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 等注解通过代理机制生效。
  3. Spring Cloud OpenFeign

    • 用于声明式REST客户端。开发者定义一个接口(Subject),并用 @FeignClient 注解标记。Spring在运行时动态生成该接口的代理实现类(通常使用JDK动态代理或通过工厂创建HttpURLConnection/RestTemplate/WebClient实例)。当客户端调用代理接口的方法时,代理会将方法签名和参数转换为HTTP请求(基于注解如@GetMapping, @PostMapping, @PathVariable, @RequestParam),发送到远程服务,并处理响应和错误。
  4. Java 标准库中的 java.net.URLjava.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等主流框架中的核心应用,是成为高级后端架构师不可或缺的能力。它完美诠释了“间接性”带来的强大力量:解耦、控制、扩展。

posted @ 2025-08-29 13:21  NeoLshu  阅读(5)  评论(0)    收藏  举报  来源