Spring Boot 默认使用 CGLIB,但CGLIB 无法代理 final 类或 final 方法

那么当这两件事冲突时,Spring Boot 是怎么"解决"的呢?

答案是:它不解决,也无法解决。当这种情况发生时,你的应用程序会直接启动失败。

这不是 Spring Boot 的疏忽,而是由 CGLIB 的底层原理和 Java 语言的规则所决定的。

工作流程和失败原因

让我们来模拟一下 Spring Boot 启动时会发生什么:

  1. Spring 容器开始创建所有的 Bean。
  1. 它找到了一个需要被 AOP 增强的 Bean(例如,一个被 @Service 注解的类,并且它的方法匹配了某个 @Aspect 切面)。
  1. Spring Boot 查看 AOP 配置,发现默认使用 CGLIB (proxy-target-class=true)。
  1. 它尝试为这个 Bean 创建一个代理。CGLIB 上场,准备动态地创建这个 Bean 的一个子类。
  1. 此时,CGLIB 发现这个 Bean 的类是 final 的。
  1. Java 语法规定 final 类不能被继承。CGLIB 的核心机制被阻断了。
  1. CGLIB 抛出一个异常。
  1. 这个异常会向上传递,最终导致 Spring 容器无法创建这个 Bean。
  1. Bean 创建失败,整个 Spring 应用程序的启动过程被中断,并抛出 BeanCreationException 或类似的错误。

你通常会在控制台日志中看到非常明确的错误信息,它会告诉你:

> Caused by: java.lang.IllegalArgumentException: Cannot subclass final class com.example.YourFinalService

这个错误是响亮而明确的 (Fail-fast)。它在启动时就告诉你"此路不通",而不是在运行时产生一些难以预料的奇怪行为。

为什么 Spring Boot 仍然选择 CGLIB 作为默认?

这是一个设计上的权衡取舍。Spring Boot 的设计者认为:

  1. final 业务类是少数情况:在大多数业务应用开发中,开发者很少会将自己写的 Service 或 Component 类声明为 final。
  1. 内部调用 AOP 失效问题更常见、更隐蔽:相比之下,使用 JDK 代理时,开发者在同一个类中调用 this.anotherMethod() 导致 AOP 失效的问题,是一个非常常见且容易让人困惑的陷阱。它不会报错,只是静默地不工作,非常难以排查。

所以,Spring Boot 选择了"长痛不如短痛":

  • 默认 CGLIB:解决了那个常见且隐蔽的"内部调用"问题,让 AOP 的行为在95%的场景下都符合直觉。
  • 代价:当遇到那5%的 final 类场景时,它会以一种非常直接、暴力的方式(启动失败)来提醒开发者。

总结:开发者如何应对?:

  • 首选方案:如果代码可控,移除 final 关键字。这是最简单、最直接的修复方式。
  • 备选方案:如果不能移除 final,就为这个类提取一个接口,然后在注入点使用接口,让 AOP 可以通过 JDK 代理工作(但这可能需要你手动将 spring.aop.proxy-target-class 设置为 false,或者进行更细粒度的控制)。
  • 终极方案:如果以上都不行,才考虑使用 AspectJ 静态织入。