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 就完全看不到你的实现。
立即学习“Java免费学习笔记(深入)”;
- ✅ 正确位置:
src/main/resources/META-INF/services/com.example.Logger - ❌ 常见错误:
- 文件名大小写不对(
com.example.logger≠com.example.Logger) - 实现类没打到最终运行的 classpath(比如 Maven 多模块中漏加
spi-impl依赖) - 使用了 IDE 的“热部署”或“构建输出到 target/classes”,但没刷新 resources 目录
- 文件名大小写不对(
ServiceLoader 加载时到底发生了什么?
它不是“懒加载所有实现”,而是延迟迭代 + 即时反射:
- 调用
ServiceLoader.load(...)只是创建一个 loader 对象,不读文件、不加载类 - 第一次调用
iterator().hasNext()或forEach(...)时,才去读配置、解析类名、用 TCCLClass.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、重新解析文件、重新反射——高频调用场景(如日志门面)必须自己加单例缓存,否则性能雪崩。










