Java SPI 的 META-INF/services/ 必须位于 classpath 根路径,文件名是接口全限定名、UTF-8无BOM编码;ServiceLoader 静默跳过加载失败的实现类,需手动遍历并捕获异常;模块化环境下须在 module-info.java 中声明 uses 和 requires。

Java SPI 的 META-INF/services/ 目录必须放在 classpath 根路径下
很多人把 META-INF/services/ 放在源码目录(如 src/main/java/META-INF/services/)里,结果运行时 ServiceLoader 找不到实现类——因为 Java SPI 只扫描 classpath 根目录下的 META-INF/services/,不是源码路径,也不是 jar 包内任意位置。
正确做法是:确保该目录最终出现在编译输出根目录中(如 target/classes/META-INF/services/)。Maven 项目应把配置文件放在 src/main/resources/META-INF/services/,Gradle 同理用 src/main/resources。
-
META-INF/services/必须全大写,大小写敏感,meta-inf或Meta-Inf均无效 - 服务接口全限定名作为文件名,例如接口为
com.example.Logger,则文件路径必须是META-INF/services/com.example.Logger - 文件编码必须是 UTF-8 无 BOM,Windows 记事本默认保存可能带 BOM,会导致
ServiceLoader解析失败
ServiceLoader.load() 不会主动抛出类加载异常,但会静默跳过失败项
调用 ServiceLoader.load(YourInterface.class) 后遍历 iterator() 时,若某个实现类因 NoClassDefFoundError、ClassNotFoundException 或静态块抛异常,ServiceLoader 默认忽略它,继续尝试下一个——你根本不知道哪个实现“悄悄挂了”。
排查方式只能靠手动触发加载并捕获异常:
立即学习“Java免费学习笔记(深入)”;
ServiceLoader<YourInterface> loader = ServiceLoader.load(YourInterface.class);
for (YourInterface impl : loader) {
try {
// 触发实际实例化(可能抛异常)
impl.doSomething();
} catch (Throwable t) {
System.err.println("Failed to load SPI impl: " + impl.getClass().getName());
t.printStackTrace();
}
}
- 不要只依赖
loader.iterator().hasNext()判断是否有可用实现——它返回true并不意味着能成功构造实例 - 如果多个 jar 提供同一接口的实现,JVM 按 classpath 顺序加载,但不保证顺序稳定(尤其模块化环境),别依赖加载次序
接口方法签名不能含泛型参数或默认方法(Java 8+ 兼容性陷阱)
SPI 接口本身可以是泛型(如 interface Processor<T>),但 ServiceLoader 加载的是具体类型擦除后的类。真正容易出问题的是:接口里写了 default 方法,而实现类没重写,又在 JDK 8 以下运行——会报 java.lang.IncompatibleClassChangeError。
- 尽量让 SPI 接口保持“纯抽象”,避免
default方法;如有必要,确保所有目标 JDK 版本都支持(JDK 8+) - 泛型仅用于编译期检查,运行时无法通过反射获取真实类型参数,所以不要在 SPI 加载逻辑里依赖泛型类型做分发
- 接口中禁止使用
static方法——ServiceLoader只找 public 无参构造器的实现类,static 方法不会被识别为服务提供者
模块化(JPMS)下 requires 和 uses 必须显式声明
如果你用 JDK 9+ 且启用了模块系统(module-info.java),SPI 行为会变严格:即使 META-INF/services/ 文件存在,若模块未声明 uses YourInterface,ServiceLoader 就不会扫描该模块里的实现类。
同时,提供实现的模块还必须 requires 服务接口所在模块,否则编译失败。
module logger.impl {
requires logger.api; // 接口所在模块
uses com.example.Logger; // 声明“我参与这个SPI”
}
- 没加
uses是最常见遗漏点,现象是ServiceLoader返回空迭代器,但文件和类明明都存在 - 模块名必须与
module-info.java中一致,大小写敏感;路径中的/不能写成. - 非模块化代码(传统 classpath)不受此限制,但混用时要注意模块路径优先级高于 classpath










