Java SPI vs Spring SPI

一、前言:到底什么是SPI?

SPI全称:Service Provider Interface,服务提供者接口

核心思想就8个字:接口与实现完全解耦。

不用修改框架原有源码,只需要提前写好接口,后续新增功能实现类、改个配置文件,框架就能自动发现并加载新功能,这就是SPI的核心价值。

主流技术栈底层全都在用SPI:

  • JDBC驱动加载
  • Dubbo分布式扩展组件
  • SpringBoot全自动装配机制
  • 各类中间件自定义插件拓展

目前行业主流就两套SPI方案:Java原生SPI + Spring升级版SPI,下面从零拆解,全程无晦涩难懂的废话。

二、Java原生SPI:JDK自带,简陋但必学

1、核心强制规则

Java SPI是JDK内置原生能力,不用额外导依赖,但有三条死规则,写错直接失效:

  1. 固定文件夹:必须放在 resources/META-INF/services/ 目录下,目录不能改名、不能换位置
  2. 文件名硬性要求:文件名必须和接口完整全类名一模一样,多一个点、少一个字母都找不到
  3. 文件内容规范:里面填写当前接口所有实现类的完整全类名,一行写一个,换行分隔

2、完整实战代码演示

第一步:定义通用业务接口

复制代码
// 统一支付渠道标准拓展接口
public interface PayChannelSPI {
    // 通用支付下单核心方法
    void unifiedPay();
}

第二步:编写多个业务实现类

复制代码
// 实现类1:微信支付渠道专属实现
public class WechatPayImpl implements PayChannelSPI {
    @Override
    public void unifiedPay() {
        System.out.println("🧧 调用微信支付SDK,发起微信快捷下单支付");
    }
}

// 实现类2:支付宝支付渠道专属实现
public class AliPayImpl implements PayChannelSPI {
    @Override
    public void unifiedPay() {
        System.out.println("💳 调用支付宝开放平台,发起支付宝当面付扣款");
    }
}

第三步:创建核心SPI配置文件(关键步骤)

在项目资源目录下,严格创建对应路径及文件:
resources/META-INF/services/com.pay.spi.PayChannelSPI

⚠️ 重点:文件名 = 接口全类名,绝对不能乱写!

文件内部写入两个实现类的全类名:

复制代码
com.pay.spi.WechatPayImpl
com.pay.spi.AliPayImpl

第四步:编写测试类,运行原生SPI加载逻辑

复制代码
import java.util.ServiceLoader;

public class JavaPaySPITest {
    public static void main(String[] args) {
        // JDK原生API,绑定支付渠道拓展接口
        ServiceLoader<PayChannelSPI> serviceLoader = ServiceLoader.load(PayChannelSPI.class);
        
        // 自动加载所有支付实现,批量执行下单逻辑
        for (PayChannelSPI payChannelSPI : serviceLoader) {
            payChannelSPI.unifiedPay();
        }
    }
}

运行结果

复制代码
🧧 调用微信支付SDK,发起微信快捷下单支付
💳 调用支付宝开放平台,发起支付宝当面付扣款

3、Java SPI底层怎么精准找到对应文件?(解答核心疑问)

很多人疑惑:有几十个接口、几十个配置文件,它会不会找乱、会不会全加载?

答案:绝对不会乱,也不会加载无关文件!

底层执行逻辑全自动精准匹配:

  1. 你代码里传入 VideoPlaySPI.class
  2. 底层自动提取该接口的完整全类名
  3. 自动拼接固定前缀路径:META-INF/services/ + 接口全类名
  4. 只精准读取这一个同名文件,其他所有接口的SPI文件,一概不扫描、不加载、不读取

4、Java原生SPI致命缺点

原生SPI能用,但企业开发没人直接用,全是硬伤:

  • ❌ 不能按需挑选实现类:配置文件里写了几个实现类,就必须全部加载、全部实例化,想单独只用某一个实现,完全做不到
  • ❌ 无法自定义执行顺序:只能按文件里书写顺序加载,不能手动调整优先级
  • ❌ 配置文件极度分散混乱:一个接口对应一个独立配置文件,接口多了之后,services目录下全是零散文件,后期维护极其麻烦
  • ❌ 加载性能差:采用懒加载迭代器遍历,频繁反射创建对象,没有缓存机制

三、Spring SPI:针对性优化,企业主流方案

Spring没有推翻Java原生SPI的核心思想,只是全套升级优化,完美解决原生SPI所有痛点,SpringBoot自动配置底层全靠它支撑。

1、Spring SPI核心优化亮点

  • ✅ 统一配置文件:所有接口、所有实现类,全部写在同一个 spring.factories 文件里,整洁好维护
  • ✅ 支持按需指定实现:想加载哪个就加载哪个,不用全部强制加载
  • ✅ 支持自定义排序优先级:配合注解就能调整实现类执行先后顺序
  • ✅ 自带缓存机制:加载过的实现类直接缓存,提升底层运行性能

2、Spring SPI完整实战案例

第一步:自定义数据库拓展接口(通用标准)

复制代码
// 全平台消息推送统一拓展接口
public interface MessagePushSPI {
    // 统一消息批量推送核心方法
    void sendPushMessage();
}

第二步:编写多数据库场景实现类

复制代码
// 实现1:APP站内弹窗消息推送实现
public class AppPushImpl implements MessagePushSPI {
    @Override
    public void sendPushMessage() {
        System.out.println("📱 成功推送APP弹窗通知,全员触达在线用户");
    }
}

// 实现2:短信验证码营销消息推送实现
public class SmsPushImpl implements MessagePushSPI {
    @Override
    public void sendPushMessage() {
        System.out.println("📨 成功调用短信网关,批量下发业务通知短信");
    }
}

第三步:创建Spring专属配置文件

路径同样放在:resources/META-INF/ 目录下

新建固定文件名:spring.factories(全局就这一个配置文件)

文件内标准key-value格式编写:

复制代码
# key = 消息推送接口完整全类名
# value = 多个推送渠道实现类,逗号隔开
com.message.spi.MessagePushSPI=com.message.spi.AppPushImpl,com.message.spi.SmsPushImpl

# ✅ 补充重点:项目有【多个接口、多组实现类】时,统一往下续写即可
# 核心规则:不同接口换行分开写,key单独绑定对应自己的实现类,互不干扰
# 额外示例:新增支付渠道接口、文件上传接口,全部写在这同一个文件里
# com.pay.spi.PayChannelSPI=com.pay.spi.WechatPayImpl,com.pay.spi.AliPayImpl
# com.upload.spi.FileUploadSPI=com.upload.spi.OssUploadImpl,com.upload.spi.LocalUploadImpl

第四步:Spring工具类直接加载测试

复制代码
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;

public class SpringMessageSPITest {
    public static void main(String[] args) {
        // Spring专用加载工具,加载所有消息推送实现类
        List<MessagePushSPI> spiList = SpringFactoriesLoader.loadFactories(
                MessagePushSPI.class,
                Thread.currentThread().getContextClassLoader()
        );
        // 遍历执行全渠道消息推送
        for (MessagePushSPI pushSPI : spiList) {
            pushSPI.sendPushMessage();
        }
    }
}

运行效果

复制代码
📱 成功推送APP弹窗通知,全员触达在线用户
📨 成功调用短信网关,批量下发业务通知短信

四、核心重点对比(面试直接背,满分答案)

对比维度 Java 原生 SPI Spring SPI(spring.factories)
配置文件数量 一个接口对应一个独立文件,零散混乱 全局统一仅一个 spring.factories 文件,整洁易维护
加载方式 只能一次性加载全部实现类,无法按需挑选 支持按需加载单个实现,也可批量加载,灵活度高
执行顺序 固定按文件书写顺序,无法手动调整排序 支持 @Order 注解自定义优先级,灵活控制执行顺序
底层工具类 JDK原生 ServiceLoader Spring专属 SpringFactoriesLoader
性能体验 无缓存,频繁反射,性能较差 自带本地缓存机制,反复加载性能更强
适用场景 简单小型工具项目,极少单独使用 SpringBoot、中间件、企业级核心框架必备

五、底层核心原理一句话总结

  1. Java SPI:靠「固定文件夹+文件名=接口全类名」精准匹配,强制全量加载,简单但短板明显,适合入门学习

  2. Spring SPI:沿用SPI解耦核心思想,统一配置、优化加载逻辑,新增排序+缓存+按需加载能力,是SpringBoot自动配置的底层基石

六、总结

SPI是服务提供发现机制,核心实现接口与实现类解耦,提升框架扩展性。Java原生SPI配置分散、只能全量加载、无法排序;Spring SPI优化为统一spring.factories配置文件,支持按需加载、自定义排序、自带缓存,性能更强,SpringBoot自动装配底层依赖Spring SPI实现。

相关推荐
希望永不加班3 小时前
Java数据类型陷阱:int和Integer的7个关键区别
java·开发语言
boonya3 小时前
Idea CC GUI插件如何通过 CC Switch 工具将 Claude Code 的后端配置为 DeepSeek 的 v4-pro 模型?
java·ide·intellij-idea
花千树-0103 小时前
从业务接口到 MCP Tool:多语言工程化实践指南(Python / TypeScript / Java)
java·python·rpc·typescript·api·mcp
qcx233 小时前
深度解析Deepseek V4:1M 上下文不是军备竞赛,是养 Agent 的人才知道的痛
java·开发语言
小则又沐风a3 小时前
基础的开发工具(2)---Linux
java·linux·前端
晨非辰3 小时前
吃透C++两大默认成员函数:const成员函数、 & 取地址运算符重载
java·大数据·开发语言·c++·人工智能·后端·面试
梵得儿SHI3 小时前
(第三篇)Spring AI 架构设计与优化:容器化与云原生部署,基于 K8s 的 AI 应用全生命周期管理
java·ci/cd·docker·云原生·kubernetes·容器化·spring ai
普修罗双战士3 小时前
项目设计-文章系统发布文章完整前后端设计
java·数据库·vue.js·spring boot·git·intellij-idea
程序员老邢3 小时前
《人生底稿・番外篇12》37 岁程序员的工位双生 —— 旧主机的 “开发 + 摸鱼” 效率分区
java·程序员日常·人生底稿番外·中年码农·工作效率分区