一、AOP 的本质:为什么需要代理模式?
假设有如下业务场景,电商项目中,需要给订单服务的createOrder()、cancelOrder()等方法添加日志记录和事务控制。如果直接在每个方法里硬编码,不仅代码冗余,后期要修改日志格式都得逐个调整,简直是维护噩梦。
这就是 AOP 要解决的核心问题:将日志、事务等横切逻辑与业务逻辑解耦。而实现这种 "无侵入增强" 的关键技术,就是代理模式 ------ 通过创建一个代理对象,在调用目标方法前后嵌入增强逻辑,同时保证调用者无感知。
Spring AOP 提供了两种代理实现:JDK 动态代理和 CGLIB 代理,它们的适用场景和实现机制有着本质区别。
二、JDK 动态代理:接口驱动的增强方案
JDK 动态代理是 JDK 原生支持的代理方式,不需要额外依赖,这也是 Spring 默认优先选择它的原因。但它有个硬性要求:目标类必须实现接口。
1. 源码级理解 JDK 代理
JDK 动态代理的核心是java.lang.reflect.Proxy类和InvocationHandler接口。一段简化的订单服务代理实现:
java
// 订单服务接口
public interface OrderService {
void createOrder(String orderNo);
}
// 接口实现类
public class OrderServiceImpl implements OrderService {
@Override
public void createOrder(String orderNo) {
System.out.println("创建订单:" + orderNo);
}
}
// 日志增强处理器
public class LogInvocationHandler implements InvocationHandler {
private Object target; // 目标对象
public LogInvocationHandler(Object target) {
this.target = target;
}
// 代理方法:所有对代理对象的调用都会走到这里
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("日志开始:" + method.getName());
Object result = method.invoke(target, args); // 调用目标方法
System.out.println("日志结束:" + method.getName());
return result;
}
}
// 测试代码
public class JdkProxyDemo {
public static void main(String[] args) {
OrderService target = new OrderServiceImpl();
// 创建代理对象
OrderService proxy = (OrderService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(), // 必须传入接口
new LogInvocationHandler(target)
);
proxy.createOrder("20240501001");
}
}
运行后会发现,日志逻辑成功嵌入到了订单创建前后。这里有个细节:通过proxy.getClass().getName()会发现代理类名是com.sun.proxy.$Proxy0,这是 JVM 在运行时动态生成的字节码文件,它实现了OrderService接口,并重写了接口方法。
2. JDK 代理的优缺点实战总结
在实际项目中,我总结出 JDK 动态代理的几个特点:
- 接口依赖 :如果目标类没实现接口(比如一些遗留系统的 POJO),直接抛出
ClassCastException - 性能表现:代理类生成速度快,但每次调用都要通过反射执行目标方法,高频调用场景下性能略逊
- 灵活性:可以代理多个接口,适合面向接口编程的架构
在高频交易系统中,使用 JDK 代理会导致反射调用开销累积,改用 CGLIB 后接口响应时间会明显降低
三、CGLIB 代理:继承式的增强方案
当目标类没有实现接口时,Spring 会自动切换到 CGLIB 代理。CGLIB(Code Generation Library)是一个字节码生成库,它通过继承目标类生成代理对象,这也是它能突破接口限制的原因。
1. CGLIB 的底层实现原理
CGLIB 的核心是Enhancer类和MethodInterceptor接口。还是以订单服务为例:
java
// 没有实现接口的订单服务
public class OrderService {
public void createOrder(String orderNo) {
System.out.println("创建订单:" + orderNo);
}
// final方法无法被代理
public final void cancelOrder() {
System.out.println("取消订单");
}
}
// 日志拦截器
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("日志开始:" + method.getName());
Object result = proxy.invokeSuper(obj, args); // 调用父类(目标类)方法
System.out.println("日志结束:" + method.getName());
return result;
}
}
// 测试代码
public class CglibProxyDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderService.class); // 设置父类
enhancer.setCallback(new LogMethodInterceptor());
OrderService proxy = (OrderService) enhancer.create(); // 创建代理对象
proxy.createOrder("20240501001"); // 会被增强
proxy.cancelOrder(); // final方法不会被增强
}
}
运行后会发现,createOrder()被成功增强,但cancelOrder()因为被final修饰,无法被 CGLIB 重写,所以没有日志输出。这是一个很容易踩的坑,在实际开发中要特别注意。
2. CGLIB 的实战注意事项
根据我的项目经验,使用 CGLIB 需要注意这些点:
- 继承限制 :目标类不能是
final的,方法也不能是final的,否则无法生成代理 - 性能特点:代理类生成时需要操作字节码,初始化速度较慢,但执行时不需要反射,所以高频调用场景下表现更好
- 内存占用:生成的代理类比 JDK 代理更复杂,内存占用略高
在 Spring Boot 2.x 中,默认配置spring.aop.proxy-target-class=true,也就是优先使用 CGLIB。这个配置曾让我排查过一个问题:升级 Spring Boot 后,某些依赖接口的代理逻辑突然失效,后来发现是因为 CGLIB 代理的是类而不是接口,导致注入时类型匹配出错。
四、Spring AOP 的代理选择策略
Spring AOP 并不是简单地二选一,而是有一套智能的选择逻辑:
- 默认策略:如果目标类实现了接口,用 JDK 动态代理;否则用 CGLIB
- 强制切换 :通过
proxy-target-class=true强制使用 CGLIB(Spring Boot 2.x 默认开启) - 特殊情况 :即使实现了接口,如果目标对象是
final类,也会切换到 CGLIB(但此时会抛异常,因为无法继承 final 类)
在调试时,我们可以通过打印代理对象的类名快速判断:
- JDK 代理类名含
$Proxy前缀 - CGLIB 代理类名含
$$EnhancerByCGLIB$$前缀
比如在 Spring 中获取 Bean 后打印:
java
System.out.println(orderService.getClass().getName());
// JDK代理: com.sun.proxy.$Proxy12
// CGLIB代理: com.example.OrderService$$EnhancerByCGLIB$$5a3d2b3f
五、实战抉择:该用哪种代理?
结合多年开发经验,我总结了一套选择建议:
| 场景 | 推荐代理方式 | 理由 |
|---|---|---|
| 面向接口编程 | JDK 动态代理 | 符合设计原则,代理逻辑更清晰 |
| 无接口的类 | CGLIB | 唯一选择 |
| 单例 Bean | CGLIB | 初始化成本一次摊销,执行效率更高 |
| 多例 Bean | JDK 动态代理 | 避免频繁生成代理类的性能开销 |
| 高频调用方法 | CGLIB | 减少反射调用的性能损耗 |
在实际项目中,除非有特殊需求,建议使用 Spring 的默认策略。如果需要强制切换,一定要做充分的测试,特别是注意 final 方法、类型注入等潜在问题。
六、常见误区澄清
- "AOP 就是代理":不准确,代理只是 AOP 的实现手段之一,AOP 还包括切入点表达式解析、通知类型等完整体系
- "CGLIB 一定比 JDK 好":在 Bean 频繁创建的场景(如多例),JDK 代理的初始化速度优势更明显
- "Spring AOP 只能用这两种代理":其实还可以集成 AspectJ 实现编译期织入,适合对性能要求极高的场景