在Java里什么是SPI机制_Java服务发现机制解析

ServiceLoader 是 JDK 内置的 SPI 官方入口,需满足三要素:接口与实现类在 classpath 中、META-INF/services/下以接口全限定名命名文件、文件内容为实现类全限定名(UTF-8 编码,无注释);其加载依赖上下文类加载器,易因类加载器隔离、构造异常、资源丢失等静默失败。

ServiceLoader 是 Java SPI 机制的唯一官方入口,它不是“可选方案”,而是 JDK 内置的、约定驱动的服务发现机制——接口定义方不写实现,也不硬编码类名,只靠 META-INF/services/接口全限定名 文件触发运行时加载。

怎么让 ServiceLoader 找到你的实现类

关键不是“写个接口+实现类”就完事,而是必须满足三要素:

  • 接口和实现类必须在 classpath 中(比如打包进 jar 或放在 resources 下)
  • META-INF/services/ 目录下创建一个文件,文件名必须是接口的 全限定名(例如 com.example.Search
  • 该文件内容必须是实现类的 全限定名,一行一个,不能有空格或注释(# 不被识别,会被当作类名一部分导致 ClassNotFoundException

为什么 ServiceLoader.iterator() 有时不抛错却没加载到类

常见静默失败原因:

  • ServiceLoader.load(Interface.class) 使用的是 Thread.currentThread().getContextClassLoader(),如果你在 Web 容器、Spring Boot 或模块化环境(Java 9+)中运行,这个 CL 很可能看不到你放配置文件的模块 —— 改用 ServiceLoader.load(Interface.class, YourClass.class.getClassLoader()) 更可靠
  • 实现类构造器抛异常(比如依赖未注入),ServiceLoader 会吞掉异常并跳过该实现,iterator.hasNext() 仍返回 true,但 next() 会直接抛出 ServiceConfigurationError
  • 文件编码不是 UTF-8(尤其 Windows 记事本保存默认是 GBK),会导致类

    名读取乱码,加载失败

和 Spring 的 @SPI 或 Dubbo 的 @SPI 不是一回事

JDK 原生 ServiceLoader 是最简模型,没有分组、优先级、条件加载、懒初始化等能力:

  • 它不支持按参数动态选择实现(比如根据 type=xml 加载 XmlSerializer
  • 它不缓存实例,每次 next() 都 new 一次对象 —— 如果实现类有状态或开销大,得自己加单例包装
  • 它不支持 fallback 机制:如果配置了 3 个实现,其中 1 个加载失败,其余 2 个仍可用;但不会告诉你哪几个挂了
ServiceLoader loader = ServiceLoader.load(Search.class);
for (Search s : loader) {
    // 每次循环都 new 一个新实例,不是复用
    s.searchDoc("test");
}

真正上线项目里最容易被忽略的一点

你写的 META-INF/services/com.example.Xxx 文件,在 Maven 多模块构建中极易丢失 —— 尤其当实现类在子模块,而测试代码在父模块启动时,resources 可能没被打包进最终 jar,或者被 shade 插件误删。务必验证最终 jar 包里是否存在该路径和文件,别只信 IDE 的 “Run” 按钮。