面试知识库
进阶

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);  // 调用切面逻辑
    }
}
java

2. CGLIB(subclass-based)

JDK 动态代理 vs CGLIB 对比:

维度JDK 动态代理CGLIB
要求目标类必须有接口无要求(但不能代理 final 类/方法)
实现方式反射 + Proxy.newProxyInstanceASM 字节码操作,生成子类
性能调用时反射,略慢(JDK 8 后差距减小)直接调用子类方法,更快
Spring 默认有接口 → JDK(Spring Boot 2.x 后默认改为 CGLIB)无接口 → CGLIB
Spring Bootspring.aop.proxy-target-class=true(默认)→ 强制用 CGLIB

深挖:CGLIB 为什么比 JDK 代理快——MethodProxy + FastClass

JDK 代理调原方法走的是反射 Method.invoke(),反射要做权限检查、参数装箱、查方法表,有固定开销。CGLIB 不用反射:它为目标类和代理类各生成一个 FastClass,把每个方法映射成一个 int 索引,调用时通过 MethodProxy.invokeSuper() 根据索引直接 switch-case 定位到对应方法直接调用(等价于普通方法调用),绕过了反射。

所以拦截器里拿到的是 MethodProxy 而非 MethodmethodProxy.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 是字节码编织——是运行时生成子类