ServiceLoader 是 JDK 内置的 SPI 官方入口,需满足三要素:接口与实现类在 classpath 中、META-INF/services/下以接口全限定名命名文件、文件内容为实现类全限定名(UTF-8 编码,无注释);其加载依赖上下文类加载器,易因类加载器隔离、构造异常、资源丢失等静默失败。

ServiceLoader 是 Java SPI 机制的唯一官方入口,它不是“可选方案”,而是 JDK 内置的、约定驱动的服务发现机制——接口定义方不写实现,也不硬编码类名,只靠 META-INF/services/接口全限定名 文件触发运行时加载。
怎么让 ServiceLoader 找到你的实现类
关键不是“写个接口+实现类”就完事,而是必须满足三要素:
- 接口和实现类必须在 classpath 中(比如打包进 jar 或放在
resources下) - 在
META-INF/services/目录下创建一个文件,文件名必须是接口的全限定名(例如com.example.Search) - 该文件内容必须是实现类的
全限定名,一行一个,不能有空格或注释(#不被识别,会被当作类名一部分导致ClassNotFoundException)
为什么 ServiceLoader.iterator() 有时不抛错却没加载到类
常见静默失败原因:
-
ServiceLoader.load(Interface.class)使用的是Thread.currentThread().getContextClassLoader(),如果你在 Web 容器、Spring Boot 或模块化环境(Java 9+)中运行,这个 CL 很可能看不到你放配置文件的模块 —— 改用ServiceLoader.load(Interface.class, YourClass.class.getClassLoader())更可靠 - 实现类构造器抛异常(比如依赖未注入),
ServiceLoader会吞掉异常并跳过该实现,iterator.hasNext()仍返回true,但next()会直接抛出ServiceConfigurationError - 文件编码不是 UTF-8(尤其 Windows 记事本保存默认是 GBK),会导致类名读取乱码,加载失败
和 Spring 的 @SPI 或 Dubbo 的 @SPI 不是一回事
JDK 原生 ServiceLoader 是最简模型,没有分组、优先级、条件加载、懒初始化等能力:
- 它不支持按参数动态选择实现(比如根据
type=xml加载XmlSerializer) - 它不缓存实例,每次
next()都 new 一次对象 —— 如果实现类有状态或开销大,得自己加单例包装 - 它不支持 fallback 机制:如果配置了 3 个实现,其中 1 个加载失败,其余 2 个仍可用;但不会告诉你哪几个挂了
ServiceLoaderloader = ServiceLoader.load(Search.class); for (Search s : loader) { // 每次循环都 new 一个新实例,不是复用 s.searchDoc("test"); }
真正上线项目里最容易被忽略的一点
你写的 META-INF/services/com.example.Xxx 文件,在 Maven 多模块构建中极易丢失 —— 尤其当实现类在子模块,而测试代码在父模块启动时,resources 可能没被打包进最终 jar,或者被 shade 插件误删。务必验证最终 jar 包里是否存在该路径和文件,别只信 IDE 的 “Run” 按钮。
立即学习“Java免费学习笔记(深入)”;










