这是偏门但很实用的一篇:插件化、SPI、可插拔架构都绕不开
ServiceLoader。
先说结论
ServiceLoader 能跑起来的关键有三件事:
META-INF/services文件必须放在实现类所在的 jar- 加载时要使用正确的 ClassLoader
- 同一个 SPI 最好只有一个默认实现,否则顺序不可控
一、最小可用的 SPI 示例
1) 定义接口(SPI)
java
public interface PayStrategy {
String name();
void pay(int amount);
}
2) 实现类
java
public class WechatPay implements PayStrategy {
@Override
public String name() { return "wechat"; }
@Override
public void pay(int amount) { /* ... */ }
}
3) 注册文件
路径:META-INF/services/全限定接口名
文件内容:
com.example.pay.WechatPay
4) 加载
java
ServiceLoader<PayStrategy> loader = ServiceLoader.load(PayStrategy.class);
for (PayStrategy s : loader) {
System.out.println(s.name());
}
二、最容易踩的 5 个坑
1) 注册文件放错 jar
接口在 api.jar,实现类在 impl.jar,
META-INF/services/... 必须跟着 实现类 的 jar 走。
2) 类加载器不对
在容器/插件体系里经常出现:
ServiceLoader.load() 找不到实现类。
解决方式:明确传入 ClassLoader。
java
ClassLoader cl = Thread.currentThread().getContextClassLoader();
ServiceLoader<PayStrategy> loader = ServiceLoader.load(PayStrategy.class, cl);
3) 多实现顺序不可控
ServiceLoader 的顺序与 jar 扫描顺序相关,不能依赖 。
如果有默认实现,建议按 name() 显式选择。
4) 实现类没有无参构造
ServiceLoader 通过反射创建实例,必须要无参构造。
5) 打包时资源被过滤
某些构建工具会把 META-INF/services 过滤掉,
最终运行时找不到任何实现。
三、一个"够用"的选择器写法
java
public class PayStrategyFactory {
private static final Map<String, PayStrategy> CACHE = new HashMap<>();
static {
ServiceLoader<PayStrategy> loader =
ServiceLoader.load(PayStrategy.class);
for (PayStrategy s : loader) {
CACHE.put(s.name(), s);
}
}
public static PayStrategy get(String name) {
return CACHE.get(name);
}
}
这样你就能按业务类型拿到对应实现,而不是靠加载顺序"碰运气"。
四、排查清单(实战版)
如果 SPI 加载失败,我通常按这个顺序排:
META-INF/services文件是否打进 jar- 文件内容是否是实现类全限定名
- 实现类是否在 classpath
- ClassLoader 是否正确
- 是否存在多个 jar 提供同名实现
最后总结
ServiceLoader 很轻,但坑不少。
它真正适合的场景是:少量可插拔实现 + 简单配置。
只要把注册文件、类加载器和实现选择这三件事做好,
SPI 就能很稳定地跑起来。