人人都会AI编程

5.2 动态代理底层机制:JDK 动态代理与 CGLIB 动态代理对比

更新时间:2026-07-10

Spring AOP 的实现主要依赖两种动态代理技术:JDK 动态代理CGLIB 动态代理。它们各自有不同的工作机制、适用场景和边界约束。理解二者的区别,不仅有助于面试或解决线上诡异问题,也能在系统设计时做出更准确的技术选型。

5.2.1 JDK 动态代理:基于接口的代理

JDK 动态代理是 Java 标准库自带的代理机制,位于 java.lang.reflect 包下。它的核心思路是:代理对象与目标对象实现相同的接口,调用方通过接口引用代理对象,代理对象将方法调用转发给 InvocationHandler 处理

1. 工作机制

JDK 动态代理在运行时通过 Proxy.newProxyInstance() 动态生成一个代理类,这个代理类会实现目标对象所实现的所有接口。每个方法调用都会被分发到一个统一的 InvocationHandler.invoke() 方法中,由开发者决定如何增强或直接调用目标方法。

典型代码结构如下(简化示例,不要求立即掌握):

public class JdkProxyDemo {
    public static void main(String[] args) {
        UserService target = new UserServiceImpl();          // 目标对象
        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                (proxyObj, method, args1) -> {
                    System.out.println("前置增强");
                    Object result = method.invoke(target, args1); // 反射调用目标方法
                    System.out.println("后置增强");
                    return result;
                }
        );
        proxy.saveUser(); // 通过接口调用代理
    }
}

2. 关键特征

  • 必须基于接口:目标类必须至少实现一个接口,代理类也只能转为接口类型使用。如果强行转为具体实现类会抛出 ClassCastException
  • 反射机制调用目标方法method.invoke(target, args) 是基于 Java 反射的,在早期 JDK 版本有一定性能开销,但现代 JVM 已做了大量优化,通常不会成为主瓶颈。
  • 生成代理类的速度较快:由于只生成接口的代理类,结构相对简单,代理类的创建成本较低。

5.2.2 CGLIB 动态代理:基于继承的代理

CGLIB(Code Generation Library)是一个强大的高性能代码生成库,Spring 将其内嵌在 spring-core 中。CGLIB 代理的实现思路是:通过字节码技术动态生成目标类的子类,重写目标方法实现增强

1. 工作机制

CGLIB 使用 ASM 字节码框架直接操作 class 文件,在运行时生成一个目标类的子类。这个子类会覆盖所有非 final 的方法,并在方法内部插入回调逻辑(MethodInterceptor),由回调决定是否调用父类方法以及如何增强。

典型代码结构(CGLIB 原生 API,Spring 对其做了封装):

public class CglibProxyDemo {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserServiceImpl.class);       // 目标类(没有接口也可以)
        enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> {
            System.out.println("前置增强");
            Object result = proxy.invokeSuper(obj, args1);   // 调用父类方法
            System.out.println("后置增强");
            return result;
        });
        UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
        proxy.saveUser(); // 直接调用具体类
    }
}

2. 关键特征

  • 无需接口:可以代理任何非 final 的普通类,解决了 JDK 代理“必须有接口”的限制。
  • 通过子类重写方法实现:本质上是继承,因此 final 方法无法被代理,final 类完全无法被 CGLIB 代理。private 方法也不可代理。
  • 调用目标方法使用的是 FastClass 机制:CGLIB 会为目标类和代理类各生成一个 FastClass,通过索引直接调用方法,避免了反射的性能损耗,因此执行效率通常高于 JDK 代理(尤其是在调用频繁的场景)。
  • 不能代理 finalstatic 方法
  • 生成代理类的成本稍高:需要生成完整的子类及相关的 FastClass 辅助类,启动时耗时会略高于 JDK 代理。不过在 Spring 环境中,大部分代理类是在启动时一次性生成的,运行期并无影响。
  • 需要注意构造函数:生成的子类会调用父类的无参构造函数,如果目标类没有无参构造函数,代理会失败。Spring 结合 Objenesis 库可以绕过构造函数直接实例化对象,解决了这一问题。

5.2.3 JDK 动态代理与 CGLIB 的全面对比

从实际使用的角度,我们可以从以下几个维度对比:

| 对比维度 | JDK 动态代理 | CGLIB 代理 |
| ------------------ | -------------------------------------- | ---------------------------------------- |
| 代理前提 | 目标对象必须实现接口 | 目标类不能是 final,代理方法不能是 final |
| 代理对象类型 | 接口类型,不能转为具体实现类 | 目标类的子类,可直接转为具体实现类 |
| 方法调用方式 | 基于反射 Method.invoke | 基于 FastClass 索引直接调用,性能较高 |
| 生成代理类成本 | 较低(只需生成接口实现) | 较高(需要生成子类及 FastClass 辅助类) |
| 默认代理方式 | Spring AOP 中,目标有接口时优先使用 | 当目标没有接口时自动选用 |
| 底层实现包 | java.lang.reflect.Proxy | CGLIB(通过 ASM 直接操作字节码) |
| 限制 | 只能代理接口方法,目标自身方法无法代理 | 不能代理 final 方法,不能代理 final 类 |
| Spring 中是否可配置 | 可强制 proxyTargetClass = true 来使用 CGLIB 替代 | 通常由 Spring 自动选择 |

性能误区纠正:早期文章中常说 JDK 动态代理性能远差于 CGLIB,实际上在现代 JVM 中二者差距已经很小。JDK 代理的反射调用经过 JIT 优化后可以接近直接调用,而 CGLIB 的 FastClass 虽快,但内存占用和启动成本稍高。通常不是性能瓶颈,选择时应更多考虑“是否必须强制使用 CGLIB”(比如必须代理具体类),而非性能。

5.2.4 Spring AOP 中的代理选择机制

在 Spring 的实际开发中,我们几乎无需手动选择代理技术,因为 Spring AOP 会根据目标 Bean 的实际情况自动判断。

Spring 的 DefaultAopProxyFactory 会按照以下策略决定使用哪种代理:

  1. 如果目标对象实现了至少一个接口,则默认使用 JDK 动态代理
  2. 如果目标对象没有实现任何接口,则使用 CGLIB 动态代理
  3. 如果配置 @EnableAspectJAutoProxy(proxyTargetClass = true) 或基于 XML 设置 <aop:aspectj-autoproxy proxy-target-class="true"/>,则强制使用 CGLIB 代理,即使目标对象实现了接口。

在 Spring Boot 环境中,从 Spring Boot 2.0 开始,默认的 proxyTargetClass 属性被设置为 true(即默认使用 CGLIB 代理),这是为了统一处理体验、避免类型转换问题。当你的项目使用 @SpringBootApplication 启动时,内部自动启用了 @EnableAspectJAutoProxy(proxyTargetClass = true)。因此,实际开发中主要面对的是 CGLIB 代理

实际编码注意点

  • 同一个类内部的自调用(this.method())不会触发代理逻辑,这是两种代理共同的局限性,需要通过 AopContext.currentProxy() 或重构方法到另一个 Bean 解决。
  • 如果使用 JDK 代理,只能通过接口类型注入 Bean;如果用具体类类型注入并且目标 Bean 是 JDK 代理,则会抛出 NoSuchBeanDefinitionException。但 Boot 2.x 之后默认 CGLIB 代理则不存在这个问题。
  • 对于 final 类或 final 方法,CGLIB 无法代理,Spring 会跳过该 Bean 的代理增强,不会报错,但注解形如 @Transactional@Cacheable 等将静默失效。排查技巧:如果发现事务不生效,检查类或方法是否被 final 修饰。

5.2.5 一个调试小技巧:查看正在使用的代理类型

在日常开发中,如果怀疑 AOP 增强没有生效,可以快速检查当前 Bean 是哪种代理对象:

@RestController
public class CheckProxyController {

    @Autowired
    private ApplicationContext context;

    @GetMapping("/check-proxy")
    public String checkProxy(String beanName) {
        Object bean = context.getBean(beanName);
        String proxyType = "未知";
        if (Proxy.isProxyClass(bean.getClass())) {
            proxyType = "JDK动态代理";
        } else if (Enhancer.isEnhanced(bean.getClass())) {
            proxyType = "CGLIB动态代理";
        } else {
            proxyType = "原始对象,无代理";
        }
        return "Bean [" + beanName + "] 的代理类型: " + proxyType;
    }
}

通过这个简单接口,可以确认容器中的某个 Bean 是否被正确代理。

掌握这两种动态代理机制的区别与联系,不仅能帮你更深入地理解 Spring AOP 的底层原理,也能在实际问题排查中找到“切面为什么失效”“事务为何不回滚”等疑难杂症的根因。