详细介绍:深入理解 SPI:从定义到 Spring Boot 实践

news/2025/10/1 16:12:59/文章来源:https://www.cnblogs.com/wzzkaifa/p/19122514

在之前剖析 Spring Boot 底层机制的文章中,多次提到SPI(Service Provider Interface,服务提供者接口) 是核心支撑技术之一 —— 无论是加载SpringApplicationRunListenerEnvironmentPostProcessor,还是实现自动配置的扩展,都依赖 SPI 机制。但 SPI 究竟是什么?它的底层原理如何?在 Spring 生态中又有哪些特殊实现?本文将从定义、原生 Java SPI、Spring SPI 扩展、实际应用四个维度,彻底讲透 SPI。

一、SPI 的本质:什么是 SPI?

SPI 是一种服务发现机制,核心思想是 “接口定义与实现分离”:

  • 服务方(如 Spring 框架) 定义统一的接口(如SpringApplicationRunListener);
  • 第三方(如开发者或框架扩展模块) 提供接口的具体实现类;
  • 通过配置文件声明实现类的位置,让系统在运行时自动扫描、加载并实例化这些实现,无需硬编码依赖。

简单来说,SPI 解决了 “如何让系统在不修改源码的情况下,灵活接入新的服务实现” 的问题 —— 这也是 Spring Boot “自动配置” 和 “可扩展” 的底层基础之一。

举个生活中的例子:

你买了一台打印机(相当于 “接口定义”),打印机厂商只提供了打印功能的标准接口;而不同品牌的墨盒(相当于 “实现类”)只要符合这个接口规范,就能插入打印机使用。你无需修改打印机本身,只需更换墨盒(实现类),就能实现不同的打印效果(如彩色、黑白)—— 这就是 SPI 的核心逻辑:接口统一,实现可替换

二、原生 Java SPI:基础原理与实现步骤

SPI 并非 Spring 独创,而是 Java 原生就支持的机制(JDK 1.6 + 引入),定义在java.util.ServiceLoader类中。我们先从原生 Java SPI 入手,理解其最基础的工作流程。

2.1 原生 Java SPI 的核心要素

原生 SPI 的实现必须满足三个约定,缺一不可:

  1. 接口定义:服务方提供一个公开的接口(如com.example.Logger);
  2. 实现类:第三方开发接口的实现(如com.example.Log4jLoggercom.example.Slf4jLogger);
  3. 配置文件:在classpath下的META-INF/services/目录中,创建一个以 “接口全限定名” 命名的文件(如com.example.Logger),文件内容为实现类的全限定名(每行一个)。

2.2 原生 Java SPI 的实现步骤(示例)

我们通过一个 “日志服务” 的例子,演示原生 SPI 的完整流程:

步骤 1:定义服务接口(服务方)

服务方(如框架)定义日志接口,声明核心能力:

// 服务接口:日志服务
package com.example;
public interface Logger {
void info(String message); // info级别日志
void error(String message); // error级别日志
}
步骤 2:开发实现类(第三方)

第三方开发者提供两种日志实现(Log4j 和 Slf4j):

// Log4j实现
package com.example;
public class Log4jLogger implements Logger {
@Override
public void info(String message) {
System.out.println("[Log4j] INFO: " + message);
}
@Override
public void error(String message) {
System.out.println("[Log4j] ERROR: " + message);
}
}
// Slf4j实现
package com.example;
public class Slf4jLogger implements Logger {
@Override
public void info(String message) {
System.out.println("[Slf4j] INFO: " + message);
}
@Override
public void error(String message) {
System.out.println("[Slf4j] ERROR: " + message);
}
}
步骤 3:编写 SPI 配置文件

在项目的src/main/resources/目录下,创建如下目录和文件:

  • 目录:META-INF/services/(固定路径,原生 SPI 必须在此目录);

  • 文件:com.example.Logger(文件名 = 接口全限定名);

  • 文件内容(实现类全限定名,每行一个):

    com.example.Log4jLogger
    com.example.Slf4jLogger
步骤 4:加载并使用实现类

通过 JDK 提供的ServiceLoader类,自动加载所有实现类并使用:

package com.example;
import java.util.ServiceLoader;
public class SpiDemo {
public static void main(String[] args) {
// 1. 获取ServiceLoader实例(指定接口类型)
ServiceLoader<Logger> serviceLoader = ServiceLoader.load(Logger.class);// 2. 遍历所有加载到的实现类(延迟加载,遍历到才创建实例)for (Logger logger : serviceLoader) {System.out.println("加载到日志实现:" + logger.getClass().getSimpleName());logger.info("SPI测试日志"); // 调用实现类的方法logger.error("SPI错误日志");}}}
运行结果
加载到日志实现:Log4jLogger
[Log4j] INFO: SPI测试日志
[Log4j] ERROR: SPI错误日志
加载到日志实现:Slf4jLogger
[Slf4j] INFO: SPI测试日志
[Slf4j] ERROR: SPI错误日志

2.3 原生 Java SPI 的底层原理

ServiceLoader的核心工作流程可拆解为 4 步(基于 JDK 源码):

  1. 定位配置文件:根据接口全限定名,在classpath下所有 JAR 包的META-INF/services/目录中,查找名为 “接口全限定名” 的文件;
  2. 读取实现类名:读取配置文件中的每一行,解析出实现类的全限定名(忽略注释和空行);
  3. 延迟实例化ServiceLoader是迭代器模式实现,遍历serviceLoader时,才通过类加载器(ClassLoader) 反射创建实现类实例(Class.forName(实现类名).newInstance());
  4. 缓存实例:创建后的实现类实例会被缓存到ServiceLoaderproviders集合中,避免重复反射创建。

2.4 原生 Java SPI 的局限性

原生 SPI 虽然实现了服务发现,但在实际开发中存在明显缺点,这也是 Spring 为何要自定义SpringFactoriesLoader的原因:

  1. 强制加载所有实现类ServiceLoader会加载配置文件中的所有实现类,无法按需加载(即使只需要其中一个,也会全部创建实例);
  2. 不支持依赖注入:只能通过无参构造器创建实例,无法注入其他依赖(如 Spring 中的EnvironmentSpringApplication);
  3. 线程不安全ServiceLoader的迭代器不支持多线程并发操作;
  4. 加载顺序不可控:实现类的加载顺序完全依赖配置文件中的顺序,无法通过代码干预。

三、Spring SPI:对原生 SPI 的增强与扩展

Spring 框架为了解决原生 SPI 的局限性,自定义了一套 SPI 实现 ——SpringFactoriesLoader,这也是 Spring Boot 中最核心的 SPI 机制(之前代码中的SpringApplicationRunListenerEnvironmentPostProcessor加载,都依赖它)。

3.1 Spring SPI 与原生 Java SPI 的核心差异

Spring SPI 在原生 SPI 的基础上做了三大关键改进,更贴合 Spring 生态的需求:

特性原生 Java SPISpring SPI(SpringFactoriesLoader)
配置文件路径META-INF/services/接口全限定名META-INF/spring.factories(固定文件名)
配置格式每行一个实现类全限定名接口全限定名=实现类1,实现类2,...(键值对)
加载方式强制加载所有实现类支持按需加载(指定接口 + 过滤实现类)
依赖注入仅支持无参构造器支持构造器参数注入(通过ArgumentResolver
集成 Spring 环境不支持(与 Spring 容器无关)支持(可注入EnvironmentSpringApplication等)

3.2 Spring SPI 的核心实现:SpringFactoriesLoader

SpringFactoriesLoader的核心逻辑与原生 SPI 类似,但在配置格式、加载灵活性、依赖注入上做了大幅增强。我们结合之前的 Spring Boot 代码示例(如C02_SpringBootStartupEventDemo),拆解其工作流程。

3.2.1 Spring SPI 的配置文件格式

Spring SPI 的配置文件固定为META-INF/spring.factories(文件名不可变),采用键值对格式,键是 “接口全限定名”,值是多个实现类全限定名(用逗号分隔)。例如:

# 配置SpringApplicationRunListener的实现类
org.springframework.boot.SpringApplicationRunListener=org.springframework.boot.context.event.EventPublishingRunListener
# 配置EnvironmentPostProcessor的实现类
org.springframework.boot.env.EnvironmentPostProcessor=org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor

这种格式的优势是:一个配置文件可以同时配置多个接口的实现类,无需为每个接口创建单独文件。

3.2.2 Spring SPI 的加载流程(结合代码示例)

C02_SpringBootStartupEventDemo中加载SpringApplicationRunListener为例,讲解SpringFactoriesLoader的完整流程:

// 代码片段:C02_SpringBootStartupEventDemo
SpringFactoriesLoader loader = SpringFactoriesLoader.forDefaultResourceLocation(); // 1. 创建loader
SpringFactoriesLoader.ArgumentResolver resolver = SpringFactoriesLoader.ArgumentResolver
.of(SpringApplication.class, springApp) // 2. 配置构造器参数(注入SpringApplication)
.andSupplied(String[].class, () -> args); // 注入命令行参数
Class<SpringApplicationRunListener> targetInterface = SpringApplicationRunListener.class;List<SpringApplicationRunListener> runListeners = loader.load(targetInterface, resolver); // 3. 加载实现类

上述代码的底层流程可拆解为 5 步:

  1. 创建SpringFactoriesLoader实例

    SpringFactoriesLoader.forDefaultResourceLocation()会创建一个默认的loader,其默认配置文件路径为META-INF/spring.factories(支持自定义路径,但 Spring Boot 中默认使用此路径)。

  2. 配置构造器参数注入

    原生 SPI 只能用无参构造器,而 Spring SPI 通过ArgumentResolver解决依赖注入问题。例如:

    • of(SpringApplication.class, springApp):表示当实现类的构造器需要SpringApplication类型参数时,注入springApp实例;
    • andSupplied(String[].class, () -> args):表示需要String[](命令行参数)时,通过 lambda 表达式提供args
  3. 扫描spring.factories文件

    loader会遍历classpath下所有 JAR 包的META-INF/spring.factories文件,读取键为org.springframework.boot.SpringApplicationRunListener的 value(即实现类全限定名,如EventPublishingRunListener)。

  4. 过滤与实例化实现类

    loader会根据targetInterfaceSpringApplicationRunListener.class)过滤出匹配的实现类,并通过以下步骤创建实例:

    • ClassLoader加载实现类的Class对象(Class.forName(实现类全限定名));
    • 分析实现类的构造器参数列表(如EventPublishingRunListener的构造器需要SpringApplicationString[]);
    • 通过ArgumentResolver找到对应的参数值,调用构造器创建实例(constructor.newInstance(参数1, 参数2))。
  5. 返回实现类列表

    实例化后的实现类会被收集到List中返回(如runListeners),后续代码可按需使用(如过滤出EventPublishingRunListener作为事件发布者)。

3.2.3 Spring SPI 的核心优势(结合代码场景)

在之前的 Spring Boot 代码示例中,Spring SPI 的优势体现得淋漓尽致:

  1. 按需加载

    例如C05_EnvironmentPostProcessorDemo中,加载EnvironmentPostProcessor时,可通过代码过滤出需要的实现类(如ConfigDataEnvironmentPostProcessor),无需加载所有实现;

  2. 依赖注入支持

    EventPublishingRunListener的构造器需要SpringApplicationString[]参数,ArgumentResolver自动注入,避免了硬编码依赖;

  3. 集成 Spring 环境

    加载的实现类可以直接使用 Spring 的核心组件(如EnvironmentApplicationContext),与 Spring 容器深度集成(原生 SPI 无法做到)。

四、SPI 在 Spring Boot 中的实际应用

理解 SPI 的最好方式是看它在 Spring Boot 中的具体用途 —— 几乎所有 “自动配置” 和 “扩展点” 都依赖 SPI 机制。结合之前的代码示例,我们梳理出 Spring Boot 中 SPI 的三大核心应用场景:

4.1 场景 1:加载启动生命周期监听器(SpringApplicationRunListener

C02_SpringBootStartupEventDemo所示,Spring Boot 通过SpringFactoriesLoader加载SpringApplicationRunListener的实现类(默认是EventPublishingRunListener),负责发布启动全生命周期事件(startingenvironmentPreparedready等)。

  • 接口org.springframework.boot.SpringApplicationRunListener
  • 实现类org.springframework.boot.context.event.EventPublishingRunListener
  • 配置META-INF/spring.factories中配置键值对;
  • 作用:作为启动事件的 “发布者”,串联整个启动流程。

4.2 场景 2:加载环境增强后处理器(EnvironmentPostProcessor

C05_EnvironmentPostProcessorDemo所示,Spring Boot 通过SpringFactoriesLoader加载EnvironmentPostProcessor的实现类,对StandardEnvironment进行增强(加载配置文件、生成随机值)。

4.3 场景 3:自动配置类加载(EnableAutoConfiguration

Spring Boot 的 “自动配置”(@EnableAutoConfiguration)本质也是 SPI 机制:

  • 接口org.springframework.boot.autoconfigure.EnableAutoConfiguration
  • 实现类:所有自动配置类(如DataSourceAutoConfigurationTomcatAutoConfiguration);
  • 配置spring-boot-autoconfigure.jarMETA-INF/spring.factories中,配置了数百个自动配置类;
  • 作用:启动时自动加载这些配置类,实现 “开箱即用”(如自动配置数据源、嵌入式 Tomcat)。

五、SPI 的核心价值:为什么需要 SPI?

无论是原生 Java SPI 还是 Spring SPI,其核心价值都可以概括为 “解耦” 与 “扩展”

  1. 解耦服务接口与实现

    服务方(如 Spring)只需定义接口,无需关心具体实现;第三方(如开发者)只需实现接口并配置,无需修改服务方代码。例如:你要为 Spring Boot 添加自定义Banner,只需实现Banner接口并配置到spring.factories,无需修改 Spring Boot 源码。

  2. 标准化扩展方式

    所有扩展都遵循统一的配置和加载规则(如META-INF/spring.factories),避免了 “各扩展模块自定义加载逻辑” 的混乱。例如:不同框架的EnvironmentPostProcessor实现,都通过同一套SpringFactoriesLoader加载,规则统一。

  3. 支持热插拔

    更换实现类时,只需修改配置文件(或替换 JAR 包),无需重新编译代码。例如:将日志实现从Log4j换成Slf4j,只需修改spring.factoriesLogger接口对应的实现类。

六、总结

SPI(Service Provider Interface)是一种服务发现机制,核心是 “接口定义与实现分离”,让系统在不修改源码的情况下灵活接入新的服务实现。

  • 原生 Java SPI:JDK 自带的基础实现,通过META-INF/services/配置,但存在 “强制加载所有实现、不支持依赖注入” 等局限性;
  • Spring SPI:Spring 自定义的SpringFactoriesLoader,通过META-INF/spring.factories键值对配置,支持按需加载、构造器参数注入,是 Spring Boot 自动配置的核心;
  • 实际应用:Spring Boot 中的SpringApplicationRunListenerEnvironmentPostProcessor、自动配置类,都依赖 SPI 机制加载,实现了 “开箱即用” 和 “灵活扩展”。

理解 SPI,不仅能帮你看透 Spring Boot 底层的 “自动配置黑盒”,更能在需要自定义扩展时(如开发中间件的 Spring Boot Starter),写出符合 Spring 生态规范的代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/924004.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

详细介绍:深入理解 SPI:从定义到 Spring Boot 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

RVC WebUI(Retrieval-based-Voice-Conversion-WebUI)配置 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

有哪些网站可以做h5莱芜十大首富

原标题&#xff1a;从安卓换到苹果到底是什么感受&#xff1f;最后一个让我彻底放弃了iPhone&#xff01;iPhone X是近几年来苹果最具创新力的iPhone&#xff0c;然而高昂的售价让很多消费者望而却步。随着今年苹果秋季新品发布会的临近&#xff0c;苹果即将发布价格更低廉&…

个人资料库网站怎么做资源下载类网站源码

Java 中的并发包指的是 java.util.concurrent(简称 JUC)包和其子包下的类和接口,它为 Java 的并发提供了各种功能支持,比如: 提供了线程池的创建类 ThreadPoolExecutor、Executors 等;提供了各种锁,如 Lock、ReentrantLock 等;提供了各种线程安全的数据结构,如 Concur…

第一次软工作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/202501SoftwareEngineering这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/202501SoftwareEngineering/homework/13546这个作业的目标 自我介绍,初…

Nginx核心配备详解:访问控制、用户认证与HTTPS部署

Nginx核心配备详解:访问控制、用户认证与HTTPS部署pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas"…

深入解析:5. Prompt 提示词

深入解析:5. Prompt 提示词2025-10-01 15:59 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important;…

基于Python+Vue开发的婚恋交友管理系统源码+运行步骤

项目简介该项目是基于Python+Vue开发的婚恋交友管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于…

南通医院网站建设方案网站 mysql数据库 字符

一直以来玩各种开发板&#xff0c;焊接水平太差始终是阻碍我买性价比高的板子的最大原因。淘宝上好多芯片搭载上肥猪流板子是不包排针焊接的。终于下定决心要克服这个困难。不过&#xff0c;只是会焊接排针在高手面前最好不要说自己会焊锡&#xff0c;这应该是两码事。 首先上…

详细介绍:Python 编辑器:PyCharm

详细介绍:Python 编辑器:PyCharm2025-10-01 15:45 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !impo…

2025 年搅拌机设备厂家 TOP 企业品牌推荐排行榜,盘点磁混凝系统 / 发酵罐 / 刮泥机 / 推进式 / 脱硫侧搅拌机公司推荐!

在当前工业生产领域,搅拌机作为关键设备,广泛应用于石化、化工、新能源、环保等多个行业,其性能与质量直接影响生产效率和产品品质。然而,随着市场需求的不断增长,搅拌机生产厂家数量持续增加,行业内产品质量参差…

福州市 2025 国庆集训 Day1 前三题题解

福州市 2025 国庆集训 Day1 前三题题解 别问为啥只有前三题,因为后面我不会…… Day1 题单 T1 旅行 传送门 注意到 \(P\) 非常小,所以可以考虑指数级别的做法。 考虑状压 dp。设 \(f_{s,u}\) 表示经过 \(P\) 内的点集…

Python常用数据类型详解:字符串、列表、字典全解析

在 Python 学习过程中,理解数据类型是基础也是关键。Python 的核心数据类型能够帮助开发者高效处理不同类型的数据,本篇文章将系统介绍三种最常用的数据类型:字符串(String)、列表(List)、字典(Dict),并结合…

【自学笔记】Redis 飞快入门

【自学笔记】Redis 飞快入门pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco",…

做网站工作室名字安徽二建标准

参考链接&#xff1a; Python中的私有变量 我们这里就直奔主题&#xff0c;不做基础铺垫&#xff0c;默认你有一些Python类的基础&#xff0c;大家在看这篇博客的时候&#xff0c;如果基础知识忘了&#xff0c;可以去菜鸟教程 从一个简单的类开始 class A(): #定义一…

强连通,Tarjan,缩点

在本文中,我们用 \(f(x,y)=1\) 来表示 \(x\) 可以到达点 \(y\),用 \(g(x,y)=1\) 表示 \(f(x,y)=1\) 且 \(f(y,x)=1\)。 I、强连通 对于图 \(U\) 上的任意两点 \(x\) 和 \(y\),如果有 \(g(x,y)=1\),那么称 \(x,y\) …

实用指南:K8s日志架构:Sidecar容器实践指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

做网站时量宽度的尺子工具thinkphp做的网站源码

目录结构 全局文件 1.app.json 文件 用来对微信小程序进行全局配置&#xff0c;决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。文件内容为一个 JSON 对象。 1.1 page用于指定小程序由哪些页面组成&#xff0c;每一项都对应一个页面的 路径&#xff08;含文…

企业网站建设综合实训心得wordpress安装系统

http://home.cnblogs.com/blog/转载于:https://www.cnblogs.com/yanyanhappy/archive/2012/09/07/2675050.html

彩票网站自己可以做吗wordpress加密页面访问

文章目录 一、 Zookeeper常用命令1. zk服务命令2. zk客户端命令 二、HBASE常见运维命令1. 集群启动关闭2. 扩容增加regionserver3. 下线regionserver ing 一、 Zookeeper常用命令 例如&#xff1a;ZOOKEEPER_HOME&#xff1a;/opt/zk/zookeeper 1. zk服务命令 1. 启动ZK服务…