JDK 动态代理 vs CGLIB

面试官翻到候选人小赵的简历,上面写着"精通 Spring 框架原理",开口问道:

"Spring 默认用 CGLIB 做代理,你知道为什么吗?"

小赵说:"因为 CGLIB 性能更好?"面试官追问:"好多少?怎么测的?"

小赵语塞。面试官继续:"那 JDK 动态代理的 newProxyInstance 方法,接收哪三个参数?"

小赵支支吾吾答不上来。

【面试官心理】 这道题我用来试探候选人对 Spring 源码的钻研程度。能说出 CGLIB 和 JDK 代理差异的是基本操作,能讲清楚 proxyTargetClass 演进历史的是 P6,能分析出 Spring Boot 为什么要改默认行为的,基本是 P7 了。

一、两种代理的底层原理 🔴

1.1 JDK 动态代理:接口即契约

JDK 动态代理的核心思想是:我不继承你,我只和你签契约(接口)

当你调用 Proxy.newProxyInstance() 时,JVM 在内存中动态生成一个 $Proxy0 类(名字按顺序递增),这个类:

  1. 继承自 java.lang.reflect.Proxy
  2. 实现你传入的所有接口
  3. 在每个接口方法中,调用 InvocationHandler.invoke()

生成的代理类大约长这样(简化版):

public final class $Proxy0 extends Proxy implements UserService {

    // 构造器持有 InvocationHandler 引用
    public $Proxy0(InvocationHandler h) {
        super(h);
    }

    // 覆盖接口方法
    @Override
    public void saveUser(String name) {
        try {
            // 调用InvocationHandler.invoke()
            // 方法名、参数、方法对象全部传过去
            super.h.invoke(this, m3, new Object[]{name});
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable t) {
            throw new UndeclaredThrowableException(t);
        }
    }
}

newProxyInstance 的三个参数

  • ClassLoader:加载生成的 $Proxy0 类,大多数场景用目标类的 ClassLoader 就够
  • Interface[]:代理类要实现的接口数组(必须是接口,不能是类)
  • InvocationHandler:方法调用的处理器,代理对象的每次方法调用都会触发这个 Handler

1.2 CGLIB 代理:继承即能力

CGLIB 的核心思想是:我不是在接口上做文章,我直接继承你,用字节码生成一个你的子类

Enhancer 是 CGLIB 的核心类,它的工作流程:

  1. 设置父类(目标类)
  2. 设置回调(MethodInterceptor
  3. 调用 enhancer.create() 生成子类实例

生成的代理类大约长这样(简化原理):

// CGLIB 生成的代理类(伪代码)
public class UserService$$EnhancerByCGLIB$$xxx extends UserService {

    private MethodInterceptor callback;

    // 覆盖父类的所有方法
    @Override
    public void saveUser(String name) {
        // 调用拦截器
        callback.intercept(this,
            CGLIB$saveUser$Method,  // 方法对象
            new Object[]{name},      // 参数
            CGLIB$saveUser$Proxy    // 方法代理对象
        );
    }

    // final 方法不会被覆盖(编译期就决定了)
    // static 方法也不会被代理
}

1.3 InvocationHandler vs MethodInterceptor

维度InvocationHandlerMethodInterceptor
使用框架JDK 动态代理CGLIB
拦截方法接口中声明的方法所有非 final、非 static 方法
方法调用method.invoke(target, args)methodProxy.invoke(target, args)methodProxy.invokeSuper(obj, args)
控制力完全控制,可不调用原方法同左侧
性能纯反射调用FastClass 优化后接近直接调用
⚠️

很多候选人搞混了 method.invoke()methodProxy.invoke() 的区别:

  • method.invoke(target, args)反射调用,要走 JVM 的方法访问检查
  • methodProxy.invoke(target, args)直接调用,绕过反射,直接跳转
  • methodProxy.invokeSuper(obj, args) 是调用父类的实现(即目标方法本身)

在 CGLIB 拦截器中,如果要调用原方法,必须用 methodProxy.invokeSuper(),而不是 method.invoke(),否则会引发死循环。

二、Spring 的代理演进 🟡

2.1 版本演进历史

Spring 的代理策略经历了三个阶段,这个演进历史是 P6/P7 的高频追问:

版本默认代理关键变化
Spring 2.xJDK 动态代理目标类必须实现接口
Spring 3.xCGLIB(需要额外依赖)CGLIB 从 AspectJ 项目独立,Spring 引入集成
Spring 4.xCGLIB(内置,无需额外依赖)CGLIB 打包进 spring-core,引入 FastClass
Spring 5.xCGLIB 增强优化了 Objenesis 机制,进一步减少对象创建开销

2.2 Objenesis 机制:绕过构造器

这是 Spring 4.x 引入的重要改进,也是 Spring Boot 2.x 默认 CGLIB 的技术基础之一。

传统 CGLIB 创建代理对象时,需要调用目标类的默认构造器

// 早期 CGLIB 的写法
enhancer.create(); // 要求目标类有默认构造器

问题是:如果目标类的构造器有参数,或者构造器里有重要逻辑(副作用),调用默认构造器就会出问题。

Spring 4.x 引入 Objenesis:

// Spring 内部使用的是 ObjenesisStd
// 它可以在不调用任何构造器的情况下创建对象实例
// 原理:直接分配一块内存,不执行任何字节码
ObjenesisStd objenesis = new ObjenesisStd();
UserService proxy = (UserService) objenesis.newInstance(UserService.class);

这意味着即使目标类没有默认构造器,CGLIB 代理也能创建成功,且不会触发构造器中的任何逻辑。

2.3 为什么 Spring Boot 默认用 CGLIB?

Spring Boot 2.x 在全局配置中设置了 spring.aop.proxy-target-class = true,强制使用 CGLIB。原因有三:

  1. 无接口依赖:很多业务类不实现任何接口,但同样需要 AOP 能力(事务、日志等)
  2. 方法覆盖:CGLIB 可以代理所有非 final、非 static 方法,而 JDK 代理只能代理接口方法
  3. Objenesis 成熟:Spring 4.x+ 内置 Objenesis,解决了 CGLIB 对默认构造器的依赖
// Spring Boot 2.x 的自动配置源码(简化)
// org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
@Configuration
@EnableConfigurationProperties(AopProperties.class)
public class AopAutoConfiguration {
    @Bean
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true)
    public AspectJWeaverAutoProxyConfiguration aspectJWeaverAutoProxyConfiguration() {
        return new AspectJWeaverAutoProxyConfiguration();
    }
}
💡

matchIfMissing = true 意味着如果没配置 spring.aop.proxy-target-class,默认就强制 CGLIB。这是 Spring Boot 的设计哲学:安全优于灵活。CGLIB 功能覆盖范围更广,默认用 CGLIB 出错的概率更低。

三、核心对比表 🟡

维度JDK 动态代理CGLIB
实现机制实现接口,生成 $ProxyN 类继承 Proxy继承父类,生成 $XXX$$EnhancerByCGLIB$$N 子类
代理范围只能代理实现了接口的类,且只能拦截接口中声明的方法可代理所有非 final、非 static 方法
构造器调用不需要调用目标类构造器(通过反射直接实例化接口类型)默认调用默认构造器(Spring 4.x+ 用 Objenesis 绕过)
性能(无优化)每次调用都反射 method.invoke()每次调用都反射 method.invoke()
性能(有 FastClass)无 FastClassFastClass 将反射调用降维为直接索引调用
类加载要求需要接口类在类加载器中可用需要父类在类加载器中可用(且未被 final 修饰)
生成文件$Proxy0.class 等(可通过 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 保存)ClassName$$EnhancerByCGLIB$$HashCode.class
Spring 默认配置Spring 2.xSpring 3.x+(Spring Boot 2.x 强制)

四、❌ 错误示范

候选人原话一

"CGLIB 比 JDK 动态代理快,因为 CGLIB 是编译期的。"

问题诊断

  • CGLIB 和 JDK 代理都是运行期字节码生成,不是编译期
  • 两者性能差距主要在是否有 FastClass 优化,而非生成时机

候选人原话二

"JDK 动态代理只能代理接口,因为 Proxy 继承了 java.lang.Object,而 Object 是一切类的父类。"

问题诊断

  • 这句话后半句是对的,但没解释清楚为什么"只能代理接口"
  • 正确原因:Proxy 类需要实现你传入的接口数组,所以接口是必需的
  • 实际上 $Proxy0 继承 Proxy 并实现接口,同时也间接是 Object 的子类

候选人原话三

"Spring 用 CGLIB 是因为 CGLIB 性能更好,Spring Boot 强制用 CGLIB 是因为性能考虑。"

问题诊断

  • 性能只是原因之一,不是全部
  • 更深层的原因是功能覆盖范围:Spring Boot 时代大量业务类不实现接口,但同样需要事务、AOP 等能力
  • 混淆了"功能需求"和"性能优化"的优先级

【面试官心理】 能答出"JDK 动态代理要求接口"这个基本点的占 60%,能解释清楚 CGLIB 的 FastClass 机制的是 40%,能讲出 Spring Boot 为什么改默认策略的只有 15%。这道题我一般从"为什么默认用 CGLIB"切入,往版本演进、性能对比、Spring Boot 设计哲学三个方向追问。

五、实战追问链

第一层:Spring 用的是哪种代理?答:"CGLIB 或 JDK 动态代理。"——基本概念

第二层:什么情况下用 JDK,什么情况下用 CGLIB?答:"看目标类有没有实现接口。"——理解判断逻辑

第三层@EnableAspectJAutoProxy(proxyTargetClass = false) 会发生什么?答:"强制用 JDK 动态代理。"——参数理解

第四层:为什么 Spring Boot 要强制 CGLIB?它考虑了哪些因素?

答不出来 → P6 候选人;能说出 Objenesis 和无接口依赖 → P7 候选人

六、生产避坑

坑一:final 类无法代理

CGLIB 通过继承实现代理,所以被 final 修饰的类无法被 CGLIB 代理。AOP 切面对这类 Bean 不会生效,且 Spring 不会报错,只会在运行时发现"事务没生效"。

// ⚠️ 这种类无法被 Spring AOP 代理
public final class FinalUserService {
    @Transactional
    public void save() { /* ... */ }
    // Spring AOP 完全不会拦截这个方法
}

排查方法:开启 debug 日志 logging.level.org.springframework.aop=DEBUG,Spring 会打印哪些 Bean 被跳过了代理创建。

坑二:self-invocation 在两种代理下都失效

不管是 JDK 代理还是 CGLIB 代理,内部方法调用走的都是 this 引用,不是代理对象。这个问题与选哪种代理无关,是 Spring AOP 的根本限制。

坑三:性能测试的坑

很多人在本地测 JDK vs CGLIB 性能,结果发现差不多甚至 JDK 更快。这是因为测试没有预热——JIT 编译器对反射调用有内联优化,多次调用后 method.invoke() 的开销会显著下降。FastClass 的优势在首次调用大量方法签名不同的场景下更明显。