动态代理(CGLIB 与 JDK)
JDK 动态代理类和委托类需要都实现同一个接口。也就是说只有实现了某个接口的类可以使用 Java 动态代理机制。但是,事实上使用中并不是遇到的所有类都会给你实现一个接口。因此,对于没有实现接口的类,就不能使用该机制。而 CGLIB 则可以实现对类的动态代理。
这段话点出了两种代理的根本差异:JDK 代理基于接口,CGLIB 代理基于继承。但要在 Spring 里用好它们,还需要理解代理为什么存在、Spring 如何二选一、以及各自的边界。
为什么需要代理
Spring AOP 要在不修改业务代码的前提下织入事务、日志、权限等横切逻辑。做法是:容器不直接暴露目标对象,而是暴露一个”长得一样、但方法前后能插逻辑”的代理对象。调用方无感知,增强逻辑在代理层完成——这就是代理模式在框架里的落地。
JDK 动态代理
三要素:Proxy.newProxyInstance(classLoader, interfaces, handler) + 实现 InvocationHandler:
Object proxy = Proxy.newProxyInstance( |
原理:运行期由 JVM 生成实现指定接口的代理类 $Proxy0,其方法体统一转发到 InvocationHandler。限制很明显:目标类必须实现接口,且代理只覆盖接口方法。
CGLIB 动态代理
CGLIB 用 ASM 在运行期生成目标类的子类,覆写非 final 方法并织入回调(MethodInterceptor):
- 不要求接口,直接代理类
- 限制:final 类/方法无法代理;通过继承实现,构造器会被调用(Objenesis 可绕过)
- Spring 内置了 repackaged 的 CGLIB,无需额外依赖
Spring 如何选择
默认规则:目标类实现了接口 → JDK 代理,否则 CGLIB。但 Spring Boot 2.x 起 spring.aop.proxy-target-class 默认为 true,即统一使用 CGLIB——原因是 JDK 代理下”按类注入”会失败(代理对象不是目标类的实例),统一 CGLIB 减少了这类困惑。
这个差异会真实地咬人:
UserServiceImpl impl; // 若实际是 JDK 代理 → 注入失败 |
所以社区惯例是面向接口注入,让代理方式的选择对业务透明。
性能与语义边界
- 生成成本:CGLIB 生成子类字节码比 JDK 生成接口代理慢,但代理对象创建后调用性能差异很小(JDK8 之后反射调用已大幅优化)
- 自调用失效:同类中方法 A 调用方法 B,走的是 this 而非代理对象,B 上的事务/缓存注解不生效。这是代理模型最著名的坑,解法是拆类、注入自身或 AopContext
- 代理只增强 public(CGLIB 含 protected/package,但语义上 AOP 只承诺 public)方法
手写最小代理体会本质
interface Service { void run(); } |
十行代码即 JDK 代理全貌:接口 + 转发 + 插入点。CGLIB 只是把”实现接口”换成”继承类”,思想完全一致。
理解动态代理的关键不是背 API,而是抓住一句话:代理对象是容器与调用方之间的契约替身,增强发生在替身层。所有 AOP 的坑(自调用、注入类型、final 限制)都能从这句话推出来。











