JDK 动态代理 vs CGLIB

候选人小林在面试蚂蚁 P6 时,面试官问道:

"Spring AOP 默认用 JDK 代理还是 CGLIB?"

小林说:"JDK 代理..."

面试官说:"不对,Spring 默认用 CGLIB。"

小林一脸茫然。

面试官追问:"那 JDK 代理和 CGLIB 有什么区别?为什么 Spring Boot 2.x 默认用 CGLIB?"

小林彻底答不上来了。

【面试官心理】 这道题我用来测试候选人对 Spring 代理机制的理解深度。Spring Boot 2.x 确实默认用 CGLIB,这是一个重要的变化。很多人还在用 Spring MVC 时代的知识答题。知道为什么默认用 CGLIB,才能理解 Spring 的设计权衡。


一、核心问题 🔴

1.1 问题拆解

第一层:基本认知

  • "JDK 动态代理和 CGLIB 代理有什么区别?"
  • "Spring 默认用哪个?"

第二层:实现原理

  • "JDK 动态代理是怎么实现的?"
  • "CGLIB 是怎么创建代理对象的?"

第三层:选择依据

  • "Spring 是怎么决定用 JDK 还是 CGLIB 的?"
  • "为什么 Spring Boot 2.x 默认用 CGLIB?"

第四层:性能与限制

  • "JDK 代理和 CGLIB 哪个性能更好?"
  • "CGLIB 有什么限制?"

1.2 ❌ 错误示范

候选人原话 A:"JDK 代理是 JDK 自带的,CGLIB 是第三方库,所以 Spring 默认用 JDK。"

问题诊断

  • 混淆了"自带"和"默认"的区别
  • 不知道 Spring Boot 2.x 的默认配置变化
  • 不理解为什么 Spring 要做这个选择

候选人原话 B:"CGLIB 比 JDK 代理快,因为 CGLIB 是字节码生成。"

问题诊断

  • 这是个常见误区
  • 实际上 JDK 8 之后,JDK 代理的性能和 CGLIB 相差无几
  • 没有考虑创建代理的开销

候选人原话 C:"JDK 代理需要目标类实现接口,CGLIB 不需要。"

问题诊断

  • 知道这个区别,但没有理解为什么
  • 不理解 Spring 如何在这个基础上做选择

1.3 标准回答

P5 回答:核心区别

JDK 动态代理和 CGLIB 是两种不同的代理技术:

对比维度JDK 动态代理CGLIB 代理
实现方式实现接口,生成 $ProxyN继承父类,生成子类
要求目标类必须实现至少一个接口目标类不能是 final
方法限制无法代理非接口方法可以代理任何方法(包括 private,但有限制)
依赖Java 标准库CGLIB 库(Spring 已集成)
Spring Boot 默认是(2.x 版本)
// JDK 动态代理要求:目标类必须有接口
public class JdkProxyDemo {
    public static void main(String[] args) {
        // 目标接口
        interface OrderService {
            void createOrder();
        }

        // 目标实现
        class OrderServiceImpl implements OrderService {
            public void createOrder() {
                System.out.println("创建订单");
            }
        }

        // JDK 代理只能代理接口
        OrderService proxy = (OrderService) Proxy.newProxyInstance(
            OrderServiceImpl.class.getClassLoader(),
            new Class[]{OrderService.class}, // 必须提供接口
            (p, method, args) -> {
                System.out.println("前置逻辑");
                Object result = method.invoke(new OrderServiceImpl(), args);
                System.out.println("后置逻辑");
                return result;
            }
        );

        proxy.createOrder();
    }
}

// CGLIB 代理:不需要接口
public class CglibProxyDemo {
    public static void main(String[] args) {
        // 目标类(不需要实现接口)
        class OrderService {
            public void createOrder() {
                System.out.println("创建订单");
            }
        }

        // CGLIB 通过继承创建代理
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(OrderService.class); // 设置父类
        enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
            System.out.println("前置逻辑");
            Object result = proxy.invokeSuper(obj, args); // 调用父类方法
            System.out.println("后置逻辑");
            return result;
        });

        OrderService proxy = (OrderService) enhancer.create();
        proxy.createOrder();
    }
}

1.4 追问升级

追问 1:Spring 默认选择逻辑

Spring 和 Spring Boot 的默认选择不同:

// Spring 默认行为(Spring 3.x ~ 5.x)
// 在 AspectJAwareAdvisorAutoProxyCreator 中
@Override
public boolean shouldProxyTargetClass(Class<?> beanClass, String beanName) {
    // 【关键】检查是否有 @Aspect 切面匹配这个 Bean
    if (this.advisorFactory.aspects.isEmpty()) {
        return false; // 没有切面,不创建代理
    }

    // 如果 Bean 实现了接口,且有匹配的切面,仍然用 JDK 代理
    Class<?>[] interfaces = beanClass.getInterfaces();
    if (interfaces.length > 0) {
        return false; // 用 JDK 代理
    }

    // 否则用 CGLIB
    return true;
}

Spring 的默认选择

  1. 如果目标类实现了接口 → JDK 代理(除非强制使用 CGLIB)
  2. 如果目标类没有实现接口 → CGLIB

Spring Boot 2.x 的改变

// Spring Boot 2.x 强制使用 CGLIB
// AutoConfigurationTransactions
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public AopAutoConfiguration
    @ConditionalOnClass(AspectJAwareAdvisorAutoProxyCreator.class)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = true) // 【关键】matchIfMissing = true
    public static class JpaRepositoriesAutoConfiguration {
        // ...
    }
}

Spring Boot 2.x 的 spring.aop.proxy-target-class 默认值是 true,且 matchIfMissing = true,意味着即使不配置,默认也使用 CGLIB

⚠️

Spring Boot 2.x 改变了游戏规则:之前很多资料说"Spring 默认用 JDK 代理",这个说法在 Spring MVC 时代是对的,但在 Spring Boot 2.x 时代是错的。

追问 2:为什么 Spring Boot 2.x 默认用 CGLIB?

三个核心原因:

原因 1:接口不是必需的

Spring Boot 推崇"约定优于配置",很多业务类不需要实现接口:

// Spring MVC 时代:推荐面向接口编程
@Service
public class OrderServiceImpl implements OrderService { }

// Spring Boot 时代:直接写实现类
@Service
public class OrderService { // 不需要接口
    public void createOrder() { }
}

如果用 JDK 代理,这类不实现接口的 Bean 就无法被代理。

原因 2:避免强制接口暴露

很多业务类被设计为不暴露接口(内部类、包级私有实现等)。如果 Spring 默认用 JDK 代理,这些 Bean 就无法使用 AOP。

原因 3:统一性

CGLIB 可以代理任何类(除了 final 类和 final 方法),用 CGLIB 作为默认可以避免开发者困惑。

// 如果默认用 JDK,以下代码会出问题:
@Service
public class UserService {
    @Transactional
    public void saveUser() { }
    // 没有实现任何接口
}

// 结论:无法被代理,@Transactional 不生效
// 所以 Spring Boot 默认用 CGLIB

追问 3:JDK 动态代理的源码实现

// JDK 动态代理核心类:Proxy

// 1. Proxy.newProxyInstance() 做了什么?
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {
    // 1. 检查 InvocationHandler 不能为空
    Objects.requireNonNull(h);

    // 2. 复制接口类(防御性拷贝)
    Class<?>[] intfs = interfaces.clone();

    // 3. 查找或生成代理类
    Class<?> cl = getProxyClass0(loader, intfs);

    // 4. 获取代理类的构造函数(带 InvocationHandler 参数)
    Constructor<?> cons = cl.getConstructor(constructorParams);

    // 5. 调用构造函数创建实例
    return cons.newInstance(new Object[]{h});
}

// 2. 生成的代理类是什么样的?
/*
生成的代理类类似:
public final class $Proxy0 extends Proxy implements OrderService {
    private InvocationHandler h;

    public $Proxy0(InvocationHandler h) {
        this.h = h;
    }

    @Override
    public void createOrder() {
        try {
            // 调用 InvocationHandler
            h.invoke(this, m3, null); // m3 是 createOrder 方法的 Method 对象
        } catch (...) { }
    }
}
*/

// 3. 调用链
proxy.createOrder()
  └─ $Proxy0.createOrder()
      └─ h.invoke(this, m3, args) // h 是我们的 InvocationHandler
          └─ 我们的逻辑:前置通知
          └─ method.invoke(target, args) // 调用真实对象
          └─ 我们的逻辑:后置通知

追问 4:CGLIB 的源码实现

// CGLIB 核心类:Enhancer

// 1. Enhancer.create() 做了什么?
public Object create() {
    // 1. 检验父类是否是 final
    classOnly = (arguments == null);

    // 2. 生成代理类字节码
    byte[] b = strategy.generate(this);

    // 3. 加载字节码到 JVM
    Class<?> cls = ClassLoaderUtils.defineClass(
        classLoader, entityName, b, context);

    // 4. 创建实例
    return firstInstance(cls);
}

// 2. 生成的代理类是什么样的?
/*
生成的代理类类似:
public class OrderService$$EnhancerByCGLIB$$123456 extends OrderService {
    private MethodInterceptor interceptor;

    // 方法拦截
    public void createOrder() {
        try {
            // 调用 MethodInterceptor
            interceptor.intercept(this,
                CGLIB$createOrder$Method,
                new Object[0],
                CGLIB$createOrder$Proxy);
        } catch (Throwable $ex) {
            throw $ex;
        }
    }

    // 调用父类的方法(用于实际执行)
    public void CGLIB$createOrder$0() {
        super.createOrder();
    }
}
*/

// 3. CGLIB 的限制
enhancer.setSuperclass(FinalClass.class); // ❌ 报错:不能代理 final 类
enhancer.setSuperclass(FinalMethodClass.class); // 可以代理,但 final 方法无法被拦截

// CGLIB 代理 final 方法的结果:不会走拦截器,直接执行原始方法
public class FinalMethodClass {
    public final void finalMethod() {
        // 这个方法不会被 CGLIB 拦截
    }
}
💡

CGLIB 不能代理 private 方法,因为子类无法重写父类的 private 方法。这也是为什么 Spring 事务不能代理 private 方法——@Transactional 标注在 private 方法上会被静默忽略。

二、延伸问题 🟡

2.1 性能对比

阶段JDK 动态代理CGLIB 代理
首次创建快(生成简单类)慢(生成复杂类)
运行时调用JDK 7/8 之后已优化几乎无差别
总体接口多时优势明显类多时更通用

实际测试(JDK 11 环境):

测试场景:1000万次方法调用

JDK 动态代理:耗时 ~800ms
CGLIB 代理:  耗时 ~850ms

差距在 5% 以内,现代 JDK 已经优化得很好

真正影响性能的不是"用哪个代理",而是"有没有必要用代理"。

2.2 强制使用某种代理

# application.yml
spring:
  aop:
    proxy-target-class: true  # 强制 CGLIB
    # proxy-target-class: false # 强制 JDK 代理
// @EnableAspectJAutoProxy
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 强制 CGLIB
@EnableAspectJAutoProxy(proxyTargetClass = false) // 强制 JDK 代理
// @Transactional
@Transactional(proxyTargetClass = true)  // 这个配置在 Spring 5.x 中被忽略
// 原因:Spring 5.x 中 TransactionManagementConfigurationSelector
// 强制选择了 CGLIB,忽略了 proxyTargetClass 属性

2.3 代理对象的创建时机

// AbstractAutoProxyCreator.postProcessAfterInitialization()
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    // 在这里判断是否需要代理,如果需要则创建
    return wrapIfNecessary(bean, beanName, getCacheKey(bean.getClass(), beanName));
}
Bean 生命周期中的代理创建:
1. createBeanInstance()        → new OrderService() 原始对象
2. populateBean()              → 注入依赖
3. initializeBean()
   ├─ Aware 回调
   ├─ postProcessBeforeInit()
   ├─ init-method (@PostConstruct 等)
   └─ postProcessAfterInit()    ← 代理在这里创建
4. 代理对象被存入一级缓存

三、生产避坑

3.1 final 类无法代理

// ❌ 错误:final 类无法被代理
@Service
public final class FinalService {
    @Transactional
    public void doSomething() {
        // 这个方法永远不会被事务增强
        // 因为 final 类不能被继承,CGLIB 无法工作
    }
}

// ✅ 正确:去掉 final
@Service
public class NormalService {
    @Transactional
    public void doSomething() {
        // 可以正常被事务增强
    }
}

3.2 private 方法无法被代理

@Service
public class OrderService {
    @Transactional
    public void save() {
        // 这个方法会被事务增强
    }

    @Transactional
    private void doPrivate() {
        // ❌ 这个方法不会被事务增强!
        // CGLIB 通过继承实现,private 方法不会被继承
        // Spring 会在启动时忽略 private 的 @Transactional 方法
        // 并打印警告日志
    }
}

3.3 自我调用不走代理

@Service
public class OrderService {
    @Transactional
    public void methodA() {
        // 走事务
        this.methodB(); // ❌ 不走事务
        methodB();      // ❌ 不走事务
    }

    @Transactional
    public void methodB() {
        // 这里没有事务!
    }
}

四、工程选型

4.1 什么情况下用 JDK 代理?

  • Bean 实现了多个接口,需要统一代理接口
  • 性能敏感,代理类数量多
  • 需要代理的是接口而不是实现类

4.2 什么情况下用 CGLIB?

  • Bean 没有实现接口(业务类直接写)
  • 需要代理类的所有方法
  • Spring Boot 项目(默认)

4.3 是否需要关心这个问题?

大多数情况下,你不需要关心 Spring 用 JDK 还是 CGLIB:

  1. Spring 会自动选择合适的代理方式
  2. 两种代理的效果完全一致
  3. Spring Boot 2.x 默认用 CGLIB,基本覆盖了大多数场景

但如果遇到以下情况,需要注意:

  1. Bean 是 final 类 → 无法代理
  2. 需要精确控制代理行为 → 考虑强制指定
  3. 排查 AOP 不生效的问题 → 需要知道代理类型

五、面试总结

JDK 动态代理 vs CGLIB 这道题,考的不是"记结论",而是"理解权衡"。

P5 候选人能说出"JDK 代理要求接口,CGLIB 不需要"。 P6 候选人能说出 Spring Boot 2.x 默认用 CGLIB,能解释为什么这样设计。 P7 候选人能说出 Spring 如何选择代理方式(看接口 + 看切面),能解释 JDK 代理和 CGLIB 的具体实现原理,能说出 final 类、private 方法的代理限制。

能在这道题上答到最后的,基本都深入研究过 Spring 源码。