【Spring6笔记】 - 12 - 代理模式

【Spring6笔记】 - 12 - 代理模式

什么是代理模式?

代理模式是大名鼎鼎的 GoF 23种设计模式 之一,属于结构型模式

核心定义: 为其他对象提供一种代理以控制对这个对象的访问。 通俗地说,代理对象就像是一个"中介"或者"经纪人"。客户端不再直接与目标对象打交道,而是通过代理对象来间接访问。

代理模式的作用

  1. 功能增强: 在执行目标对象的方法前后,添加额外的逻辑(如:性能监控、日志、事务、安全校验)。
  2. 控制访问: 隐藏目标对象的具体实现细节,保护目标对象。
  3. 解耦: 让目标对象只关注核心业务,而将公共的边角料逻辑交给代理类处理。

代理模式中的三个重要角色

  • 抽象角色(Subject): 业务接口。声明目标对象和代理对象共同遵循的业务规范。
  • 目标角色(Real Subject): 真正执行业务逻辑的类,即"被代理对象"。
  • 代理角色(Proxy): 内部持有目标对象的引用。它负责在调用目标方法前后进行"环境预处理"或"结果后处理"。

1. 静态代理 (Static Proxy)

1.1 核心概念与背景

在软件开发中,我们经常面临一个需求:在不修改原有业务代码的基础上,为业务方法添加额外的功能(例如:性能统计、日志记录、事务管理等)。

  • 需求案例 :统计 OrderService 接口中所有业务方法的执行耗时。

  • 原始代码结构

    • 接口:OrderService

    java 复制代码
    public interface OrderService {
        void generate();
        void modify();
        void detail();
    }
    • 实现类(目标对象):OrderServiceImpl

1.2 解决方案的演进与对比

在引入静态代理之前,通常会有以下两种尝试,但它们都存在明显的缺陷:

方案一:硬编码修改 (Direct Modification)

直接在 OrderServiceImpl 的每个方法中编写统计耗时的逻辑。

  • 代码示例

    java 复制代码
    public void generate() {
        long begin = System.currentTimeMillis();
        // 核心业务逻辑...
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - begin));
    }
  • 缺点

    1. 违背 OCP (开闭原则):对修改开放了,每次增加监控都要改动源代码。
    2. 代码重复:同样的统计逻辑散落在各个方法中,无法复用。
方案二:子类继承 (Inheritance / IS-A)

编写 OrderServiceImplSub 继承 OrderServiceImpl,重写方法并调用 super.method()

  • 代码示例

    java 复制代码
    public class OrderServiceImplSub extends OrderServiceImpl {
        @Override
        public void generate() {
            long begin = System.currentTimeMillis();
            super.generate();
            long end = System.currentTimeMillis();
        }
    }
  • 缺点

    1. 高耦合:继承是 Java 中耦合度最高的类关系。子类深度依赖父类的结构。
    2. 代码冗余:依然需要在每个重写的方法里写重复的统计逻辑。

1.3 静态代理的实现 (Association / HAS-A)

静态代理 是通过关联关系 (组合/聚合)来实现的。代理类与目标类实现相同的接口代理类内部持有目标类的引用

核心三要素:
  1. 公共接口 :定义业务规范(如 OrderService)。
  2. 目标对象 (Target) :真正的业务实现类(如 OrderServiceImpl)。
  3. 代理对象 (Proxy) :负责增强逻辑,并调用目标对象的方法(如 OrderServiceProxy)。
代码实现:
java 复制代码
/**
 * 代理类:实现了与目标类相同的接口
 */
public class OrderServiceProxy implements OrderService {
    
    // 持有目标对象的引用 (关联关系,has-a)
    // 建议使用接口类型,提高灵活性
    private OrderService target;

    // 通过构造方法注入目标对象
    public OrderServiceProxy(OrderService target) {
        this.target = target;
    }

    @Override
    public void generate() {
        // 1. 增强逻辑:开始时间
        long begin = System.currentTimeMillis();
        
        // 2. 调用目标对象的核心业务方法
        target.generate();
        
        // 3. 增强逻辑:结束时间
        long end = System.currentTimeMillis();
        System.out.println("订单生成耗时:" + (end - begin) + "毫秒");
    }
    
    // ... modify 和 detail 方法同理
}

1.4 静态代理的优缺点分析

优点:
  1. 符合 OCP 原则:不需要修改目标对象的源代码,即可完成功能的增强。
  2. 降低耦合度:相比继承关系,关联关系(HAS-A)更加灵活,符合"合成复用原则"。
  3. 职责清晰:目标对象只关注核心业务,代理对象只关注公共的辅助逻辑(如耗时统计)。
缺点(核心痛点):
  1. 类爆炸 (Class Explosion)
    • 这是静态代理最严重的缺点。
    • 由于代理类在编译阶段就已经固定,一个接口通常对应一个代理类。如果系统中有 1000 个接口,为了实现耗时统计,就需要手动编写 1000 个代理类。
  2. 维护成本高
    • 如果接口增加一个方法,目标类和代理类都必须同时修改,代码维护非常繁琐。

1.5 小结

静态代理解决了"怎么增强"的问题,但没解决"规模化增强"的问题。为了解决 "类爆炸""硬编码代理类" 的问题,我们需要引入 动态代理技术

2. 动态代理 (Dynamic Proxy)

动态代理是 Spring AOP 的底层基石。它解决了静态代理"类爆炸"的痛点,实现了"一次编写,到处增强"。

2.1 JDK 动态代理

2.1.1 核心原理

JDK 动态代理是 Java 原生提供的代理技术 。它的核心在于:在程序运行期间,通过反射机制动态地在内存中生成代理类的字节码 (.class) 并实例化对象

  • 特点 :目标类必须实现接口 。因为生成的代理类默认继承了 java.lang.reflect.Proxy,由于 Java 不支持多继承,所以只能通过实现接口来达到与目标类行为一致的目的。

2.1.2 核心组件一:调用处理器 InvocationHandler

这是我们编写"增强逻辑"的地方。JDK 规定,所有动态生成的代理对象在调用方法时 ,都会转而执行 InvocationHandlerinvoke 方法。

代码实现:

java 复制代码
public class TimeInvocationHandler implements InvocationHandler {
    // 目标对象(被代理的对象)
    private Object target;

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    /**
     * @param proxy  代理对象的引用(很少用到)
     * @param method 当前执行的目标方法对象
     * @param args   目标方法的实际参数
     * @return       目标方法的返回值
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. 【增强代码】:记录开始时间
        long begin = System.currentTimeMillis();

        // 2. 【核心业务】:调用目标对象的方法(通过反射)
        // 四要素:哪个对象 (target)、哪个方法 (method)、传什么参 (args)、返回什么值 (resultValue)
        Object resultValue = method.invoke(target, args);

        // 3. 【增强代码】:记录结束时间
        long end = System.currentTimeMillis();
        System.out.println(method.getName() + " 方法耗时:" + (end - begin) + "毫秒");

        // 返回目标方法的执行结果
        return resultValue;
    }
}

2.1.3 核心组件二:代理创建工厂 Proxy.newProxyInstance()

这是生成代理对象的"工厂方法"。调用此方法时,JDK 会在内存中动态拼接字节码。

方法参数详解:

  1. ClassLoader loader
    • 作用:类加载器。生成的字节码 class 必须加载到内存才能运行。
    • 要求:JDK 要求目标类的加载器与代理类的加载器必须是同一个。
  2. Class<?>[] interfaces
    • 作用:代理类需要实现的接口列表。
    • 含义:告诉 JDK 代理类要长什么样(具备哪些业务方法)。
  3. InvocationHandler h
    • 作用:关联调用处理器。
    • 含义:告诉代理对象,方法被触发时去哪里执行增强逻辑。

2.1.4 调用示例
java 复制代码
@Test
public void testDynamicProxy() {
    // 1. 创建目标对象
    OrderService target = new OrderServiceImpl();

    // 2. 动态创建代理对象
    OrderService proxy = (OrderService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(), // 类加载器
            target.getClass().getInterfaces(),  // 目标实现的接口
            new TimeInvocationHandler(target)   // 增强逻辑
    );

    // 3. 调用代理方法
    proxy.generate(); // 内部触发 TimeInvocationHandler 的 invoke 方法
    String name = proxy.getName(); 
    System.out.println("获取到的名称: " + name);
}

2.1.5 JDK 动态代理执行流程图解
  1. 调用 :客户端执行 proxy.generate()
  2. 转发 :JVM 将调用转发给 InvocationHandlerinvoke() 方法。
  3. 增强 :在 invoke() 中执行预处理逻辑(计时开始)。
  4. 反射 :通过 method.invoke(target, args) 真正执行目标对象的业务代码。
  5. 后处理:执行后置增强(计时结束,打印结果)。
  6. 返回:将业务方法的返回值返回给客户端。
2.1.6 为什么说它解决了静态代理的缺点?
  • 解决类爆炸 :即使有 1000 个接口,我们也只需要写 一个 TimeInvocationHandler。代理类是在运行时动态生成的,不需要程序员手动创建 .java 文件。
  • 解耦:增强逻辑(计时)与业务逻辑(订单处理)完全分离,互不干扰。

2.2 CGLIB 动态代理

2.2.1 核心原理

CGLIB (Code Generation Library) 是一个强大的、高性能的字节码生成库。它的原理是:在内存中动态生成目标类的子类,并重写父类(目标类)的方法以实现增强。

  • 核心特性
    1. 无需接口:目标类可以是一个普通的 POJO 类,不需要实现任何接口。
    2. 继承机制 :代理类通过 extends 目标类来接管方法。
    3. 底层技术:使用 ASM 字节码框架来生成代理类的字节码。

2.2.2 核心组件一:方法拦截器 MethodInterceptor

在 CGLIB 中,增强逻辑写在 MethodInterceptor 接口的实现类里,它类似于 JDK 动态代理中的 InvocationHandler

代码实现:

java 复制代码
public class TimerMethodInterceptor implements MethodInterceptor {
    /**
     * @param obj    代理对象本身
     * @param method 目标方法对象
     * @param args   方法实参
     * @param proxy  代理方法(用于调用父类方法,性能更高)
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 1. 前置增强
        long begin = System.currentTimeMillis();

        // 2. 调用目标对象的目标方法
        // 注意:CGLIB 是基于继承的,obj 就是代理对象(也是子类对象),
        // 它的父类就是目标类。因此这里调用的是 invokeSuper (调用父类方法)
        Object resultValue = proxy.invokeSuper(obj, args);

        // 3. 后置增强
        long end = System.currentTimeMillis();
        System.out.println(method.getName() + "耗时:" + (end - begin) + "毫秒");

        return resultValue;
    }
}

2.2.3 核心组件二:字节码增强器 Enhancer

Enhancer 是 CGLIB 的核心类,负责配置代理并生成最终的代理对象。

创建步骤:

  1. 实例化 Enhancer 对象
  2. 设置父类 (setSuperclass):告诉 CGLIB 目标类是谁,生成的代理类将继承它。
  3. 设置回调 (setCallback) :设置拦截器逻辑(如 TimerMethodInterceptor)。
  4. 创建代理对象 (create)
    • 在内存中生成字节码文件。
    • 实例化对象。

2.2.4 客户端调用与特征
java 复制代码
@Test
public void testCGLIBProxy() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(UserService.class); // 目标类
    enhancer.setCallback(new TimerMethodInterceptor());

    // 生成代理对象
    UserService userServiceProxy = (UserService) enhancer.create();

    // 观察类名特征
    // 格式通常为:目标类名$$EnhancerByCGLIB$$哈希值
    System.out.println(userServiceProxy); 

    userServiceProxy.login("admin", "123456");
}

2.3 JDK 与 CGLIB 的深度对比

我们可以用表格来直观对比:

特性 JDK 动态代理 CGLIB 动态代理
实现原理 基于接口(生成的代理类实现目标接口) 基于继承(生成的代理类继承目标类)
约束条件 目标类必须实现接口 目标类不能被 final 修饰,方法不能是 final/static
底层库 JDK 自带反射机制 第三方库(Spring 已集成)
代理对象类名 com.sun.proxy.$Proxy0 ...$$EnhancerByCGLIB$$...
性能 在较新 JDK 版本中,反射效率已极高 理论上调用父类方法比反射稍快,但生成类的时间略长
Spring 选择 如果目标类实现了接口,默认用 JDK 如果没实现接口,强制用 CGLIB

2.4 动态代理的意义总结

  1. 解耦:将非业务代码(计时、事务、安全)与业务代码分离。
  2. 复用:一个拦截器可以应用于成千上万个目标类。
  3. 零侵入:在不触动源代码的前提下,通过 AOP(面向切面编程)实现了功能的灵活扩展。
相关推荐
lifallen2 小时前
Paimon 与 ForSt 场景选型分析
java·大数据·flink
我命由我123453 小时前
U 盘里出现的文件 BOOTEX.LOG
运维·服务器·经验分享·笔记·学习·硬件工程·学习方法
Rick19933 小时前
Spring Cloud 原理是什么?
后端·spring·spring cloud
潇洒畅想3 小时前
1.2 希腊字母速查表 + 公式阅读实战
java·人工智能·python·算法·rust·云计算
Thexhy3 小时前
Java 后端完整成长路线(含项目)
java·开发语言
27669582923 小时前
携程旅行 token1005
java·linux·前端·javascript·携程旅行·token1005·携程酒店
墨着染霜华3 小时前
Linux 下查看 Java 服务进程占用(CPU / 内存)并定位具体服务
java·linux·运维
楚辞大魔王3 小时前
通过ExternalTools打开编译之后的class
java·开发语言
九成宫3 小时前
IT项目管理期末复习——Chapter 5 项目范围管理
笔记·项目管理·软件工程