那么当这两件事冲突时,Spring Boot 是怎么"解决"的呢?
答案是:它不解决,也无法解决。当这种情况发生时,你的应用程序会直接启动失败。
这不是 Spring Boot 的疏忽,而是由 CGLIB 的底层原理和 Java 语言的规则所决定的。
工作流程和失败原因
让我们来模拟一下 Spring Boot 启动时会发生什么:
- Spring 容器开始创建所有的 Bean。
- 它找到了一个需要被 AOP 增强的 Bean(例如,一个被 @Service 注解的类,并且它的方法匹配了某个 @Aspect 切面)。
- Spring Boot 查看 AOP 配置,发现默认使用 CGLIB (proxy-target-class=true)。
- 它尝试为这个 Bean 创建一个代理。CGLIB 上场,准备动态地创建这个 Bean 的一个子类。
- 此时,CGLIB 发现这个 Bean 的类是 final 的。
- Java 语法规定 final 类不能被继承。CGLIB 的核心机制被阻断了。
- CGLIB 抛出一个异常。
- 这个异常会向上传递,最终导致 Spring 容器无法创建这个 Bean。
- Bean 创建失败,整个 Spring 应用程序的启动过程被中断,并抛出 BeanCreationException 或类似的错误。
你通常会在控制台日志中看到非常明确的错误信息,它会告诉你:
> Caused by: java.lang.IllegalArgumentException: Cannot subclass final class com.example.YourFinalService
这个错误是响亮而明确的 (Fail-fast)。它在启动时就告诉你"此路不通",而不是在运行时产生一些难以预料的奇怪行为。
为什么 Spring Boot 仍然选择 CGLIB 作为默认?
这是一个设计上的权衡取舍。Spring Boot 的设计者认为:
- final 业务类是少数情况:在大多数业务应用开发中,开发者很少会将自己写的 Service 或 Component 类声明为 final。
- 内部调用 AOP 失效问题更常见、更隐蔽:相比之下,使用 JDK 代理时,开发者在同一个类中调用 this.anotherMethod() 导致 AOP 失效的问题,是一个非常常见且容易让人困惑的陷阱。它不会报错,只是静默地不工作,非常难以排查。
所以,Spring Boot 选择了"长痛不如短痛":
- 默认 CGLIB:解决了那个常见且隐蔽的"内部调用"问题,让 AOP 的行为在95%的场景下都符合直觉。
- 代价:当遇到那5%的 final 类场景时,它会以一种非常直接、暴力的方式(启动失败)来提醒开发者。
总结:开发者如何应对?:
- 首选方案:如果代码可控,移除 final 关键字。这是最简单、最直接的修复方式。
- 备选方案:如果不能移除 final,就为这个类提取一个接口,然后在注入点使用接口,让 AOP 可以通过 JDK 代理工作(但这可能需要你手动将 spring.aop.proxy-target-class 设置为 false,或者进行更细粒度的控制)。
- 终极方案:如果以上都不行,才考虑使用 AspectJ 静态织入。