聊聊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.pswp.cn/web/70987.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/70987.shtml
英文地址,請注明出處:http://en.pswp.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 插件。你可…