serviceloader通过“接口契约+配置驱动+懒加载”实现开闭原则:新增实现只需添加类和meta-inf/services中一行配置,主流程代码无需修改;其依赖接口抽象、无参构造、类加载器机制及运行时发现,确保对扩展开放、对修改封闭。

ServiceLoader 怎么做到“对扩展开放、对修改封闭”
它不依赖 new 实例化,也不硬编码实现类名,而是靠 META-INF/services/ 下的配置文件声明实现类全限定名。只要接口定义稳定,新增一个实现类 + 一行配置,就能被自动发现和加载,主流程代码完全不动。
关键在于:接口是契约,实现是插件,ServiceLoader 是插槽 —— 插谁进谁,不改插槽本身。
- 接口必须是 public 的,且不能在模块(module-info.java)中被封禁导出
- 所有实现类必须有无参构造函数,否则
ServiceLoader实例化失败会静默跳过(不报错,但hasNext()返回 false) - 配置文件路径必须严格为
META-INF/services/接口全限定名,大小写、斜杠、点号都不能错 - 多个实现类写在同一文件里,每行一个,末尾空行或注释(# 开头)会被忽略
为什么 ServiceLoader.next() 可能返回 null 或抛异常
这不是 bug,是懒加载机制下的典型表现。调用 iterator().next() 时才真正触发类加载和实例化,此时若类不存在、构造器抛异常、静态块失败,ServiceLoader 会直接跳过该实现,并继续尝试下一个 —— 但如果你只写了一个实现且它出问题,next() 就会抛 NoSuchElementException。
- 常见错误:
ClassNotFoundException(jar 没打进去 / 类路径不对)、NoClassDefFoundError(依赖缺失)、ExceptionInInitializerError(static 块异常) - 调试建议:先用
iterator().hasNext()判断是否有可用实现,再用 try-catch 包住next() - 不要在实现类的 static 块里做重操作(如连数据库),它会在首次加载时阻塞整个 SPI 初始化
如何避免多个插件互相干扰或覆盖
ServiceLoader 默认使用当前线程上下文类加载器(Thread.currentThread().getContextClassLoader())查找资源。如果插件 jar 由不同 ClassLoader 加载(比如 Spring Boot 的 Fat Jar 或 OSGi 环境),就可能找不到 META-INF/services/ 文件。
- 显式指定 ClassLoader:
ServiceLoader.load(MyService.class, MyPluginClassLoader.class) - 确保所有插件 jar 都在同一个 classpath 层级,或统一由某个父 ClassLoader 加载
- 避免在
static字段里缓存ServiceLoader实例 —— 它不是线程安全的,且内部缓存与 ClassLoader 绑定 - 若需热插拔,不能依赖单例
ServiceLoader,每次都要新load()并遍历,否则旧实例仍持有已卸载类的引用
ServiceLoader 和 Spring 的 @SPI 注解/FactoryBean 有什么本质区别
Java 原生 ServiceLoader 是纯 JDK 机制,零依赖、无 IOC、无生命周期管理;Spring 的扩展方式(如 @ConditionalOnClass、自定义 FactoryBean 或 SpringFactoriesLoader)则深度绑定容器上下文,支持依赖注入、AOP、条件装配等。
-
ServiceLoader加载的是裸对象,字段不会被 Spring 注入,@Autowired 无效 -
spring.factories(Spring Boot)和META-INF/services/是两套独立配置,不能混用 - 混合使用时注意顺序:若用
@Component扫描插件实现类,就绕过了 SPI,也失去了“不改代码加插件”的意义 - 真正需要插件隔离和动态加载时,
ServiceLoader更轻量;需要统一管理生命周期和依赖时,应优先走 Spring 扩展点
实际项目中最容易被忽略的,是类加载器作用域和配置文件路径的精确匹配 —— 看似简单的一行配置,一旦落在错误的 jar 包、错误的 classpath 目录、或被模块系统(JPMS)拦截,就会彻底静默失效,连日志都不会输出。










