JDK 动态代理和 CGLIB 动态代理有什么区别?
简化版
JDK 动态代理基于接口,运行期生成实现接口的代理类;CGLIB 基于继承,运行期生成目标类的子类。目标类有接口时可用 JDK 动态代理,没有接口时通常用 CGLIB,但 CGLIB 不能代理 final 类和 final 方法。
详细版
二者核心区别如下:
| 对比项 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 实现方式 | 生成接口实现类 | 生成目标类子类 |
| 是否需要接口 | 需要 | 不需要 |
| 核心回调 | InvocationHandler | MethodInterceptor |
| 调用目标 | method.invoke(target, args) | methodProxy.invokeSuper(obj, args) |
| 对 final 的限制 | 接口代理,不直接受类 final 影响 | 不能代理 final 类/方法 |
| Spring AOP 场景 | 目标有接口时常用 | 目标无接口或强制类代理时常用 |
JDK 动态代理示意:
代理类 implements 接口 -> InvocationHandler -> 目标对象
CGLIB 示意:
代理类 extends 目标类 -> MethodInterceptor -> super 原方法
面试回答要注意:不要简单说谁性能一定更好。早期 CGLIB 常被认为调用性能较好,但现代 JDK 和框架不断优化,实际选择通常先看“有没有接口、是否允许继承、框架配置”,性能不是首要判断标准。
完整版教学
一、从生成代理类的角度理解区别
JDK 动态代理生成的代理类实现接口。
如果有接口:
interface UserService {
void createUser();
}
JDK 大致生成:
class $Proxy0 implements UserService {
private InvocationHandler handler;
public void createUser() {
handler.invoke(this, method, null);
}
}
CGLIB 生成的是目标类子类:
class UserServiceProxy extends UserService {
public void createUser() {
interceptor.intercept(...);
}
}
一个走接口,一个走继承,这是所有差异的源头。
二、从调用链理解区别
JDK 动态代理:
proxy.method()
-> InvocationHandler.invoke()
-> method.invoke(target, args)
-> target.method()
CGLIB 动态代理:
proxy.method()
-> MethodInterceptor.intercept()
-> methodProxy.invokeSuper(proxy, args)
-> super.method()
因此 JDK 动态代理更像“接口转发器”,CGLIB 更像“子类增强器”。
三、从限制条件理解区别
JDK 动态代理最大限制是必须有接口。如果业务类没有接口,就无法直接创建接口代理。
CGLIB 最大限制是继承规则。目标类不能是 final,被增强的方法不能是 final 或 private。
例如:
final class PaymentService {
public void pay() {}
}
CGLIB 无法继承这个类。
class PaymentService {
public final void pay() {}
}
CGLIB 可以生成子类,但不能重写 pay(),因此无法对这个方法做增强。
四、Spring AOP 中如何选择
Spring AOP 常见规则可以这样理解:
- 有接口时,可以使用 JDK 动态代理;
- 没有接口时,使用 CGLIB;
- 如果配置强制使用类代理,也会走 CGLIB;
- 最终注入到容器中的通常是代理对象,不是原始目标对象。
这会影响类型注入。例如 JDK 动态代理生成的是接口代理,如果你按具体实现类注入,可能会出现类型不匹配;CGLIB 代理是子类,按实现类注入通常更自然。
这个点在 Spring 面试中很常见。
五、性能问题怎么答更稳
不要背“JDK 慢,CGLIB 快”这种绝对结论。
原因有三点:
第一,JDK 反射和动态代理一直在优化。
第二,CGLIB 创建代理类、方法拦截也有自己的成本。
第三,业务系统里代理开销通常远小于数据库、网络、缓存、序列化等开销。
更稳的回答是:选型主要看接口约束、继承限制和框架配置;性能需要结合具体版本和场景测试,不能脱离上下文绝对判断。
六、代理选择不能只看性能口号
若目标 Bean 实现接口并按接口注入,JDK 代理即可;若必须按具体类类型注入,则要评估 CGLIB 及 final 限制,而不是凭“快几纳秒”决定。
has interface? --yes--> JDK proxy; --no/force class--> CGLIB subclass
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 选型先看类型契约和限制,再用基准测试谈性能,不能背“某一种永远更快”。 |
| 适用边界 | 现代 JVM 下二者性能差距通常不是首要矛盾,接口设计、类型兼容和可代理性更重要。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:选型先看类型契约和限制,再用基准测试谈性能,不能背“某一种永远更快”。
八、常见误区与追问
- 误区:CGLIB 在所有版本中都一定比 JDK 代理快。 生成和调用机制、JDK 版本及业务负载都会影响结果,没有脱离基准条件的固定结论。
- 误区:代理类存在,就代表所有调用都会经过代理。 只有客户端持有并调用代理引用时增强才生效;绕过代理直接调用目标对象,调用链自然不会出现增强。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:Spring 中有接口就绝对使用 JDK 代理吗? 默认通常如此,但配置可强制类代理;实际还要结合 Spring 版本和代理配置判断。
- 追问:代理模式和装饰器模式能只靠类图区分吗? 不能;两者都可能包装同一接口,必须结合“控制访问”还是“叠加职责”的设计意图区分。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
JDK 动态代理靠接口,CGLIB 动态代理靠继承。接口充足时 JDK 简洁,没接口时 CGLIB 补位;但 CGLIB 怕 final,性能不是第一判断标准。