【Spring6笔记】 - 12 - 代理模式
什么是代理模式?
代理模式是大名鼎鼎的 GoF 23种设计模式 之一,属于结构型模式。
核心定义: 为其他对象提供一种代理以控制对这个对象的访问。 通俗地说,代理对象就像是一个"中介"或者"经纪人"。客户端不再直接与目标对象打交道,而是通过代理对象来间接访问。
代理模式的作用
- 功能增强: 在执行目标对象的方法前后,添加额外的逻辑(如:性能监控、日志、事务、安全校验)。
- 控制访问: 隐藏目标对象的具体实现细节,保护目标对象。
- 解耦: 让目标对象只关注核心业务,而将公共的边角料逻辑交给代理类处理。
代理模式中的三个重要角色
- 抽象角色(Subject): 业务接口。声明目标对象和代理对象共同遵循的业务规范。
- 目标角色(Real Subject): 真正执行业务逻辑的类,即"被代理对象"。
- 代理角色(Proxy): 内部持有目标对象的引用。它负责在调用目标方法前后进行"环境预处理"或"结果后处理"。
1. 静态代理 (Static Proxy)
1.1 核心概念与背景
在软件开发中,我们经常面临一个需求:在不修改原有业务代码的基础上,为业务方法添加额外的功能(例如:性能统计、日志记录、事务管理等)。
-
需求案例 :统计
OrderService接口中所有业务方法的执行耗时。 -
原始代码结构:
-
接口:
OrderService
javapublic interface OrderService { void generate(); void modify(); void detail(); }- 实现类(目标对象):
OrderServiceImpl
-
1.2 解决方案的演进与对比
在引入静态代理之前,通常会有以下两种尝试,但它们都存在明显的缺陷:
方案一:硬编码修改 (Direct Modification)
直接在 OrderServiceImpl 的每个方法中编写统计耗时的逻辑。
-
代码示例:
javapublic void generate() { long begin = System.currentTimeMillis(); // 核心业务逻辑... long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - begin)); } -
缺点:
- 违背 OCP (开闭原则):对修改开放了,每次增加监控都要改动源代码。
- 代码重复:同样的统计逻辑散落在各个方法中,无法复用。
方案二:子类继承 (Inheritance / IS-A)
编写 OrderServiceImplSub 继承 OrderServiceImpl,重写方法并调用 super.method()。
-
代码示例:
javapublic class OrderServiceImplSub extends OrderServiceImpl { @Override public void generate() { long begin = System.currentTimeMillis(); super.generate(); long end = System.currentTimeMillis(); } } -
缺点:
- 高耦合:继承是 Java 中耦合度最高的类关系。子类深度依赖父类的结构。
- 代码冗余:依然需要在每个重写的方法里写重复的统计逻辑。
1.3 静态代理的实现 (Association / HAS-A)
静态代理 是通过关联关系 (组合/聚合)来实现的。代理类与目标类实现相同的接口 ,代理类内部持有目标类的引用。
核心三要素:
- 公共接口 :定义业务规范(如
OrderService)。 - 目标对象 (Target) :真正的业务实现类(如
OrderServiceImpl)。 - 代理对象 (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 静态代理的优缺点分析
优点:
- 符合 OCP 原则:不需要修改目标对象的源代码,即可完成功能的增强。
- 降低耦合度:相比继承关系,关联关系(HAS-A)更加灵活,符合"合成复用原则"。
- 职责清晰:目标对象只关注核心业务,代理对象只关注公共的辅助逻辑(如耗时统计)。
缺点(核心痛点):
- 类爆炸 (Class Explosion) :
- 这是静态代理最严重的缺点。
- 由于代理类在编译阶段就已经固定,一个接口通常对应一个代理类。如果系统中有 1000 个接口,为了实现耗时统计,就需要手动编写 1000 个代理类。
- 维护成本高 :
- 如果接口增加一个方法,目标类和代理类都必须同时修改,代码维护非常繁琐。
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 规定,所有动态生成的代理对象在调用方法时 ,都会转而执行 InvocationHandler 的 invoke 方法。
代码实现:
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 会在内存中动态拼接字节码。
方法参数详解:
ClassLoader loader:- 作用:类加载器。生成的字节码 class 必须加载到内存才能运行。
- 要求:JDK 要求目标类的加载器与代理类的加载器必须是同一个。
Class<?>[] interfaces:- 作用:代理类需要实现的接口列表。
- 含义:告诉 JDK 代理类要长什么样(具备哪些业务方法)。
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 动态代理执行流程图解
- 调用 :客户端执行
proxy.generate()。 - 转发 :JVM 将调用转发给
InvocationHandler的invoke()方法。 - 增强 :在
invoke()中执行预处理逻辑(计时开始)。 - 反射 :通过
method.invoke(target, args)真正执行目标对象的业务代码。 - 后处理:执行后置增强(计时结束,打印结果)。
- 返回:将业务方法的返回值返回给客户端。
2.1.6 为什么说它解决了静态代理的缺点?
- 解决类爆炸 :即使有 1000 个接口,我们也只需要写 一个
TimeInvocationHandler。代理类是在运行时动态生成的,不需要程序员手动创建.java文件。 - 解耦:增强逻辑(计时)与业务逻辑(订单处理)完全分离,互不干扰。
2.2 CGLIB 动态代理
2.2.1 核心原理
CGLIB (Code Generation Library) 是一个强大的、高性能的字节码生成库。它的原理是:在内存中动态生成目标类的子类,并重写父类(目标类)的方法以实现增强。
- 核心特性 :
- 无需接口:目标类可以是一个普通的 POJO 类,不需要实现任何接口。
- 继承机制 :代理类通过
extends目标类来接管方法。 - 底层技术:使用 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 的核心类,负责配置代理并生成最终的代理对象。
创建步骤:
- 实例化
Enhancer对象。 - 设置父类 (
setSuperclass):告诉 CGLIB 目标类是谁,生成的代理类将继承它。 - 设置回调 (
setCallback) :设置拦截器逻辑(如TimerMethodInterceptor)。 - 创建代理对象 (
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 动态代理的意义总结
- 解耦:将非业务代码(计时、事务、安全)与业务代码分离。
- 复用:一个拦截器可以应用于成千上万个目标类。
- 零侵入:在不触动源代码的前提下,通过 AOP(面向切面编程)实现了功能的灵活扩展。