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

相关推荐
曹牧10 分钟前
Jackson 反序列化字段名不匹配
java
名字还没想好☜24 分钟前
Spring Boot 优雅停机实战:等在途请求处理完再退出,配合 K8s preStop 别丢请求
java·后端·spring
摇滚侠29 分钟前
《Spring Boot 3:高级与架构设计》第 1 章 Bean 与 BeanDefinition 个人理解 2
java·spring boot·笔记·后端
qq_2695067531 分钟前
第30课-请求与响应
java
qq_269506751 小时前
第28课-AJAX与前端框架
java
程序员清风1 小时前
Java 后端如何接入大语言模型
java·spring boot·架构·aigc
xieliyu.1 小时前
JVM 垃圾回收机制详解:从标记过程、回收算法到垃圾收集器
java·jvm·笔记·java-ee
JavacKaka1 小时前
Redis大Key导致生产事故复盘博客-2026-09-18
java
辛苦才能1 小时前
C++多态原理:虚函数表的内存布局与动态绑定的汇编真相
开发语言·c++
铲灰1 小时前
做个总结而已
开发语言