serviceloader 是 java 原生 spi 的唯一标准实现,通过 meta-inf/services/ 下按接口全限定名命名的配置文件加载 public 无参构造器的实现类,支持延迟加载但不提供依赖注入或生命周期管理。

ServiceLoader 是 Java 原生 SPI 的唯一标准实现
Java 的 ServiceLoader 就是 JDK 提供的、开箱即用的 SPI(Service Provider Interface)机制落地方式。它不依赖第三方库,但必须严格遵循约定路径和加载规则,否则会静默失败——这是绝大多数人卡住的第一步。
核心逻辑很简单:接口定义在 API 模块,具体实现放在独立模块里;运行时 ServiceLoader.load(YourInterface.class) 会扫描 META-INF/services/ 下对应全限定名的文件,按行读取实现类名并反射加载。
- 接口必须是 public,且不能是内部类或泛型类(泛型擦除后无法匹配)
- 服务配置文件路径必须是
META-INF/services/com.example.YourInterface(注意是接口全限定名,无 .class 后缀) - 文件内容每行一个实现类全限定名,末尾不能有空格或注释(# 不被识别)
- 实现类必须有 public 无参构造器,否则
InstantiationException
load() 返回的 Iterator 是延迟加载 + 单次遍历的
ServiceLoader.load() 看似返回 Iterator<t></t>,但它不是一次性加载所有实现,而是每次调用 next() 才尝试加载下一个类。一旦抛出异常(比如 NoClassDefFoundError 或 IllegalAccessException),该迭代器就失效,后续调用 next() 会直接抛 IllegalStateException。
- 别用
forEachRemaining或流式处理,容易漏掉异常信息 - 务必用 try-catch 包裹单次
next()调用,捕获ServiceConfigurationError(SPI 专属异常)和Throwable - 同一个
ServiceLoader实例不能重复遍历,需重新load() - 若需缓存全部可用实例,应主动收集到
List中,并在收集过程中处理每个加载异常
ClassLoader 传错会导致“找不到服务”却无报错
ServiceLoader.load(YourInterface.class) 默认使用当前线程上下文类加载器(Thread.currentThread().getContextClassLoader())。如果服务实现类不在该类加载器的路径下(比如 Web 应用中实现类在 WEB-INF/lib,而调用方在系统类路径),load() 会返回空迭代器,且不报任何错误。
- 显式传入类加载器更可靠:
ServiceLoader.load(YourInterface.class, YourInterface.class.getClassLoader()) - 优先用接口自身的类加载器(
YourInterface.class.getClassLoader()),它大概率能看见实现类 - 避免混用不同模块的 ClassLoader,尤其在 OSGi、Spring Boot DevTools 或热部署场景下
- 可通过
serviceLoader.iterator().hasNext()快速验证是否真有服务被发现
ServiceLoader 无法自动注入或管理生命周期
ServiceLoader 只负责“发现+实例化”,不提供依赖注入、单例控制、销毁回调或优先级排序。如果你需要按权重选实现、或让某个实现只初始化一次、或在 Spring 环境里复用 @Autowired,它做不到。
- 自己加缓存:用
ConcurrentHashMap存已实例化的服务对象,避免重复反射创建 - 自己加标记接口:比如定义
Ordered接口,让实现类提供getOrder(),再手动排序 - 与 Spring 集成时,不要直接在
@Bean方法里 returnServiceLoader.load(...).findFirst(),因为 Spring 容器启动早于某些插件模块的类加载;改用ObjectProvider<yourinterface></yourinterface>延迟获取 - 别指望它替代 Dubbo 的
@SPI或 Apache Commons Discovery——那些是增强版,JDK 原生的就这能力
真正麻烦的是跨模块类加载隔离和异常静默——这两点不提前验证,上线后服务“突然消失”,日志里连个痕迹都没有。










