一、前言:到底什么是SPI?
SPI全称:Service Provider Interface,服务提供者接口
核心思想就8个字:接口与实现完全解耦。
不用修改框架原有源码,只需要提前写好接口,后续新增功能实现类、改个配置文件,框架就能自动发现并加载新功能,这就是SPI的核心价值。
主流技术栈底层全都在用SPI:
- JDBC驱动加载
- Dubbo分布式扩展组件
- SpringBoot全自动装配机制
- 各类中间件自定义插件拓展
目前行业主流就两套SPI方案:Java原生SPI + Spring升级版SPI,下面从零拆解,全程无晦涩难懂的废话。
二、Java原生SPI:JDK自带,简陋但必学
1、核心强制规则
Java SPI是JDK内置原生能力,不用额外导依赖,但有三条死规则,写错直接失效:
- 固定文件夹:必须放在 resources/META-INF/services/ 目录下,目录不能改名、不能换位置
- 文件名硬性要求:文件名必须和接口完整全类名一模一样,多一个点、少一个字母都找不到
- 文件内容规范:里面填写当前接口所有实现类的完整全类名,一行写一个,换行分隔
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底层怎么精准找到对应文件?(解答核心疑问)
很多人疑惑:有几十个接口、几十个配置文件,它会不会找乱、会不会全加载?
答案:绝对不会乱,也不会加载无关文件!
底层执行逻辑全自动精准匹配:
- 你代码里传入 VideoPlaySPI.class
- 底层自动提取该接口的完整全类名
- 自动拼接固定前缀路径:META-INF/services/ + 接口全类名
- 只精准读取这一个同名文件,其他所有接口的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、中间件、企业级核心框架必备 |
五、底层核心原理一句话总结
-
Java SPI:靠「固定文件夹+文件名=接口全类名」精准匹配,强制全量加载,简单但短板明显,适合入门学习
-
Spring SPI:沿用SPI解耦核心思想,统一配置、优化加载逻辑,新增排序+缓存+按需加载能力,是SpringBoot自动配置的底层基石
六、总结
SPI是服务提供发现机制,核心实现接口与实现类解耦,提升框架扩展性。Java原生SPI配置分散、只能全量加载、无法排序;Spring SPI优化为统一spring.factories配置文件,支持按需加载、自定义排序、自带缓存,性能更强,SpringBoot自动装配底层依赖Spring SPI实现。