JDK 动态代理与 CGLIB
JDK 动态代理与 CGLIB 是 Spring AOP 创建代理的两种方式。代理接收外部方法调用,在业务方法前后加入事务、日志等增强逻辑。本文以 Spring Framework 6.2 和 Spring Boot 3.5 为准。先比较两种代理暴露的类型和可拦截的方法,再说明默认选择、注解匹配和性能判断。
1. 面试核心
先记住这几条,再读正文- 接口代理。 JDK 动态代理实现指定接口,接口方法的调用由
InvocationHandler处理。卡 · JDK 动态代理 - 子类代理。 CGLIB 生成目标类的子类,通过覆盖方法拦截调用,因此可以代理没有业务接口的类。
- Boot 默认选择。 Boot 3.5 的 AOP 自动配置默认使用类代理,
spring.aop.proxy-target-class=false让有接口的目标改用 JDK 代理。 - final 方法与接口。 JDK 代理可以增强接口调用,即使目标实现方法声明为
final。卡 · final 实现方法与接口代理 - 事务方法可见性。 Spring 6.0 起,类代理默认支持在
protected和包可见方法上使用@Transactional。
2. JDK 动态代理
JDK 动态代理在运行时生成一个实现指定接口的类。调用代理的接口方法时,代理把方法和参数交给 InvocationHandler。它是处理代理调用的接口,可以在转发给目标对象前后执行增强逻辑。下面用它把支付接口的调用转发给目标对象:
PaymentService target = new DefaultPaymentService();
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
PaymentService.class.getClassLoader(),
new Class<?>[] {PaymentService.class},
(object, method, arguments) -> {
try {
return method.invoke(target, arguments);
} catch (InvocationTargetException exception) {
throw exception.getCause();
}
}
);
示例中的代理实现 PaymentService,第三个参数中的 Lambda 就是调用处理器。调用代理的接口方法后,处理器用 method.invoke(target, arguments) 执行目标方法。
目标方法抛出异常时,反射会把它包装成 InvocationTargetException。示例取出 getCause() 并重新抛出,让处理器传出原始异常。若它是接口方法未声明的受检异常,JDK 代理仍会包装成 UndeclaredThrowableException。
调用方应使用代理实现的接口类型接收它。这个代理与 DefaultPaymentService 都实现了接口,但彼此没有继承关系。因此,代理无法强制转换为目标实现类,接口未声明的方法也不会通过该接口暴露。
在 Spring 中,按目标实现类查找或注入这类代理 Bean,也会遇到类型不匹配。因此,应按代理实现的接口声明依赖。
3. CGLIB
CGLIB 通过生成子类,为没有业务接口的目标类提供代理。子类覆盖目标方法后,调用先进入覆盖后的方法,再执行增强逻辑。因此,能否拦截某个方法,取决于 Java 的继承和可见性规则:
| 成员 | 子类代理边界 |
|---|---|
final 类 | 无法生成子类 |
final 方法 | 无法覆盖 |
private 方法 | 子类无法覆盖 |
static 方法 | 属于类调用,无法通过实例覆盖 |
| 跨包不可见方法 | 受 Java 可见性约束 |
Spring 已把 CGLIB 重新打包到 spring-core 中,使用 Spring AOP 时无需单独添加 cglib 依赖。Java 模块还控制包和成员能否被外部访问。模块没有开放所需访问时,即使类允许继承,代理生成也会受限。
Spring 使用 Objenesis 创建类代理实例。Objenesis 能绕过构造器创建对象,所以代理实例无需再执行一次目标类构造器。在 JVM 支持这一机制时,目标构造器只为目标实例执行一次。若 JVM 禁止绕过构造器,Spring 会尝试调用构造器创建代理。回退成功后,目标类的构造器会额外执行一次。
Kotlin 的类和方法默认是 final,因此也受这些继承限制。使用类代理时,需要让目标类和方法可继承、可覆盖。项目可显式使用 open,或通过 kotlin-spring 插件开放符合条件的组件。
4. Spring 的默认选择
判断实际代理类型时,需要区分 Framework 的默认规则与 Boot 的自动配置。Spring Framework 6.2 默认在目标有接口时使用 JDK 代理,没有接口时使用 CGLIB。设置 proxyTargetClass=true 可以强制使用类代理。
Boot 3.5 的 AOP 自动配置默认启用类代理。设置 spring.aop.proxy-target-class=false 后,有接口的目标改用 JDK 代理,没有接口的目标仍需类代理。Boot 2.7 的 AOP 自动配置也采用这项默认策略。
多个代理配置会共同影响最终结果,所以排查时应检查容器实际交付的对象:
AopUtils.isAopProxy(bean);
AopUtils.isJdkDynamicProxy(bean);
AopUtils.isCglibProxy(bean);
上面的三个方法分别判断对象是否为 Spring AOP 代理、JDK 代理和 CGLIB 代理。它们返回布尔值,可用于核对实际配置是否生效。即使目标类实现了接口,强制类代理的配置也会让结果变成 CGLIB。
5. 自调用
目标对象用 this 调用自身方法时,两种代理都会被绕过,原因与拆分 Bean 等处理方式见 Spring AOP。
6. 代理类型与注解匹配
读取业务注解时,需要找到真正声明注解的类或方法。直接对代理调用 getClass(),得到的是生成的代理类。因此,自定义反射代码应先识别代理,再定位目标类和该类实际执行的方法。
接口方法和实现方法是不同的反射对象,只检查其中一个会漏掉另一处的注解。Spring 的事务属性解析器会处理它们的对应关系,也会解析编译器生成的桥接方法。桥接方法用于维持泛型重写等情况下的多态调用,所以读取注解还需找到它对应的业务方法。
确定代理能够拦截方法后,还要看具体增强是否识别它。Spring 6.0 起,类代理的事务默认支持 protected 和包可见方法。JDK 代理的事务入口则必须是代理接口中声明的 public 方法。将事务属性解析器的 publicMethodsOnly 设为 true,会把识别范围收窄到公开方法。
7. 性能
选择代理类型时,先确认调用方需要什么类型,以及哪些方法需要增强。在包含数据库或网络 I/O 的业务中,代理调用开销通常小于这些等待和业务计算。
分析代理耗时时,需要区分以下三类开销:
- 方法分派负责把调用送到处理器或目标方法。
- 切点判断负责确定当前方法应执行哪些增强。
- 拦截链执行事务、日志等实际增强逻辑,这些逻辑也会消耗时间。
高频调用且方法体很短时,代理开销更容易影响总耗时。这时应使用实际部署的 JDK、Spring 和 CGLIB 版本做测量,因为实现会随版本变化。可以用 JMH 做微基准测试,再用性能剖析工具定位真实调用链的耗时。测试应包含实际增强逻辑,只测空代理难以反映业务中的成本。
8. final 实现方法与接口代理
final 限制的是继承和覆盖,因此对两种代理的影响不同。JDK 代理自己实现接口,再把调用转交给目标对象,整个过程无需继承目标类。下面的类和方法都声明为 final:
interface PaymentService {
void pay();
}
final class DefaultPaymentService implements PaymentService {
public final void pay() {}
}
该实现可以作为 JDK 代理的目标。通过代理接口调用 pay() 时,调用先进入 InvocationHandler,再由处理器执行目标方法。因此,目标实现上的 final 不妨碍接口调用的增强。
若强制使用 CGLIB,final 类会让子类生成失败。把类改成非 final、仅保留方法上的 final 后,CGLIB 可以生成子类。但 pay() 仍无法被覆盖,所以对这个方法的增强不会生效。
9. 常见问题
9.1 强制 CGLIB 后,非公开方法都能增强吗?
并非都能增强,方法要同时满足可覆盖和增强可识别这两项要求。private 方法无法被子类覆盖,因此会被排除。protected 和同包可见方法还要符合事务等增强的识别规则,具体见代理类型与注解匹配。
9.2 代理对象和目标对象是同一个实例吗?
在本文的 Spring AOP 代理方式中,代理对象与目标对象是两个实例。代理通过 TargetSource 获取目标对象,再执行匹配当前方法的拦截链。TargetSource 是用于取得目标对象的 Spring 接口,链执行到底后才调用目标方法。
容器会把代理引用交给依赖它的其他 Bean。调用方使用这个引用,才能经过增强逻辑;直接持有目标对象并调用,会绕过代理。
10 · 面试题卡片
10.1 JDK 动态代理与 CGLIB 有什么区别
出现公司:阅文集团、美团
考察重点:说明接口转发与子类覆盖怎样影响可调用的方法,并解释 final、private 和自调用的边界。能结合配置判断实际代理类型。
参考回答
JDK 动态代理实现接口,把调用交给 InvocationHandler;CGLIB 继承目标类,通过覆盖方法拦截调用。
- JDK 代理按接口使用。 它无需覆盖目标实现方法,所以接口暴露的方法即使实现为
final,仍可增强。 - CGLIB 可代理无接口的类。 目标类必须可继承,目标方法必须可覆盖,
final类以及final、private方法因此受限。
Spring Framework 默认对有接口的目标使用 JDK 代理;Boot 3.5 的 AOP 自动配置默认使用类代理,可用 spring.aop.proxy-target-class=false 切回接口优先。实际类型可用 AopUtils 核对。两者都要求调用经过代理,this 自调用会绕过增强。
10.2 目标实现方法是 final,JDK 动态代理还能增强它吗?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:解释 JDK 代理为何无需覆盖目标方法,以及 final 对接口转发和子类覆盖的不同影响。
相关内容:final 实现方法与接口代理。
参考回答
可以,前提是该方法由代理接口暴露,并且调用经过代理对象。
JDK 代理实现的是接口,接到调用后先进入 InvocationHandler,再由处理器转发给目标对象。它无需覆盖目标方法,所以目标实现方法为 final 也能增强。
CGLIB 依靠子类覆盖目标方法,final 方法无法提供这种拦截入口。若目标类本身也是 final,子类代理连生成这一步都会失败。