ServiceLoader通过读取META-INF/services/下以接口全限定名命名的配置文件来加载实现类,每行一个无参构造器的实现类全限定名,需手动维护且严格校验格式;加载时机为首次调用iterator()时,不支持依赖注入或传参,与Spring容器隔离,适用于简单插件场景。

ServiceLoader 怎么加载接口实现类
它不靠反射扫描 classpath,而是读 META-INF/services/ 下的配置文件,每行写一个实现类全限定名。你得自己手动维护这个文件,漏写、多空格、类名拼错都会导致 ServiceLoader 找不到实现。
常见错误现象:NoClassDefFoundError 或迭代器为空;不是类没编译进去,是配置文件路径不对或内容格式错了。
- 配置文件必须放在
src/main/resources/META-INF/services/,文件名是接口全限定名(如com.example.Plugin) - 文件内每行只能有一个实现类名,不能有注释、空行、前后空格
- 实现类必须有无参构造器,否则
ServiceLoader实例化失败,抛ServiceConfigurationError - 加载时机是首次调用
iterator()时才真正触发类加载,不是load()调用就立刻初始化
为什么 ServiceLoader 不支持传参或依赖注入
它设计目标就是极简发现机制,所有实例都走无参构造 + newInstance(),没有生命周期管理,也不参与 Spring 容器。想传配置、注入 Logger 或其他 Bean?不行,得自己在实现类里重新查或者硬编码。
使用场景很窄:适合插件入口点统一、行为简单、状态无关的扩展点,比如日志后端、序列化器、JDBC 驱动注册(java.sql.Driver 就是典型用法)。
立即学习“Java免费学习笔记(深入)”;
- 不能直接用
@Autowired,Spring 的@Service也无效——ServiceLoader和 Spring 容器完全隔离 - 如果实现类需要外部资源,建议暴露
init(Map<String, Object> config)方法,由调用方主动传入 - 性能上无明显开销,但每次
iterator()都会重新加载并实例化,别在 hot path 上反复调用
多个 jar 包提供同一接口实现时怎么选
按 classpath 顺序加载,先出现的 jar 里的配置文件优先,后面同名文件会被忽略。没有“覆盖”或“合并”逻辑,也没有优先级配置项。
容易踩的坑是本地测试时看到 A 实现,上线后因为依赖顺序变化,实际加载了 B 实现,行为突变却毫无提示。
- 用
ClassLoader.getResources("META-INF/services/com.example.Plugin")可以看到所有匹配的资源路径,调试时值得加一行日志 - 不要指望通过
service.getClass().getPackage().getImplementationVersion()来做路由——版本信息不可靠,且ServiceLoader不读取它 - 如果必须控制顺序,要么调整 maven 依赖声明顺序,要么把所有实现打包进同一个 jar 并只留一份配置
ServiceLoader 和 Java 9+ Module System 冲突吗
冲突。模块系统默认不导出 META-INF/services/ 目录,即使你在 module-info.java 里写了 exports,该目录也不会被 ServiceLoader 扫到。
必须显式在模块声明中添加 uses com.example.Plugin;(对应接口),并在提供方模块中写 provides com.example.Plugin with com.example.impl.MyPlugin;。
- 没加
uses,ServiceLoader.load()返回空迭代器,连异常都不抛 - 模块路径下运行时,
ClassLoader.getSystemResources()可能根本找不到服务配置文件,这是模块封装的副作用 - 如果你还在用 classpath(非模块化部署),那模块声明可以不写,但一旦打成
jmod或用--module-path启动,就必须补全模块语句
真正难处理的是跨模块的服务发现和类加载器隔离问题——ServiceLoader 默认用当前线程上下文类加载器,而模块系统可能让不同模块用不同加载器,这时候哪怕配置对了,也可能因类加载器委托失败而加载不出类。










