ServiceLoader 是 Java SPI 的官方实现,基于 META-INF/services/ 约定路径和 TCCL 加载接口实现类,本质是 JDK 内置的类路径扫描+文本解析+反射实例化流程。
ServiceLoader 是 Java SPI 机制的唯一官方入口,它不是“一种设计模式”,而是一套基于约定路径 + 线程上下文类加载器(TCCL)的硬编码发现逻辑。
SPI 就是:你定义一个接口(比如 Logger),不写实现;别人在自己 jar 包里写实现类(比如 ConsoleLogger),并在 META-INF/services/com.example.Logger 文件里写上这行:
com.example.ConsoleLogger。然后你用
ServiceLoader.load(Logger.class) 就能自动找到、实例化它——整个过程不写 new,不配 Spring,不改调用方代码。
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.logger ≠ com.example.Logger) spi-impl 依赖) ServiceLoader 加载时到底发生了什么?它不是“懒加载所有实现”,而是延迟迭代 + 即时反射:
ServiceLoader.load(...) 只是创建一个 loader 对象,不读文件、不加载类iterator().hasNext() 或 forEach(...) 时,才去读配置、解析类名、用 TCCL Class.forName(...)
iterator().next() 都会触发一次 newInstance()(Java 9+ 改为 getDeclaredConstructor().newInstance())NoClassDefFoundError 或无参构造器缺失),该实现会被跳过,但不会中断整个遍历这意味着:如果你在 forEach 中抛异常,可能根本不知道是哪个实现崩了——建议用 try-catch 包住单次 next() 调用。
@SPI、Dubbo 的 ExtensionLoader 有啥区别?Java 原生 ServiceLoader 是最简陋的版本:
get("file"))@ConditionalOnClass)Spring Boot 的 spring.factories 是对 SPI 的模仿升级:把配置从 META-INF/services/ 搬到 META-INF/spring.factories,并

SpringFactoriesLoader 解析,再交给 Spring 容器管理——所以你能用 @Autowired 注入,也能用 @Conditional 控制加载时机。
真正容易被忽略的是:原生 SPI 不做任何缓存。每次 load() 都重新扫描 classpath、重新解析文件、重新反射——高频调用场景(如日志门面)必须自己加单例缓存,否则性能雪崩。