Java基础、中级、高级、架构面试资料

欠了 8 年的 ServiceLoader 兼容性债,JDK 27 还清了

JAVA herman 5浏览
公告:“业余草”微信公众号 AI 中转站提供免费体验,点击链接 https://unity2.ai/register?ref=3XTnndN2 进行访问,支持 Claude、ChatGPT、Gemini 等最新模型!关注业余草微信公众号,添加作者微信:xttblog2!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
【腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

JDK 27 下周就要发布了,我刚去看了一眼,发现了一个存在了 8 年的技术债被修复了。

这么多年了,估计大家都忘了它了,谁知道它竟然想起来给修复了。

ServiceLoader

在 Java 生态里待得够久的老程序员应该知道,在某些使用 ServiceLoader 的场景。应用启动时突然抛出一个 NoClassDefFoundError,堆栈指向 ServiceLoader,但你翻遍代码也找不到哪里直接调用了它。你用的是某个依赖 SPI 的框架,它在底层加载实现类时遇到了类路径问题。

这个问题困扰了不少 ServiceLoader 开发者,而且是很久了。因为抛出的异常类型不稳定。有时候是 ServiceConfigurationError,有时候是 LinkageError 或其子类(如 NoClassDefFoundError)。同样是“服务提供者加载失败”,却可能抛出两种完全不同的异常。

现在,JDK 27 站出来了,终于要终结这种混乱了。

问题从何而来?

ServiceLoader 的规范其实写得清清楚楚,迭代器的 hasNext()next() 方法在“定位、加载或实例化服务提供者时发生错误”时,应当抛出 ServiceConfigurationError

文章配图参见我的公众号:https://mp.weixin.qq.com/s/B-3oeVP2RCsgwu_EprWlhw

但规范归规范,实现归实现。

从 JDK 9 开始,ServiceLoader 被大幅重写,导致它的行为出现了裂痕。

  • 场景一,当 Class.forName 加载服务提供者类时,如果类本身存在链接错误(比如依赖的类找不到),LinkageError 会被直接传播出去,而不是包装成 ServiceConfigurationError
  • 场景二,JDK 24 移除了 Security Manager 支持后,获取无参构造器的代码路径发生了变化。如果 getConstructor 触发了类加载并抛出 NoClassDefFoundError,这个错误同样会直接冒泡到调用者。

这就造成了一个尴尬的局面,规范说应该抛 ServiceConfigurationError,但实际在某些常见故障场景下,你拿到的是 NoClassDefFoundError

典型的说一套做一套!

为什么开发者会踩坑?

因为这两种异常的处理方式完全不同。

ServiceConfigurationErrorError 的子类,专门为“服务配置问题”设计。框架和工具通常会在捕获它后,输出更友好的诊断信息——比如“某某 SPI 实现无法加载,请检查依赖”。

LinkageError 及其子类(NoClassDefFoundErrorNoSuchMethodError 等)是 JVM 层面的错误,通常被视为“环境坏了”,难以在应用层优雅恢复。更麻烦的是,两者可能同时出现在同一段代码里。某些故障走 A 路径抛 ServiceConfigurationError,另一些走 B 路径抛 NoClassDefFoundError

有一个真实的例子,Yellowfin BI 和 Liquibase 就因为这个行为不一致,直接建议用户不要升级到 JDK 24/25。对于一个成熟的 Java 库来说,这种兼容性断裂是致命的,你无法要求所有下游用户去适配一个 JDK 版本的行为漂移。

JDK 27 的修复逻辑

Alan Bateman 在 OpenJDK 的 PR 中明确说明了修复方案,将实现改为在遇到链接错误时一致地抛出 ServiceConfigurationError,并将原始的 LinkageError 作为 cause 保留。

这意味着:

  1. 规范终于和实现对齐了。无论故障发生在加载阶段还是实例化阶段,调用者只需要 catch ServiceConfigurationError 就够了。
  2. 诊断信息没有丢失。原始异常作为 cause 被包装进去,开发者依然可以通过 getCause() 拿到 NoClassDefFoundError 的具体信息。
  3. 行为可预测了。框架和中间件可以放心地依赖“服务加载失败 = ServiceConfigurationError”这个契约,写出更健壮的错误处理逻辑。

Alan 在讨论中也承认这是一个可观察的行为变更,并为此专门走了 CSR 流程来评估影响。但他同时指出,问题的根源在于“坏环境”,编译期和运行期的类不匹配、缺失的依赖、或者字节码注入引用了不可见的类。这些场景下,统一异常类型本身就是一种改进

一个更大的反思

这次修改还暴露了一个更深层的问题,反射操作触发类加载的边界在哪里

Class.getConstructor() 的规范并没有明确说它可能抛出 LinkageError。当 ServiceLoader 调用它来获取无参构造器时,它只是在“查找一个方法”,并不预期会触发真正的类初始化。但 JVM 的类加载机制是懒惰的,符号引用在解析时才真正加载类,而这一步可能失败。

正如 Alan 在 PR 讨论中提到的,“反射操作触发类加载是一个更大的话题,它同样影响 find 和枚举方法”。JDK 27 的这次修改,选择了先解决 ServiceLoader 这个具体场景,而不是等待一个可能永远不会到来的“完美方案”。

对开发者的实际影响

如果你维护的代码直接或间接使用了 ServiceLoader

则需要做到:

  • 检查是否有 catch LinkageError(或其子类)来处理服务加载失败的代码。如果有,考虑是否需要同时 catch ServiceConfigurationError
  • 如果你的框架用 ServiceLoader 做插件发现,确保错误处理逻辑覆盖 ServiceConfigurationError

当然,如果是下面的这两种情况,大家可以不必要担心。

  • 原有的 ServiceConfigurationError 捕获逻辑依然有效。
  • 原始异常作为 cause 保留,诊断能力没有下降。

一个值得记住的原则是,当 JDK 的规范说“应当抛出 X”但实现却抛出 Y 时,这通常不是设计意图,而是历史遗留的 bug。JDK 27 的这次修复,就是把一个存在了至少 8 年(从 JDK 9 算起)的规范与实现的偏差修正了。

今天周五,祝大家周末愉快!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » 欠了 8 年的 ServiceLoader 兼容性债,JDK 27 还清了