cglib 与 JDK 动态代理有什么区别?

cglib 和 JDK 动态代理是 Java 中两种主流的代理实现方式,核心区别在于:JDK 动态代理基于接口,要求目标类必须实现接口;cglib 基于继承,通过生成目标类的子类来实现代理,因此可以代理没有实现接口的普通类。 选型时先看目标类是否实现接口——有接口且只需按接口调用,JDK 动态代理足够;没有接口或需要代理类的具体方法,才考虑 cglib。同时要接受 cglib 的继承限制:无法代理 final 类和 final 方法。

两者的工作机制

JDK 动态代理

JDK 自带 java.lang.reflect.Proxy 和 InvocationHandler。运行时为一组接口生成一个实现了这些接口的代理类,方法调用被转发到 InvocationHandler.invoke()。

  • 输入:目标类实现的接口列表 + 调用处理器。
  • 动作:JVM 在运行时生成代理类字节码。
  • 结果:得到一个可强转为接口类型的代理对象,调用接口方法时进入处理器逻辑。

因为代理类实现的是接口,所以只能通过接口类型引用它,目标类自己的非接口方法不在代理范围内。

cglib

cglib(Code Generation Library)是一个字节码生成库,底层依赖 ASM。它为目标类生成一个子类,在子类中覆写父类方法,把调用拦截到 MethodInterceptor.intercept()。

  • 输入:目标类(Class 对象)+ 方法拦截器。
  • 动作:运行时生成目标类的子类字节码。
  • 结果:得到一个目标类的子类实例,调用被覆写的方法时进入拦截逻辑。

由于是继承关系,代理对象可以按目标类类型使用,能覆盖目标类中所有可被继承的方法。

关键差异对比

维度 JDK 动态代理 cglib
实现基础 接口 继承(生成子类)
目标类要求 必须实现至少一个接口 无需实现接口,但类和方法不能是 final
可代理范围 接口中声明的方法 目标类中可被覆写的非 final 方法
代理对象类型 接口类型 目标类的子类类型
依赖 JDK 内置,无需额外依赖 需要引入 cglib(及其 ASM 依赖)
典型限制 无法代理没有接口的类 无法代理 final 类、final 方法、static 方法、private 方法

怎么选

按下面的顺序判断即可:

  1. 目标类是否实现了接口?
    • 是,且调用方只依赖接口 → 优先 JDK 动态代理,零额外依赖,行为可预期。
    • 是,但需要代理接口之外的方法 → 考虑 cglib。
    • 否 → 只能用 cglib(或其它字节码方案)。
  2. 目标类或关键方法是否为 final?
    • 是 → cglib 无法覆写,需改用接口方案或调整设计。
  3. 是否愿意引入额外依赖?
    • 不想引入 → JDK 动态代理。
    • 可以引入 → cglib 适用范围更广。

一个具体场景:你有一个 OrderService 类,没有实现任何接口,想在方法前后加日志。JDK 动态代理做不到,因为它需要一个接口来生成代理类;cglib 可以生成 OrderService 的子类并覆写方法,从而插入日志逻辑。反过来,如果 OrderService 实现了 IOrderService 接口,且调用方都通过接口引用,那么用 JDK 动态代理更简单,也不必引入 cglib。

关于 cglib 的获取

cglib 项目已不再托管在 SourceForge。打开 cglib.sourceforge.net 会被重定向到 GitHub 上的托管地址,源码和发布包都在那里获取。因此在新项目里引用 cglib 时,应使用 GitHub 上维护的版本坐标,而不是旧的 SourceForge 地址。

常见卡点

  • 以为 cglib 能代理一切:final 类、final 方法、static 方法、private 方法都无法被覆写,代理会失效或直接报错。
  • 用接口类型接收 cglib 代理:cglib 生成的是子类,按接口强转不一定符合预期,注意引用类型。
  • 忽略构造器:cglib 通过子类实例化,目标类的构造器会被调用,构造器里的副作用需要考虑。
  • 版本与依赖:cglib 依赖 ASM,版本不匹配时可能出现字节码相关的异常,引入时留意依赖树。
cglib.sourceforge.net