serviceloader 解耦关键在“怎么放、怎么配、怎么调”:配置文件须置于 classpath 根目录下 meta-inf/services/,实现类需 public 且含无参构造器,加载需对齐上下文类加载器,多实现需手动过滤。

ServiceLoader 实现业务解耦,关键不在“能不能用”,而在于“怎么放、怎么配、怎么调”——三处细节错一点,服务就加载失败,还查不出原因。它不负责依赖注入、不管理生命周期、也不做条件筛选,只干一件事:按约定路径找配置、按约定格式读类名、按约定规则实例化。用对了,模块之间真能像搭积木一样换插件;用错了,连 NoClassDefFoundError 都报得莫名其妙。
配置文件必须严格落在 classpath 根目录下
META-INF/services/ 不是随便放的资源路径,而是 ServiceLoader 硬编码扫描的位置。常见错误包括:
- 把配置文件放进
src/main/java/META-INF/services/—— 编译后不会进 classpath,直接找不到 - 误写成
resources/META-INF/services/com.example.MyService(多了一层 resources 文件夹)—— 实际应为src/main/resources/META-INF/services/com.example.MyService - 在多模块 Maven 项目中,只在实现模块里配了文件,但没把该模块打成 jar 并引入调用方 classpath —— 调用方 classpath 里压根没有这个文件
- Linux 或容器环境因大小写敏感,把接口名写成
com.example.myservice(小写)而接口实际是com.example.MyService
实现类必须满足基础契约
ServiceLoader 不校验、不提示、不兜底,只按最简规则执行。以下任一不满足,iterator().next() 就会抛异常:
- 实现类必须是
public的,且声明在顶层类中(不能是 private 内部类) - 必须有 public 无参构造器 —— 带参数、默认包访问权限、或被 Lombok
@RequiredArgsConstructor替换都不行 - 类名必须与配置文件中写的全限定名完全一致,包括包路径和大小写
- Java 9+ 模块化项目中,提供方模块的
module-info.java必须显式声明:provides com.example.MyService with com.example.MyServiceImpl;
加载时机与线程上下文类加载器要对齐
ServiceLoader 默认使用 Thread.currentThread().getContextClassLoader() 查找资源。Web 应用或嵌入式容器中极易出问题:
- Tomcat 中,Web 应用类加载器未被设为当前线程上下文类加载器时,
load()会去 Bootstrap 或 System ClassLoader 里找,自然找不到自己 jar 里的配置 - 异步线程(如
CompletableFuture或线程池任务)中,上下文类加载器可能被重置为 null 或父加载器 - 解决方法:显式传入正确的类加载器,例如
ServiceLoader.load(MyService.class, MyService.class.getClassLoader()) - 避免缓存
ServiceLoader实例:它本身不持有状态,每次load()是轻量操作;缓存后若未遍历 iterator,等于什么都没做
多实现共存时需主动控制选择逻辑
ServiceLoader 天然支持多个实现并存,但它不提供优先级、环境匹配、版本路由等能力,这些得由你补全:
- 遍历时不要直接
next(),先捕获NoClassDefFoundError或InstantiationException,跳过不可用实现 - 可结合系统属性、配置中心开关、Spring Profile 等做运行时过滤,例如只加载
env=prod下标记的实现 - 若需唯一主实现,建议加一个
@Primary类似语义的标记接口或注解,在遍历中识别并返回首个匹配项 - 不要依赖加载顺序:多个 jar 提供同一接口时,URL 列表顺序由类加载器决定,不可预测
它不是万能框架,而是 JVM 层级的轻量协议。真正解耦的价值,来自开发阶段的约束意识——接口定义在基础模块、实现散落在业务模块、配置文件随实现一起发布、调用方只依赖接口和 ServiceLoader.load()。只要这几条链路不断,新增支付渠道、日志输出方式、序列化策略,就真的只是加个类、配个文件、扔进 classpath。










