什么情况下 Spring 会强制从 JDK 动态代理降级/切换为 CGLIB 代理?
在 Spring 框架中,动态代理机制主要有两种:JDK 动态代理(基于接口)和 CGLIB 代理(基于子类)。
虽然早期的 Spring 默认遵循“有接口就用 JDK,没接口才用 CGLIB”的原则,但在某些特定情况或配置下,Spring 会强制降级/切换为 CGLIB 代理。以下是触发这种切换的核心场景:
1. 目标类没有实现任何“业务接口” (物理强制)
这是最根本的原因。JDK 动态代理的底层机制要求目标类必须实现至少一个接口。
- 完全无接口:如果一个类纯粹是一个普通类(POJO),没有实现任何接口,Spring AOP 在为其创建代理时,别无选择,只能通过生成子类的方式,即强制使用 CGLIB。
- 只有 Spring 内部标记接口:如果目标类仅仅实现了 Spring 的回调接口(如
InitializingBean,DisposableBean,ApplicationContextAware等)或 JDK 的标记接口(如Serializable,Cloneable),Spring 的代理机制(ProxyProcessorSupport类)会忽略这些非用户自定义的接口。此时,Spring 依然会认为该类没有实现“有效的业务接口”,从而强制切换为 CGLIB。
2. 代理 @Configuration 注解的配置类 (框架强制)
当你在类上使用 @Configuration 注解时,Spring 会在启动时使用 CGLIB 对其进行增强。
- 原因:在
@Configuration类中,如果@Bean方法 A 调用了@Bean方法 B,为了保证方法 B 返回的始终是 Spring 容器中的同一个单例对象(而不是重新 new 一个新对象),Spring 必须拦截对方法 B 的调用。 - 机制:JDK 动态代理无法拦截类内部的自我调用(self-invocation),也无法对未实现接口的配置类进行代理。因此,Spring 强制使用 CGLIB 生成配置类的子类,重写这些方法以植入从 IoC 容器中获取 Bean 的逻辑。
3. 按类型注入时使用了“实现类”而非“接口” (为避免异常而强制)
在实际开发中,这是一个非常容易踩坑的场景。
假设你有 public class UserServiceImpl implements UserService。
如果你在其他地方这样注入:
@Autowired
private UserServiceImpl userService; // 注入的是实现类类型,而不是 UserService 接口
- 如果使用 JDK 代理:Spring 生成的代理类实现了
UserService接口,但它不是UserServiceImpl的子类。这会导致强转失败,抛出BeanNotOfRequiredTypeException。 - 解决方案:为了防止这种依赖注入时的类型转换异常,开发者通常会通过配置强制 Spring 全部使用 CGLIB 代理。
4. 显式开启了 proxyTargetClass = true (配置强制)
开发者或上层框架明确告诉 Spring:“不要管有没有接口,统统给我基于类生成代理(即 CGLIB)”。
常见的触发方式包括:
<aop:config proxy-target-class="true">(XML配置)@EnableAspectJAutoProxy(proxyTargetClass = true)(AOP 注解配置)@EnableTransactionManagement(proxyTargetClass = true)(事务注解配置)
5. Spring Boot 2.0+ 的默认行为 (时代变迁导致的强制)
如果你使用的是 Spring Boot 2.0 及以上版本,你会发现哪怕你的类实现了接口,Spring 默认生成的依然是 CGLIB 代理。
- 原因:Spring Boot 团队为了彻底解决上述第 3 点提到的
ClassCastException(开发者经常因为注入实现类而不是接口而报错),在 Spring Boot 2.0 中修改了默认配置。 - 源码体现:自动配置类
AopAutoConfiguration默认将spring.aop.proxy-target-class设为了true。这意味着在现代 Spring Boot 应用中,默认就已经强制全局使用 CGLIB 了(除非你在application.yml中显式设置spring.aop.proxy-target-class=false才会退回到 JDK 代理)。
6. AOP 切点(Pointcut)命中了实现类特有的方法
假设 UserServiceImpl 实现 UserService 接口,但 UserServiceImpl 中有一个接口中不存在的方法 customMethod()。
如果你的 AOP 切面表达式恰好切入了这个 customMethod() 方法,或者事务注解 @Transactional 仅加在了这个特有方法上。
由于 JDK 代理只能代理接口中定义的方法,为了让增强逻辑生效,Spring 在某些情况下(结合具体配置)会评估并使用 CGLIB 来代理目标类,因为只有 CGLIB 才能拦截实现类自有的方法。
总结
Spring 会在以下情况切换或强制使用 CGLIB:
- 不能用 JDK 时:目标类无接口,或只有 Spring/JDK 内部标记接口。
- 必须用 CGLIB 时:处理
@Configuration配置类的单例拦截。 - 你要求它用时:显式配置了
proxyTargetClass=true。 - 框架替你决定时:Spring Boot 2.0+ 默认开启全局 CGLIB 以防类型转换异常。