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

相关推荐
hakesashou1 分钟前
python如何保存img文件
开发语言·python
直奔標竿2 分钟前
Java开发者AI转型第十三课!知识库终局方案:Spring AI Vector Store架构演进与ETL全链路入库实战
java·人工智能·后端·spring
MarkHD3 分钟前
调度、监控与部署:Python自动化任务全栈实践
开发语言·python·自动化
XiYang-DING4 分钟前
【Java EE】阻塞队列(BlockingQueue)
java·java-ee
山甫aa5 分钟前
多叉树定义与遍历-----从零开始的数据结构
开发语言·c++·二叉树·多叉树
永远睡不够的入9 分钟前
C++11新特性(2):深入 C++ 参数传递黑盒:从引用折叠到完美转发,再到可变参数模板
开发语言·c++
AndreasEmil11 分钟前
基于多设计模式的抽奖系统 - 测试报告
java·selenium·设计模式·postman
无限进步_13 分钟前
【C++】寻找数组中出现次数超过一半的数字:三种解法深度剖析
开发语言·c++·git·算法·leetcode·github·visual studio
深邃-14 分钟前
【Web安全】-Kali,Linux配置(1):Kali网络配置,LinuxEnvConfig配置脚本,APT源的讲解,Kali设置中文
linux·运维·开发语言·网络·安全·web安全·网络安全
Hello World . .15 分钟前
Linux驱动编程:内核同步的艺术-从互斥到底半部
linux·开发语言·数据库