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 就能很稳定地跑起来。

相关推荐
顽疲6 分钟前
从零用 Java 实现小红书 SpringBoot Vue UniApp(23)笔记话题多对多:一条笔记进多个话题池
java·vue.js·spring boot
老王爱玩车7 分钟前
动态内存管理
c语言·开发语言·学习·面试
I'm Jie13 分钟前
【开源】7 天,我用 Rust 复刻了 FinalShell!Rhost v1.0.0 发布
开发语言·rust·开源
木井巳19 分钟前
【BFS 解决拓扑排序】课程表
java·算法·leetcode·广度优先·宽度优先·推荐算法·图搜索算法
kimnoic30 分钟前
Python常用命令提示符使用方法详解
开发语言·python
Crazy________31 分钟前
05Linux内存管理核心原理与运维实战
java·开发语言
Nebula_g32 分钟前
JavaSE拓展:工具类Executors
java·开发语言·后端·spring·基础·javase
扶风ff39 分钟前
练题簿在线免费刷题:会员线下组卷、Word 试卷与成绩导入,课堂检测更方便
java·开发语言·算法·小程序·word
鱼宵40 分钟前
Spring AI 可观测与透明化:traceId 串起全链路,思考过程实时直播给用户
java·人工智能·spring·链路追踪·springai
程序员老陆43 分钟前
C++ 多线程通信方式全景:从共享内存到结构化协调
开发语言·c++