在Java中什么是SPI机制_Java服务发现原理解析

ServiceLoader 是 Java SPI 的官方实现,基于 META-INF/services/ 约定路径和 TCCL 加载接口实现类,本质是 JDK 内置的类路径

扫描+文本解析+反射实例化流程。

ServiceLoader 是 Java SPI 机制的唯一官方入口,它不是“一种设计模式”,而是一套基于约定路径 + 线程上下文类加载器(TCCL)的硬编码发现逻辑


什么是 SPI?一句话说清本质

SPI 就是:你定义一个接口(比如 Logger),不写实现;别人在自己 jar 包里写实现类(比如 ConsoleLogger),并在 META-INF/services/com.example.Logger 文件里写上这行:

com.example.ConsoleLogger
。然后你用 ServiceLoader.load(Logger.class) 就能自动找到、实例化它——整个过程不写 new,不配 Spring,不改调用方代码。

  • 这不是“框架功能”,而是 JDK 自带的 类路径扫描+文本解析+反射实例化 流程
  • 它只认 META-INF/services/ 下以接口全限定名为名的文件,其他路径、其他格式(如 JSON/YAML)一概无视
  • 它默认使用 Thread.currentThread().getContextClassLoader() 加载实现类,不是当前类的 classloader

为什么必须放 META-INF/services/?路径错一个字符就失效

因为 ServiceLoader 的源码里写死了这个路径逻辑:

private static final String PREFIX = "META-INF/services/";

它会拼出 META-INF/services/com.example.Logger,然后调用 classLoader.getResources(PREFIX + service.getName()) 去查所有匹配资源。一旦你写成 META-INF/service/(少个 s)、meta-inf/...(小写)、或放在 src/test/resources(测试路径未打进 jar),ServiceLoader 就完全看不到你的实现。

  • ✅ 正确位置:src/main/resources/META-INF/services/com.example.Logger
  • ❌ 常见错误:
    • 文件名大小写不对(com.example.loggercom.example.Logger
    • 实现类没打到最终运行的 classpath(比如 Maven 多模块中漏加 spi-impl 依赖)
    • 使用了 IDE 的“热部署”或“构建输出到 target/classes”,但没刷新 resources 目录

ServiceLoader 加载时到底发生了什么?

它不是“懒加载所有实现”,而是延迟迭代 + 即时反射

  • 调用 ServiceLoader.load(...) 只是创建一个 loader 对象,不读文件、不加载类
  • 第一次调用 iterator().hasNext()forEach(...) 时,才去读配置、解析类名、用 TCCL Class.forName(...)
  • 每次 iterator().next() 都会触发一次 newInstance()(Java 9+ 改为 getDeclaredConstructor().newInstance()
  • 如果某个实现类构造失败(比如抛 NoClassDefFoundError 或无参构造器缺失),该实现会被跳过,但不会中断整个遍历

这意味着:如果你在 forEach 中抛异常,可能根本不知道是哪个实现崩了——建议用 try-catch 包住单次 next() 调用。


和 Spring 的 @SPI、Dubbo 的 ExtensionLoader 有啥区别?

Java 原生 ServiceLoader 是最简陋的版本:

  • ❌ 不支持按名称获取指定实现(如 get("file")
  • ❌ 不支持扩展点的激活条件(如 @ConditionalOnClass
  • ❌ 不支持实现类排序、分组、优先级
  • ❌ 不支持依赖注入(所有实现类都是无参构造 + 手动 new)

Spring Boot 的 spring.factories 是对 SPI 的模仿升级:把配置从 META-INF/services/ 搬到 META-INF/spring.factories,并用 SpringFactoriesLoader 解析,再交给 Spring 容器管理——所以你能用 @Autowired 注入,也能用 @Conditional 控制加载时机。

真正容易被忽略的是:原生 SPI 不做任何缓存。每次 load() 都重新扫描 classpath、重新解析文件、重新反射——高频调用场景(如日志门面)必须自己加单例缓存,否则性能雪崩。