CGLIB必须生成子类因其基于字节码继承机制,通过ASM动态创建目标类的非final子类并重写方法;JDK代理仅实现接口,不依赖目标类结构,故无需继承且不受final限制。

选 CGLIB 还是 JDK 动态代理,不能只看“能用就行”,得看目标类有没有接口、方法是不是 final、调用频次高不高——这三点直接决定你上线后会不会踩内存暴涨、启动卡顿或 IllegalArgumentException: Cannot subclass final class 的坑。
为什么 CGLIB 必须生成子类,而 JDK 代理只能走接口
CGLIB 的核心是 Enhancer,它用 ASM 直接操作字节码,在运行时动态生成一个继承自目标类的新类(比如 UserService$$EnhancerByCGLIB$$a1b2c3d4),再重写所有非 final 方法,插入拦截逻辑。这就意味着:
- 目标类不能是
final,否则 ASM 无法继承 —— 否则抛IllegalArgumentException: Cannot subclass final class - 目标类的构造函数会被调用两次:一次是原始实例,一次是代理子类实例(除非显式禁用)
- 所有被代理的方法必须是非
static、非private、非final;final方法会直接跳过拦截,静默失效 - JDK 代理则完全绕开继承,靠
Proxy.newProxyInstance()生成一个实现相同接口的全新类(如$Proxy0),所以它根本不关心目标类长什么样,只认Class.getInterfaces()返回的东西
JDK 代理的反射开销 vs CGLIB 的生成延迟
两者性能不能笼统说“谁快谁慢”,要看阶段:
- 类生成阶段:
Proxy.newProxyInstance()几乎瞬时完成;Enhancer.create()要解析字节码、生成子类、加载类,首次耗时可能高出 5–10 倍(尤其类大、方法多时) - 方法调用阶段:JDK 代理每次调用都走
Method.invoke(),有反射开销;CGLIB 代理是普通 invokevirtual 调用,实测在 JDK 8+ 上单次调用快 15%–30% - 内存占用:CGLIB 每个代理类都新增一个 Class 对象,容易触发 Metaspace OOM(尤其高频创建代理的场景,如每个 HTTP 请求都 new 一个代理);JDK 代理共享同一个 $ProxyX 类,内存更友好
Spring 默认切换策略背后的取舍
Spring AOP 默认启用 proxy-target-class=false(即优先 JDK 代理),不是因为 CGLIB 不好,而是为了规避两类硬限制:
- 只要目标对象实现了至少一个接口,就用 JDK 代理 —— 安全、轻量、无侵入
- 只有当目标类没接口、又没标
@EnableAspectJAutoProxy(proxyTargetClass = true),才 fallback 到 CGLIB - 如果你手动配了
proxyTargetClass = true,但目标类里有final方法,Spring 不会报错,但那些方法压根不会被切面拦截 —— 日志、事务、监控全失效,且毫无提示
真正容易被忽略的是:CGLIB 的子类机制让「对象身份语义」变了。比如 proxy instanceof UserService 为 true,但 proxy.getClass() == UserService.class 一定为 false;如果代码里用了 getClass().getSimpleName() 做路由或序列化,可能突然出错。这不是性能问题,是契约断裂。










