Java SPI 实战:ServiceLoader 的正确打开方式(含类加载器坑)

这是偏门但很实用的一篇:插件化、SPI、可插拔架构都绕不开 ServiceLoader

先说结论

ServiceLoader 能跑起来的关键有三件事:

  1. META-INF/services 文件必须放在实现类所在的 jar
  2. 加载时要使用正确的 ClassLoader
  3. 同一个 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 加载失败,我通常按这个顺序排:

  1. META-INF/services 文件是否打进 jar
  2. 文件内容是否是实现类全限定名
  3. 实现类是否在 classpath
  4. ClassLoader 是否正确
  5. 是否存在多个 jar 提供同名实现

最后总结

ServiceLoader 很轻,但坑不少。

它真正适合的场景是:少量可插拔实现 + 简单配置

只要把注册文件、类加载器和实现选择这三件事做好,

SPI 就能很稳定地跑起来。

相关推荐
吃饱了得干活9 小时前
Spring Cloud Gateway 微服务网关:路由、断言、过滤器
java·spring cloud
lwx5728011 小时前
探秘InnoDB:搞懂它的内存、线程、磁盘与日志刷盘策略
java·后端
Flynt13 小时前
从Spring Boot 4.0升到4.1,我在Maven和gRPC上栽了跟头
java·spring boot·后端
plainGeekDev14 小时前
Activity 间传值 → Navigation 参数
android·java·kotlin
plainGeekDev14 小时前
onActivityResult → ActivityResult API
android·java·kotlin
Sunia14 小时前
《AgentX 专栏》10-生产部署:3台2C4G云服务器把企业级Agent真正跑起来的完整方案
java·架构
ZhengEnCi15 小时前
J7A-高级Java工程师面试三道灵魂拷问-深度广度与工程素养的终极检验
java·后端
狼爷1 天前
吃透 Java Function 接口,搞定 99% 的 Stream 场景
java·函数式编程
祎雪双十Gy2 天前
从 DataX 的配置加载说起:我用 FastJson2 做了一个轻量级动态配置管理库
java·后端
小锋java12342 天前
分享一套锋哥原创的SpringBoot4+Vue3宠物领养网站系统
java