主题
动态代理与 CGLIB
一句话:JDK 动态代理在运行时生成一个实现接口的类,把方法调用交给
InvocationHandler;CGLIB 生成目标类的子类,用方法拦截器覆盖非 final 方法。两者都是「在调用链上插一层」,边界由接口和继承决定。
适用 JDK 17 / JDK 21,Spring Framework 6.x / Spring Boot 3.x。CGLIB 已内嵌在 Spring 中,不再需要单独引入依赖。
为什么需要
横切逻辑(事务、日志、权限、缓存)如果写进每个业务方法,会重复且难维护。静态代理要为每个接口手写一个类,接口一多就爆炸。动态代理把「生成代理类」推迟到运行时,框架只需知道目标类型和增强逻辑。
JDK 代理和 CGLIB 解决同一个问题,但切入点不同:前者要求接口,后者要求可继承的类。选错会在启动时报 ClassCastException 或 IllegalArgumentException。
核心机制
JDK 动态代理
核心 API 是 Proxy.newProxyInstance(classLoader, interfaces, handler)。JVM 在运行时生成一个类,它:
- 继承
java.lang.reflect.Proxy; - 实现传入的所有接口;
- 每个接口方法都转调
InvocationHandler.invoke(proxy, method, args)。
生成类的构造器接收一个 InvocationHandler。每个代理实例只调用一次这个构造器,不存在「构造器被调用两次」;目标对象本身是另外创建的,JDK 代理不会调用目标对象的构造器。
equals、hashCode、toString 也会被转发到 handler。handler 里调用 method.invoke(target, args) 时,注意异常会被包装成 InvocationTargetException,要取 getCause()。
限制很明确:只能代理接口。非 public 接口要求代理类和接口在同一个包;没有接口的类无法用 JDK 代理。
CGLIB
CGLIB 通过 Enhancer 生成目标类的子类,覆盖非 final、非 private、非 static 的方法,把调用交给 MethodInterceptor.intercept。它不需要接口,但要求:
- 目标类不是 final;
- 被代理的方法不是 final/private/static;
- 能调用到合适的父类构造器。
构造器调用次数要分清两种用法:
- 直接用
Enhancer.create():默认调用父类的无参构造器,每个代理实例一次。 - Spring AOP 创建 CGLIB 代理:Spring 使用 Objenesis 绕过代理类的构造器,不会再调用目标类的构造器;目标 bean 的构造器在容器创建 bean 时已经调用过一次。
所以「CGLIB 会把构造器调用两次」是把「目标 bean 创建」和「代理实例创建」混在一起了。准确说法是:原生 CGLIB 默认调用一次父类构造器,Spring 的 CGLIB 代理通常不调用。
CGLIB 的 FastClass 为每个方法生成索引,调用时按索引直接跳转,避免反射的 Method.invoke 开销;但代理类生成、类加载和 Metaspace 占用是实际成本。
Spring 到底选哪个
| 场景 | 默认行为 |
|---|---|
Spring Framework @EnableAspectJAutoProxy | proxyTargetClass=false:有接口用 JDK 代理,无接口用 CGLIB |
| Spring Boot 2.x / 3.x 自动配置 | spring.aop.proxy-target-class=true:默认 CGLIB |
显式 proxyTargetClass=true | 强制 CGLIB |
显式 spring.aop.proxy-target-class=false | 有接口时回到 JDK 代理 |
这两个默认值经常被混为一谈:Spring Boot 确实默认 CGLIB,但 Spring Framework 原生默认不是。排查代理类型时先看当前环境有没有覆盖这个属性。
另外,Spring AOP 只拦截经过代理的调用。同一个类里 this.method() 不经过代理,事务、缓存、异步注解都会失效。
最小上手
JDK 动态代理的可运行示例:
java
import java.lang.reflect.*;
interface Greeter {
String hello(String name);
}
public class ProxyDemo {
public static void main(String[] args) {
Greeter target = name -> "hi " + name;
Greeter proxy = (Greeter) Proxy.newProxyInstance(
Greeter.class.getClassLoader(),
new Class<?>[]{Greeter.class},
(p, method, params) -> {
System.out.println("before " + method.getName());
return method.invoke(target, params);
});
System.out.println(proxy.hello("java"));
}
}bash
javac ProxyDemo.java && java ProxyDemo
# before hello
# hi java把 Greeter 换成没有接口的类,Proxy.newProxyInstance 就无法工作;这时才需要 CGLIB 或 ByteBuddy。
生产动作
- 先判断代理类型:
AopUtils.isJdkDynamicProxy(bean)、AopUtils.isCglibProxy(bean);取真实类型用AopUtils.getTargetClass(bean),不要直接bean.getClass()。 - 自调用拆出去:事务/缓存方法被同类方法直接调用时不经过代理;把方法拆到另一个 bean,或注入自身代理。
- final 检查:CGLIB 无法代理 final 类、final 方法、private 方法;Spring 启动时可能报错,或静默不增强,二者都要在测试里覆盖。
- 构造器副作用:如果目标类构造器里有注册、连接、启动线程等副作用,确认代理创建不会重复执行;Spring 的 Objenesis 绕过构造器,但原生 CGLIB 不会。
- Metaspace:大量动态代理类会占用元空间;开启
-XX:MaxMetaspaceSize兜底,并用jcmd <pid> VM.classloader_stats观察类数量。 - 接口设计:需要 JDK 代理的远程调用、RPC、MyBatis Mapper 都依赖接口;接口尽量小而稳定,不要把实现细节暴露成默认方法。
边界
- JDK 代理不能代理类:没有接口就只能换 CGLIB/ByteBuddy;不要为了代理强行造一个空接口。
- CGLIB 不能代理 final:final 类、final 方法、private 方法、static 方法都不行;Spring AOP 对 final 方法通常不会报错,而是直接不增强。
- 代理不等于 AspectJ:Spring AOP 是运行时代理,只拦截 Spring bean 的外部调用;AspectJ 是编译期/加载期织入,能拦截自调用和普通对象,但需要不同的构建配置。
- 性能不是选型唯一标准:JDK 代理和 CGLIB 的开销在不同 JDK、不同调用模式下差异很大;先用真实压测,不要凭「CGLIB 一定快」下结论。
- 代理会改变类型:注入的是代理对象,
getClass()不是目标类,instanceof对实现类可能失败;需要真实类型时用AopUtils或TargetSource。
讲清楚
JDK 动态代理生成一个实现接口的类,方法统一进
InvocationHandler;目标对象的构造器不会被代理调用,代理实例的构造器也只调用一次。CGLIB 生成目标类的子类,原生Enhancer默认调用一次父类构造器,但 Spring 用 Objenesis 绕过代理构造器,所以「构造器调用两次」的说法不准确。Spring Framework 原生默认是「有接口 JDK、无接口 CGLIB」,Spring Boot 2.x/3.x 默认
proxy-target-class=true,也就是默认 CGLIB。选型看边界:有接口且想少生成类用 JDK 代理,无接口或必须代理类用 CGLIB,但 final 和自调用始终是坑。生产上先确认代理类型,再把自调用拆出去。