SPI通过配置文件解耦实现类,避免硬编码;需绕过双亲委派模型,依赖上下文类加载器;ServiceLoader.iterator()会全量实例化,非懒加载;spring.factories非标准SPI,是Spring自定义的按需加载机制。

SPI 解决了硬编码实现类导致的不可替换问题
当你的代码里写死 new MySQLDriver() 或 new LogbackLogger(),换数据库或日志框架就得改源码、重新编译——这在中间件、框架、SaaS 服务中完全不可接受。SPI 把“用哪个实现”这件事从代码里拎出来,交给配置文件和类路径决定:只要把新实现的 JAR 放进 classpath,并在 META-INF/services/com.example.PaymentService 里写上类名,ServiceLoader.load(PaymentService.class) 就能自动找到它。
为什么必须破坏双亲委派模型才能用 SPI?
JDK 核心类(比如 java.sql.DriverManager)由 Bootstrap 类加载器加载,而第三方驱动(如 com.mysql.cj.jdbc.Driver)通常在应用 classpath 下,由 AppClassLoader 加载。按双亲委派,Bootstrap 无法委托子加载器去加载应用类——所以 ServiceLoader 在初始化时会主动使用 Thread.currentThread().getContextClassLoader(),绕过默认委派链。这意味着:如果你在非主线程(比如线程池任务)里调用 ServiceLoader,且没显式设置上下文类加载器,就会加载失败或返回空迭代器。
ServiceLoader.iterator() 的陷阱:不是懒加载,而是全量实例化
ServiceLoader 的 iterator() 方法一调用就会遍历所有配置项、反射构造每个实现类——哪怕你只想要第一个匹配的。常见错误包括:
- 某个实现类的静态块里连接了远程配置中心,结果一加载就超时
- 多个实现类都继承了同一个耗资源基类,全被初始化浪费内存
- 并发环境下多个线程同时调用
iterator(),可能触发重复加载(ServiceLoader本身不是线程安全的)
如果真要按需加载,得自己封装一层:读取 META-INF/services/xxx 文件内容,用 Class.forName(..., false, loader) 手动加载类,再用 clazz.getDeclaredConstructor().newInstance() 实例化——跳过 ServiceLoader 的自动机制。
立即学习“Java免费学习笔记(深入)”;
Spring Boot 的 spring.factories 不是标准 SPI,但思路一脉相承
spring.factories 文件位置也是 META-INF/spring.factories,格式是 org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx.AbcAutoConfiguration,但它不依赖 ServiceLoader,而是 Spring 自己解析并控制加载时机和条件(比如配合 @ConditionalOnClass)。关键区别在于:标准 SPI 是“发现即加载”,而 spring.factories 是“发现后按规则择优加载”。别误以为加了 spring.factories 就等于用了 Java SPI——它只是借了目录结构和配置风格,底层逻辑完全不同。
最常被忽略的一点:SPI 配置文件名必须是**完整接口类名**(含包路径),大小写敏感,不能有空格或 BOM;文件编码必须是 UTF-8 无签名;路径必须严格为 META-INF/services/xxx.xxx.Xxx ——少一个字母、多一个斜杠、用错类加载器,都会静默失败,且没有任何异常抛出,只会返回空 Iterator。










