SPI 的基本原理和使用场景(服务发现)
SPI(Service Provider Interface,服务提供者接口) 是一种服务发现机制,允许框架或库在运行时动态加载第三方实现,而无需在代码中硬编码具体的实现类。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
- 模块化:服务提供者的实现可以分布在不同的模块或 JAR 包中,SPI 机制使得这些实现能够轻松被发现和加载。
- 扩展性:应用程序可以通过添加新的服务提供者来扩展现有功能,而无需修改客户端代码。
- 解耦:服务接口和具体实现之间是完全解耦的,调用方只需要知道接口,而无需关心具体的实现类。
- 动态性:通过 SPI 机制,服务实现可以在运行时动态加载,这使得应用程序可以灵活地应对变化或增加新功能。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
SPI 核心思想
-
定义接口(或抽象类):由 API 提供方(如 JDK 或框架)定义。
-
实现接口:由第三方(服务提供者)提供具体实现。
-
自动发现并加载实现:运行时通过约定的配置文件自动发现并加载实现类,无需硬编码。
ServiceLoader.load(Class)会扫描 classpath 下所有 JAR 或目录中的META-INF/services/接口名文件。- 读取文件内容,通过反射实例化每一行指定的类。
- 返回一个
Iterable对象,可遍历所有实现。
- 实现类必须有无参 public 构造函数。
- 配置文件路径和命名必须严格匹配。
- 默认使用当前线程的上下文类加载器(ContextClassLoader),也可手动指定。
三者特点:
| 特性 | Java 原生 SPI | Dubbo SPI | Spring Factories |
|---|---|---|---|
| 配置位置 | META-INF/services/ | META-INF/dubbo/ | META-INF/spring.factories |
| 按名称获取 | 不支持 | 支持 | 不支持(可通过 key 分组) |
| 延迟加载 | 不支持(全加载) | 支持 | 支持 |
| IOC 支持 | 不支持 | 支持 | 支持(在 Spring 上下文中) |
| 使用场景 | JDK 标准扩展 | Dubbo 插件体系 | Spring Boot 自动配置 |
Java SPI
java SPI 通过 java.util.ServiceLoder 实现。实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
优点:
解耦:API 与实现分离,便于扩展。
插件化:第三方可轻松提供实现,无需修改主程序。
标准机制:JDK 原生支持,无需额外依赖。
缺点:
一次性加载所有实现:即使只用一个,也会全部实例化(性能问题)。
不能按需加载,需要遍历所有的实现并实例化,在循环中才能找到需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
无命名/优先级控制:无法按名称获取特定实现,也无法排序。
只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类
异常处理弱:某个实现类加载失败可能导致整个加载失败。
不支持依赖注入:无法传参或注入 Spring Bean。
线程不安全:多个并发多线程使用 ServiceLoader 类的实例是线程不安全的。
实现原理
-
ServiceLoader.load(Class)会扫描 classpath 下所有 JAR 或目录中的META-INF/services/接口名文件; -
读取文件内容,通过
InputStream流将文件具体实现类的全类名读取出来; -
根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是就通过反射的机制构造对应的实例对象;
通过反射方法
Class.forName()加载类对象,并用newInstance()将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。 -
将构造出来的实例对象添加到
Providers的列表中; -
返回一个
Iterable对象,可遍历所有实现。
解决第三方类加载的机制在
ClassLoader cl = Thread.currentThread().getContextClassLoader();中,cl就是线程上下文类加载器(Thread Context ClassLoader)。这是每个线程持有的类加载器,JDK 的设计允许应用程序或容器(如 Web 应用服务器)设置这个类加载器,以便核心类库能够通过它来加载应用程序类。线程上下文类加载器默认情况下是应用程序类加载器(Application ClassLoader),它负责加载 classpath 上的类。当核心库需要加载应用程序提供的类时,它可以使用线程上下文类加载器来完成。这样,即使是由引导类加载器加载的核心库代码,也能够加载并使用由应用程序类加载器加载的类。
Demo 示例
定义服务接口,提供具体实现
1 | public interface IShout { |
创建配置文件:建立目录 src/main/resources/META-INF/services, 新增一个以接口命名的文件 (org.foo.demo.IShout 需包含包名),内容是该接口的所有实现类全限定名(每行一个类)。
1 | org.foo.demo.animal.Dog |
加载服务,获取实例:
1 | // 使用 ServiceLoader 来加载配置文件中指定的实现 |
经典案例
-
JDBC 驱动加载
MySQL 驱动 JAR 包中包含:
1
2
3META-INF/services/java.sql.Driver
内容:
com.mysql.cj.jdbc.Driver应用启动时,
DriverManager通过ServiceLoader自动加载所有数据库驱动。 -
SLF4J 日志绑定
SLF4J 通过 SPI 机制绑定具体的日志实现(如 Logback、Log4j)。
SpringBoot SPI
Spring Boot使用 SPI 机制来实现其自动配置和插件化功能。
自动配置依赖 spring.factories 文件,该文件以 SPI 的方式声明了自动配置类,在服务运行时就能根据上下文环境自动装配合适的 Bean 和配置。
模块化扩展:不同模块或库可通过 SPI 注册各自的自动配置类,Spring Boot 在启动时会根据条件加载相应模块的配置。
自动配置:开发者可以通过 SPI 扩展自定义的自动配置模块,而无需修改 Spring Boot 的核心代码。
插件机制:开发者可以通过 SPI 编写自己的插件和扩展模块,从而定制和扩展 Spring Boot 的默认行为。
加载流程
-
声明模块配置:模块(如自动配置类)在各自的 spring.factories 文件中注册,它们的类全限定名会被记录下来。
-
动态加载模块:在应用启动时,Spring Boot 会自动扫描类路径,加载所有模块的 spring.factories 文件,并根据条件加载相应的自动配置类。
-
解耦模块与核心应用:通过 SPI 机制,模块的自动配置类可以与核心应用代码解耦,开发者可以根据需求选择不同的模块。
1 | # 自动配置类的声明 |
解析流程:
- 扫描类路径:Spring Boot 在启动时会扫描所有 JAR 包中的 META-INF/spring.factories 文件。
- 读取配置:Spring Boot 将所有的自动配置类和其他组件(如监听器、过滤器等)从文件中读取并注册到应用上下文中。
- 条件加载:自动配置类的加载依赖于条件注解(如 @ConditionalOnClass),根据运行时环境决定是否加载这些配置。
自动配置
Spring Boot 通过加载 META-INF/spring.factories 文件来实现模块化扩展
Spring Boot 自动配置的实现依赖于两个关键组件:
@EnableAutoConfiguration 注解:当应用程序启动时,Spring Boot 会扫描所有被 @EnableAutoConfiguration 注解标记的配置类。这些类是在 spring.factories 文件中注册的。
spring.factories 文件:在每个包含自动配置的模块中,都有一个 META-INF/spring.factories 文件,用于声明该模块的自动配置类。Spring Boot 会根据该文件自动加载相应的配置。
自动配置的加载流程:
配置声明:自动配置类通过 @Configuration 和 @Conditional 注解来声明条件化的 Bean 加载逻辑。
条件加载:Spring Boot 在启动时会根据运行时环境和依赖情况决定是否加载某个自动配置类。这通过 @ConditionalOnClass、@ConditionalOnMissingBean 等注解来控制。
自动配置加载:Spring Boot 扫描 spring.factories 文件中的自动配置类,并根据注解条件逐一判断是否需要加载这些配置。
1 |
|
Demo 示例
自定义 Starter 实现 SPI 扩展
一个自定义 Starter 是一个包含自动配置逻辑的库,它能够简化某种特定功能的配置。在 Spring Boot 中,创建自定义 Starter 通常会结合 SPI 机制来自动加载相应的配置类。
创建一个 Spring Boot 项目:定义一个包含自定义功能的模块。
定义自动配置类:该类使用 @Configuration 和 @Conditional 注解来实现自动化配置。
注册自动配置类到 spring.factories 文件中:使用 SPI 机制,让 Spring Boot 在启动时自动加载该配置。
1 | //定义服务接口和实现 |
1 | //自动配置类 |
spring.factories 文件
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
Dubbo SPI
Dubbo 中内置了一个轻量版本的 IoC 容器,用来管理框架内部的插件,实现包括插件实例化、生命周期、依赖关系自动注入等能力。
Dubbo 对JAVA SPI做了一定的改造与加强:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩展点加载失败,JDK SPI 没给出详细信息,不方便定位问题,Dubbo SPI 在失败时记录真正的失败原因,并打印出来
- 增加 IOC、AOP 能力
- 增加排序能力
- 增加条件激活能力
- 提供了一系列更灵活的 API,如获取所有 SPI 扩展实现、根据名称查询某个扩展实现、根据类型查询扩展实现、查询匹配条件的扩展实现等。
加载流程
-
使用
@SPI注解标记接口。 -
配置文件放在
META-INF/dubbo/或META-INF/dubbo/internal/。 -
支持按名称获取实现(如
ExtensionLoader.getExtension("redis"))。 -
支持自适应扩展(
@Adaptive)、自动包装(Wrapper)、依赖注入等。
读取并解析配置文件
缓存所有扩展实现
基于用户执行的扩展名,实例化对应的扩展实现
进行扩展实例属性的 IOC 注入以及实例化扩展的包装类,实现 AOP 特性
Demo 示例
1 | // 1.提供 SPI 插件实现类 |
1 | # 2.在指定文件配置实现类:resources/META-INF/services/ 目录下添加 org.apache.dubbo.rpc.Protocol 文件 |
1 | // 3. 通过配置启用自定义协议实现 |
SPI 与 API
SPI与API区别:
API是调用,并用于实现目标的类、接口、方法等的描述;
SPI是扩展和实现,以实现目标的类、接口、方法等的描述;
SPI和API的使用场景:
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。