JDK动态代理与CGLIB#
一句话答案#
JDK 代理基于接口(Proxy+InvocationHandler),CGLIB 基于继承(字节码生成子类),Spring 默认有接口用 JDK 否则用 CGLIB。
核心要点
AOP(Aspect-Oriented Programming,面向切面编程):
- 通过代理模式,在不修改原始代码的情况下,在方法调用前后插入横切逻辑(日志、事务、权限等)
Spring AOP 的两种代理机制:
1. JDK 动态代理(interface-based)
// 要求:目标类必须实现至少一个接口
// 原理:运行时生成实现了相同接口的代理类
interface UserService { void save(); }
class UserServiceImpl implements UserService { ... }
// JDK 动态代理生成的代理类(伪代码):
class $Proxy0 implements UserService {
InvocationHandler h;
public void save() {
h.invoke(this, saveMethod, args); // 调用切面逻辑
}
}java2. CGLIB(subclass-based)
// 不要求接口,通过继承生成目标类的子类(代理类)
// 原理:字节码增强,ASM 生成子类,重写所有方法
class UserServiceImpl {
public void save() { ... }
}
// CGLIB 生成的代理类(伪代码):
class UserServiceImpl$$EnhancerByCGLIB extends UserServiceImpl {
@Override
public void save() {
// 切面逻辑(前置通知)
super.save(); // 调用原方法
// 切面逻辑(后置通知)
}
}javaJDK 动态代理 vs CGLIB 对比:
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 要求 | 目标类必须有接口 | 无要求(但不能代理 final 类/方法) |
| 实现方式 | 反射 + Proxy.newProxyInstance | ASM 字节码操作,生成子类 |
| 性能 | 调用时反射,略慢(JDK 8 后差距减小) | 直接调用子类方法,更快 |
| Spring 默认 | 有接口 → JDK(Spring Boot 2.x 后默认改为 CGLIB) | 无接口 → CGLIB |
| Spring Boot | spring.aop.proxy-target-class=true(默认)→ 强制用 CGLIB |
深挖:CGLIB 为什么比 JDK 代理快——MethodProxy + FastClass
JDK 代理调原方法走的是反射
Method.invoke(),反射要做权限检查、参数装箱、查方法表,有固定开销。CGLIB 不用反射:它为目标类和代理类各生成一个 FastClass,把每个方法映射成一个 int 索引,调用时通过MethodProxy.invokeSuper()根据索引直接switch-case定位到对应方法直接调用(等价于普通方法调用),绕过了反射。所以拦截器里拿到的是
MethodProxy而非Method:methodProxy.invokeSuper(obj, args)调的是父类(目标类)原方法,走 FastClass 直接派发——这才是 CGLIB 比 JDK 代理略快的真正原因,不是「字节码生成」本身快。代价是首次要额外生成 FastClass 类、启动稍慢、占用更多 Metaspace。
AOP 相关概念:
Aspect(切面) = 横切逻辑的模块(如日志切面、事务切面)
JoinPoint(连接点)= 可以被拦截的方法执行点
Pointcut(切入点)= 选择哪些 JoinPoint 被拦截(表达式:execution(* com..*Service.*(..)))
Advice(通知) = 在 JoinPoint 执行的动作(Before/After/Around/AfterReturning/AfterThrowing)plaintext面试回答(2分钟版)
Spring AOP 的底层代理有两种实现。JDK 动态代理基于接口,运行时通过 Proxy 和 InvocationHandler 生成实现了相同接口的代理类,调用方法时通过反射转发到 InvocationHandler 的 invoke 方法,在其中织入切面逻辑。CGLIB 则不要求目标类有接口,它利用 ASM 字节码技术在运行时生成目标类的子类,通过重写方法来插入增强逻辑,但无法代理 final 类和 final 方法。Spring 早期的策略是有接口用 JDK 代理、没接口用 CGLIB,但从 Spring Boot 2.x 开始默认配置 proxy-target-class=true,统一使用 CGLIB,因为 CGLIB 避免了必须面向接口编程的限制,也不存在注入时类型转换异常的问题。性能方面 JDK 8 之后两者差距已经很小,选择主要看框架默认行为和是否需要代理非接口类。
追问与易错
追问方向:
- “Boot 2.x 默认用哪个?”→ Spring Boot 2.x 默认 proxy-target-class=true,统一使用 CGLIB 代理,不再区分是否有接口,避免了类型转换异常问题
- “CGLIB 代理 final 类会怎样?”→ 会抛异常,因为 CGLIB 通过继承生成子类来实现代理,final 类不可被继承,final 方法不可被重写,均无法代理
- “InvocationHandler 的三个参数?”→ proxy(生成的代理对象本身)、method(被调用的目标方法 Method 对象)、args(方法参数数组),在 invoke 中可织入切面逻辑
易错点:
- ❌ 有接口就一定用 JDK——Boot 2.x 默认 CGLIB
- ❌ CGLIB 是字节码编织——是运行时生成子类