聊聊Java的SPI机制

个人自建博客地址

什么是SPI呢?

SPI全称Service Provider Interface,翻译过来就是服务提供者接口。调用方提供接口声明,服务提供方对接口进行实现,提供服务的一种机制,服务提供方往往是第三方或者是外部扩展。

下面是一段java.util.ServiceLoader 类的注释:

A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application’s class path or by some other platform-specific means.

翻译:

服务提供者是对某一服务的具体实现。在服务提供者中的类一般会实现服务所定义的接口,并继承服务本身定义的类。服务提供者可以通过扩展的方式安装到Java平台的实现中,也就是说,将jar文件放置到标准的扩展目录之一。此外,服务提供者还可以通过将其添加到应用程序的类路径,或者使用其他与平台相关的手段来使其可用。

可以这样理解,Java提供了一种机制可以帮我们务发现加载某个接口的实现类,实现类不在本模块中,实现类可以由第三方提供,可以是依赖的jar或是其他扩展方式。

SPI的好处是什么?

SPI机制使用了接口,自然有接口的特点,面相接口编程,提供制定标准,实现由是实现者提供。

  1. **解耦和可扩展性:**SPI将接口与实现分离,我们就可以在不修改接口的情况下,轻松替换实现和新增新的实现,这也有利于模块化开发的扩展。
  2. **标准化:**SPI提供了一种标准化的方式来定义和实现服务,这样不同的开发者可以遵循相同的规则来提供和消费服务,减少了集成时的混乱和错误。

SPI原理

用一个示例画一个SPI原理图如下:

use
load
load
ServiceInterface
+serviceMethod()
ServiceProviderA
+serviceMethod()
ServiceProviderB
+serviceMethod()
ServiceLoader
  • ServiceInterface 是一个定义了服务方法的接口。
  • ServiceProviderAServiceProviderB 是实现了 ServiceInterface 的具体服务提供者。
  • ServiceLoader 负责加载服务并调用。

Java SPI ServiceLoader工作流程

  1. 首先在服务调用者中有一个功能接口A
    1. 第三方服务提供者作为插件模块要实现这个功能,首先有一个实现类com.test.AImpl实现这个接口,然后在自己的模块里的META-INF/services/目录下创建 com.test.A文件,,这里文件名是A接口的全限定名,文件内容就是com.test.AImpl,也就是实现类的全限定名。
  2. 服务调用者使用ServiceLoader 创建加载器,根据接口精确遍历META-INF/services/ 目录对对应接口的实现类进行反射并实例化,这样我们就可以获得的根据A接口的不同实现了。

接下来上示例代码

定义服务接口:

package com.example.service;/*** 定义演出接口*/
public interface Perform {void show();
}

负责表演歌曲的服务提供者:

package com.example.serviceprovider1;public class Singer implements com.example.service.Perform {public void show() {System.out.println("表演歌曲节目");}
}

服务提供者所在jar中:

文件名:META-INF/services/com.test.A

内容:com.example.serviceprovider1.Singer

负责表演舞蹈的服务提供者:

package com.example.serviceprovider;import com.example.service.Perform;/*** 舞者提供才艺目*/
public class Dancer implements Perform {public void show() {System.out.println("表演跳舞节目");}}

服务提供者所在jar中:

文件名:META-INF/services/com.test.A

内容:com.example.serviceprovider.Dancer

调用者:

package com.example.serviceuser;import com.example.service.Perform;import java.util.Iterator;
import java.util.ServiceLoader;public class MainTestSpi {public static void main(String[] args) {ServiceLoader<Perform> serviceLoader = ServiceLoader.load(Perform.class);Iterator<Perform> iterator = serviceLoader.iterator();while (iterator.hasNext()) {Perform perform = iterator.next();perform.show();}}
}
调用结果:
表演跳舞节目
表演歌曲节目

不同框架的SPI思想实现之JDBC

我们先说JDBC中的SPI机制实现

  1. JDK中java.sql.Driver 接口定义了定义了驱动与数据库交互的标准方法。
  2. 不同的数据库厂商提供具体的驱动实现类,例如MySQL驱动实现了Driver接口的connect()方法,用于建立数据库连接。
  3. 每个驱动JAR包的META-INF/services目录下需创建一个以接口全限定名(如java.sql.Driver)命名的文件,文件内容为实现类的全限定名(如com.mysql.cj.jdbc.Driver)。
  4. ServiceLoader通过此文件发现并加载驱动。

这是Mysql JDBC通过SPI机制注册驱动的核心代码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {//// Register ourselves with the DriverManager//static {try {java.sql.DriverManager.registerDriver(new Driver());} catch (SQLException E) {throw new RuntimeException("Can't register driver!");}}/*** Construct a new driver and register it with DriverManager* * @throws SQLException*             if a database error occurs.*/public Driver() throws SQLException {// Required for Class.forName().newInstance()}
}

SerciceLoader的Iterator 调用next() 方法时,就会触发java.sql.DriverManager.registerDriver(new Driver()); 对数据库厂商的驱动进行注册。

加载和注册驱动的过程如下:

ServiceLoader Driver DriverManager JVM iterator.next()触发反射加载 类加载(Loading、Linking、Initialization) 初始化阶段执行静态代码块 registerDriver(new Driver()) 将驱动实例添加到registeredDrivers列表 ServiceLoader Driver DriverManager JVM

Spring Boot 自动装配也体现了SPI思想

自动装配是 SPI 的“升级版”
  • 隐式接口:用注解和文件约定替代显式接口,降低侵入性。
  • 动态加载:通过条件注解实现按需装配,而非一次性加载所有实现类。
  • 开箱即用:通过 Starter 依赖传递,开发者只需关注业务逻辑,无需手动配置。
自动装配流程
  1. Spring Boot 通过 SpringFactoriesLoader 扫描所有依赖中的以下文件:

    • 旧方式META-INF/spring.factories(键为 EnableAutoConfiguration)。

    • 新方式(Spring Boot 2.7+):META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

# AutoConfiguration.imports
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
...
  1. 过滤和排序配置类

    • 去重与过滤:排除重复的配置类,并根据 @AutoConfigureOrder@Order 注解排序。

    • 排除不需要的配置:通过 spring.autoconfigure.exclude 配置或 @EnableAutoConfiguration(exclude=...) 排除特定配置类。

  2. 条件化评估(Conditional Evaluation)

Spring Boot 通过 @Conditional 系列注解 动态决定是否启用某个配置类或 Bean。常见的条件注解包括:

注解作用
@ConditionalOnClass类路径中存在指定类时生效。
@ConditionalOnMissingBean容器中不存在指定 Bean 时生效。
@ConditionalOnProperty配置文件中存在指定属性且匹配值时生效。
@ConditionalOnWebApplication应用是 Web 应用时生效。

示例

java

@AutoConfiguration
@ConditionalOnClass(DataSource.class) // 存在 DataSource 类时生效
public class DataSourceAutoConfiguration {@Bean@ConditionalOnMissingBean // 容器中没有 DataSource Bean 时生效public DataSource dataSource() {return new HikariDataSource();}
}
  1. 加载并注册 Bean

通过条件评估的配置类中的 @Bean 方法会被执行,生成的 Bean 实例注册到 Spring 容器中。

  1. 自动装配的优先级

    • 用户自定义 Bean 优先:如果用户手动定义了某个 Bean(如 @Bean 方法),自动配置的 Bean 会被跳过(由 @ConditionalOnMissingBean 控制)。

    • 配置类加载顺序:通过 @AutoConfigureOrder@Order 控制配置类的执行顺序(值越小优先级越高)。

自动装配的触发时机

自动装配在 Spring 容器的 refresh() 阶段完成,具体步骤如下:

  1. 准备环境(Environment):加载配置文件(如 application.properties)。
  2. 创建 BeanFactory:初始化 Spring 容器的 Bean 工厂。
  3. 执行 BeanFactoryPostProcessor:处理 Bean 工厂的后期处理(如解析 @Configuration 类)。
  4. 加载自动配置类:通过 AutoConfigurationImportSelector 选择并加载符合条件的配置类。
  5. 注册 Bean 定义:将自动配置类中的 Bean 定义注册到容器。
  6. 实例化单例 Bean:完成所有 Bean 的初始化。

因此,Spring Boot 自动装配虽然没有传统意义上的接口,但通过标准化约定和条件化注解,更灵活地实现了 SPI 的核心思想:解耦服务提供者与消费者,实现模块化扩展

应用 SpringApplication @EnableAutoConfiguration AutoConfigurationImportSelector 条件评估器 BeanFactory 启动(run()) 触发自动装配 加载候选配置类 返回配置类列表 检查条件注解 返回是否满足条件 注册配置类中的Bean 确认Bean已注册 跳过该配置类 alt [条件满足] [条件不满足] loop [遍历每个配置类] 完成自动装配 启动完成 应用 SpringApplication @EnableAutoConfiguration AutoConfigurationImportSelector 条件评估器 BeanFactory

Java 自身的SPI通过ServiceLoader 实现,使用起来简单,但是没有条件过滤,不便于按需加载。Spring Boot的自动装配,Dubbo的扫描扩展等其他框架都根据自身需求实现了更好用的SPI服务加载流程。

SPI思想的应用场景

SPI(服务提供者接口)主要用于解耦接口与实现,支持模块化,插件化扩展。典型场景包括:

  1. 框架扩展:如JDBC驱动加载(Java SPI)、Dubbo的协议扩展(自适应SPI)。

  2. 插件系统:日志组件(Log4j2的@Plugin)。

  3. 配置自动化:Spring Boot Starter通过AutoConfiguration.imports实现“开箱即用”。

  4. 服务治理:微服务中动态加载服务发现(如Spring Cloud)、配置中心扩展。

  5. 跨平台适配:如SLF4J绑定不同日志实现,屏蔽底层差异。

    SPI通过约定发现+动态加载,提升系统灵活性和可维护性。

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

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

相关文章

langchain4j+local-ai小试牛刀

序 本文主要研究一下如何本地运行local-ai并通过langchain4j集成调用。 步骤 curl安装 curl https://localai.io/install.sh | sh% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed 100 21509 …

什么是“零日漏洞”(Zero-Day Vulnerability)?为何这类攻击被视为高风险威胁?

正文 零日漏洞&#xff08;Zero-Day Vulnerability&#xff09; 是指软件、硬件或系统中存在的、尚未被开发者发现或修复的安全漏洞。攻击者在开发者意识到漏洞存在之前&#xff08;即“零日”内&#xff09;利用该漏洞发起攻击&#xff0c;因此得名。这类漏洞的“零日”特性使…

鸿蒙 ArkUI 实现 2048 小游戏

2048 是一款经典的益智游戏&#xff0c;玩家通过滑动屏幕合并相同数字的方块&#xff0c;最终目标是合成数字 2048。本文基于鸿蒙 ArkUI 框架&#xff0c;详细解析其实现过程&#xff0c;帮助开发者理解如何利用声明式 UI 和状态管理构建此类游戏。 一、核心数据结构与状态管理…

Milvus高性能向量数据库与大模型结合

Milvus | 高性能向量数据库&#xff0c;为规模而构建Milvus 是一个为 GenAI 应用构建的开源向量数据库。使用 pip 安装&#xff0c;执行高速搜索&#xff0c;并扩展到数十亿个向量。https://milvus.io/zh Milvus 是什么&#xff1f; Milvus 是一种高性能、高扩展性的向量数据…

kettle插件-自定义函数-数据脱敏

平常我们在使用kettle抽取数据的时候会涉及到敏感数据邀请脱敏或者进行掩码的需求&#xff0c;今天我们使用自定义函数插件来实现这些需求。 1、将自定义函数插件&#xff08;kettle-func-plugin.zip&#xff09;解压后放到kettle的plugins目录下面&#xff0c;然后重启服务。…

LeetCode 每日一题 2025/2/24-2025/3/2

记录了初步解题思路 以及本地实现代码&#xff1b;并不一定为最优 也希望大家能一起探讨 一起进步 目录 2/24 1656. 设计有序流2/25 2502. 设计内存分配器2/26 1472. 设计浏览器历史记录2/27 2296. 设计一个文本编辑器2/28 2353. 设计食物评分系统3/1 131. 分割回文串3/2 2/24 …

C++动态与静态转换区别详解

文章目录 前言一、 类型检查的时机二、安全性三、适用场景四、代码示例对比总结 前言 在 C 中&#xff0c;dynamic_cast 和 static_cast 是两种不同的类型转换操作符&#xff0c;主要区别体现在类型检查的时机、安全性和适用场景上。以下是它们的核心区别&#xff1a; 一、 类…

探秘《矩阵之美》:解锁矩阵的无限魅力

在这个数据驱动的时代&#xff0c;矩阵作为数学中的瑰宝&#xff0c;不仅在理论研究中占据核心地位&#xff0c;更在工程技术、计算机科学、物理学、经济学等众多领域发挥着不可替代的作用。今天&#xff0c;让我们通过中科院大学耿修瑞老师&#xff08;中科院空天信息研究院研…

【MySQL】(2) 库的操作

SQL 关键字&#xff0c;大小写不敏感。 一、查询数据库 show databases; 注意加分号&#xff0c;才算一句结束。 二、创建数据库 {} 表示必选项&#xff0c;[] 表示可选项&#xff0c;| 表示任选其一。 示例&#xff1a;建议加上 if not exists 选项。 三、字符集编码和排序…

Vue3实现文件上传、下载及预览全流程详解(含完整接口调用)

文章目录 一、环境准备1.1 创建Vue3项目1.2 安装依赖1.3 配置Element Plus 二、文件上传实现2.1 基础上传组件2.2 自定义上传逻辑&#xff08;Axios实现&#xff09; 三、文件下载实现3.1 直接下载&#xff08;已知文件URL&#xff09;3.2 后端接口下载&#xff08;二进制流&am…

分布式数据存储:提升系统弹性与性能的技术之路

分布式数据存储:提升系统弹性与性能的技术之路 在当今数据爆炸式增长的时代,传统的单机存储系统已无法满足大规模、高并发、低延迟的需求。尤其是在大数据、云计算和物联网的推动下,数据存储面临着前所未有的挑战。分布式数据存储应运而生,通过将数据分布在多个物理节点上…

在编译Linux的内核镜像和模块时,必须先编译内核镜像,再编译模块,顺序不可随意调整的原因

问&#xff1a;在编译Linux的内核镜像和模块时,必须先编译内核镜像,再编译模块,顺序不可随意调整 答&#xff1a;在编译 Linux 内核和模块时&#xff0c;必须先编译内核镜像&#xff0c;再编译模块&#xff0c;顺序不可随意调整。 原因&#xff1a; 模块依赖内核的头文件和符…

免费使用 DeepSeek API 教程及资源汇总

免费使用 DeepSeek API 教程及资源汇总 一、DeepSeek API 资源汇总1.1 火山引擎1.2 百度千帆1.3 阿里百炼1.4 腾讯云 二、其他平台2.1 华为云2.2 硅基流动 三、总结 DeepSeek-R1 作为 2025 年初发布的推理大模型&#xff0c;凭借其卓越的逻辑推理能力和成本优势&#xff0c;迅速…

千峰React:案例二

完成对html文档还有css的引入&#xff0c;引入一下数据&#xff1a; import { func } from prop-types import ./购物车样式.css import axios from axios import { useImmer } from use-immer import { useEffect } from reactfunction Item() {return (<li classNameacti…

用DeepSeek生成批量删除处理 PDF第一页工具

安装依赖库 在运行程序之前&#xff0c;请确保安装所需的库&#xff1a; pip install pymupdf python-docx Python 程序代码 import os import fitz # PyMuPDF from docx import Documentdef delete_pdf_first_page(input_path, output_path):"""删除 PDF…

redis的下载和安装详解

一、下载redis安装包 进入redis官网查看当前稳定版本&#xff1a; https://redis.io/download/发现此时的稳定版本是6.2.4&#xff0c; 此时可以去这个网站下载6.2.4稳定版本的tar包。 暂时不考虑不在windows上使用redis&#xff0c;那样将无法发挥redis的性能 二、上传tar…

如何使用 Jenkins 实现 CI/CD 流水线:从零开始搭建自动化部署流程

如何使用 Jenkins 实现 CI/CD 流水线:从零开始搭建自动化部署流程 在软件开发过程中,持续集成(CI)和持续交付(CD)已经成为现代开发和运维的标准实践。随着代码的迭代越来越频繁,传统的手动部署方式不仅低效,而且容易出错。为了提高开发效率和代码质量,Jenkins作为一款…

Python基于Django的网络课程在线学习平台【附源码】

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

Pytorch为什么 nn.CrossEntropyLoss = LogSoftmax + nn.NLLLoss?

为什么 nn.CrossEntropyLoss LogSoftmax nn.NLLLoss&#xff1f; 在使用 PyTorch 时&#xff0c;我们经常听说 nn.CrossEntropyLoss 是 LogSoftmax 和 nn.NLLLoss 的组合。这句话听起来简单&#xff0c;但背后到底是怎么回事&#xff1f;为什么这两个分开的功能加起来就等于…

rabbitmq 延时队列

要使用 RabbitMQ Delayed Message Plugin 实现延时队列&#xff0c;首先需要确保插件已安装并启用。以下是实现延时队列的步骤和代码示例。 1. 安装 RabbitMQ Delayed Message Plugin 首先&#xff0c;确保你的 RabbitMQ 安装了 rabbitmq-delayed-message-exchange 插件。你可…