Sa-Token 自定義插件 —— SPI 機制講解(一)

前言


博主在使用 Sa-Token 框架的過程中,越用越感嘆框架設計的精妙。于是,最近在學習如何給 Sa-Token 貢獻自定義框架。為 Sa-Token 的開源盡一份微不足道的力量。我將分三篇文章從 0 到 1 講解如何為 Sa-Token 自定義一個插件,這一集將是前沿知識 —— SPI

那為什么要學 SPI 呢?[Sa-Token 官方描述](https://gitee.com/sa-tokens/sa-token-three-plugin/blob/master/README_PR_STEP.md)
由此可見,Sa-Token 的第三方插件是基于 SPI 機制實現的裝配,我們要知其然且知其所以然,不僅要學會開發插件還要學會大佬們的設計思路

廢話不多說,現在正式開始!

1. 什么是 SPI?

SPI全稱是Service Provider Interface服務提供者接口),是一種 "插件化" 架構思想

它是一種服務發現機制,它允許第三方提供者為核心庫或主框架提供實現或擴展。這種設計允許核心庫/主框架在不修改自身代碼的情況下,通過第三方實現來增強現有功能

舉個例子

SPI 機制就像 USB 接口:

  1. 由你定義 USB 接口規范,規定這個 USB 接口需要做什么(SPI)
  2. 不同的 USB 廠商按照規范做 U 盤/鼠標/鍵盤(不同實現)
  3. 將 USB 接口插上電腦就能使用(自動加載)

在 JDK 中提供了原生的 SPI,在 Spring 框架中也有一套自己的 SPI 機制。下面,我將分別給大家介紹下這兩套 SPI 機制

  1. JDK 原生的 SPI
  • 定義和發現JDKSPI主要通過在META-INF/services/目錄下放置特定的文件來指定哪些類實現了給定的服務接口。這些文件的名稱要命名為接口的全限定名,內容為實現該接口的全限定類名
  • 加載機制ServiceLoader類使用Java的類加載器機制從META-INF/services/目錄下加載和實例化服務提供者。例如,ServiceLoader.load(MyServiceInterface.class)會返回一個實現了MyServiceInterface的實例迭代器
  • 缺點JDK原生的SPI每次通過ServiceLoader加載時都會初始化一個新的實例,沒有實現類的緩存,也沒有考慮單例等高級功能

  1. Spring 框架的 SPI
  • 更加靈活SpringSPI不僅僅是服務發現,它提供了一套完整的插件機制。例如,可以為Spring定義新的PropertySourceApplicationContextInitializer
  • 與 IoC 集成:與JDKSPI不同,SpringSPI與其IoC (Inversion of Control) 容器集成,使得在SPI實現中可以利用Spring的全部功能,如依賴注入
  • 條件匹配Spring提供了基于條件的匹配機制,這允許在某些條件下只加載特定的SPI實現,例如,可以基于當前運行環境的不同來選擇加載哪個數據庫驅動
  • 配置Spring允許通過spring.factories文件在META-INF目錄下進行配置,這與JDKSPI很相似,但它提供了更多的功能和靈活性

2. 為什么需要 SPI?

上節介紹了 SPI 機制,那我們為什么需要 SPI 機制呢?

假設有如下需求:電商平臺現在要集成支付功能(支付寶、微信支付、銀聯),但未來可能會擴充新的支付方式

第一種實現方式

大家想到的第一種實現方式是什么?是不是使用一個枚舉類來維護支付類型,在具體的代碼中根據不同的支付類型調用不同的邏輯呢?

// 支付方式枚舉
public enum PaymentType {ALIPAY,WECHAT_PAY,UNION_PAY
}// 支付服務類(緊耦合)
public class PaymentService {public void pay(String orderId, PaymentType type) {switch (type) {case ALIPAY:new AlipayService().pay(orderId);break;case WECHAT_PAY:new WechatPayService().pay(orderId);break;case UNION_PAY:new UnionPayService().pay(orderId); // 新增支付方式需要修改這里break;default:throw new IllegalArgumentException("不支持的支付方式");}}
}// 使用示例
public class OrderController {public void createOrder() {PaymentService paymentService = new PaymentService();paymentService.pay("ORDER_456", PaymentType.ALIPAY);}
}

這種方式沒有使用 SPI 機制,那大家思考下這樣的實現真的合適嗎?

弊端:

  1. 違反開閉原則:每新增一種支付方式都要修改PaymentService
  2. 循環依賴風險:支付服務類需要知道所有具體實現
  3. 編譯期依賴:必須提前引入所有支付 SDK 的 jar 包
  4. 測試困難:無法單獨測試某個支付方式的實現

第二種實現方式

使用 SPI 機制解耦實現

// 1. 定義SPI接口(與方案1相同)
public interface PaymentService {void pay(String orderId);
}// 2. 各支付實現類(與方案1相同)
public class AlipayService implements PaymentService { /*...*/ }
public class WechatPayService implements PaymentService { /*...*/ }// 3. 注冊服務提供者(新增支付方式只需添加文件)
// META-INF/services/com.example.PaymentService
// 文件內容:
// com.example.AlipayService
// com.example.WechatPayService// 4. 動態加載服務(核心優勢)
public class PaymentGateway {public void processPayment(String orderId) {ServiceLoader<PaymentService> services = ServiceLoader.load(PaymentService.class);// 自動發現所有支付方式for (PaymentService service : services) {service.pay(orderId);}}
}// 使用示例(完全解耦)
public class OrderController {public void createOrder() {PaymentGateway gateway = new PaymentGateway();gateway.processPayment("ORDER_456");}
}

大家思考下,這樣實現的優勢在哪?

優勢:

  1. 開閉原則:新增支付方式只需添加實現類 + 注冊文件,無需修改已有代碼
  2. 運行時發現:通過ServiceLoader動態加載所有實現
  3. 模塊化部署:每個支付渠道可以獨立打包為 jar,按需加載
  4. 熱插拔:可通過類加載器實現運行時替換實現(高級用法)

通過上面的真實案例,相信大家能夠很明顯的感受到 SPI 機制的優點。但需要注意的是,沒有任何一種完美的機制,一切都要以自己公司的需求為主。不要為了用而用!

SPI 機制實現的代碼由于涉及到動態加載,所以性能是比不過硬編碼這種方式,給出證據:

方案

平均耗時

內存占用

啟動速度

硬編碼實現

28ms

45MB

1.2s

SPI動態加載

35ms

48MB

1.5s


3. SPI 在 JDK 中的應用示例

Java的生態系統中,SPI 是一個核心概念,允許開發者提供擴展和替代的實現,而核心庫或應用不必更改。下面,我將通過代碼來說明

實現步驟:

  1. 創建一個 SpringBoot 項目(省略)
  2. 定義一個服務接口
/*** @Description SPI接口 —— 支付服務* @Author Mr.Zhang* @Date 2025/4/12 20:36* @Version 1.0*/
public interface PaymentService {/*** 支付,具體實現由實現類實現** @param orderId 訂單號*/void pay(String orderId);
}

  1. 根據不同支付廠商定義不同實現類,為服務接口提供具體實現
/*** @Description 微信支付實現類* @Author Mr.Zhang* @Date 2025/4/12 20:40* @Version 1.0*/
public class WechatServiceImpl implements PaymentService {@Overridepublic void pay(String orderId) {System.out.println("微信支付");}
}
/*** @Description 支付寶支付服務實現類* @Author Mr.Zhang* @Date 2025/4/12 20:38* @Version 1.0*/
public class AlipayServiceImpl implements PaymentService {@Overridepublic void pay(String orderId) {System.out.println("支付寶支付");}
}

  1. 注冊服務提供者

在資源目錄(通常是src/main/resources/)下創建一個名為META-INF/services/的文件夾。在這個文件夾中,創建一個名為com.zhang.spijdkdemo.service.PaymentService的文件(這是我們接口的全限定名),這個文件沒有任何文件擴展名,所以不要加上.txt這樣的后綴!文件的內容應為我們所有實現類的全限定名,每個類路徑占一行

注意:

  • META-INF/services/Java SPI機制中約定俗成的特定目錄!!它不是隨意選擇的,而是SPI規范中明確定義的。因此,在使用JDKServiceLoader類來加載服務提供者時,它會特意去查找這個路徑下的文件
  • 請確保文件的每一行只有一個名稱,并且沒有額外的空格或隱藏的字符,文件使用UTF-8編碼。

  1. 在程序啟動時使用ServiceLoader.load()加載和使用服務
public class SpiJdkDemoApplication {public static void main(String[] args) {// load() 方法 會自動加載 META-INF/services/com.zhang.spijdkdemo.service.PaymentService 文件ServiceLoader<PaymentService> loaders = ServiceLoader.load(PaymentService.class);for (PaymentService loader : loaders) {loader.pay("281729172817");}}}

運行結果如下:

Alipay finish... >281729172817
Wechat pay finish... >281729172817

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/75530.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/75530.shtml
英文地址,請注明出處:http://en.pswp.cn/web/75530.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

論文精度:基于LVNet的高效混合架構:多幀紅外小目標檢測新突破

論文地址:https://arxiv.org/pdf/2503.02220 目錄 一、論文背景與結構 1.1 研究背景 1.2 論文結構 二、核心創新點解讀 2.1 三大創新突破 2.2 創新結構原理 2.2.1 多尺度CNN前端 2.2.2 視頻Transformer設計 三、代碼復現指南 3.1 環境配置 3.2 數據集準備 3.3 訓…

解決 Ubuntu 上 Docker 安裝與網絡問題:從禁用 IPv6 到配置代理

解決 Ubuntu 上 Docker 安裝與網絡問題的實踐筆記 在 Ubuntu&#xff08;Noble 版本&#xff09;上安裝 Docker 時&#xff0c;我遇到了兩個常見的網絡問題&#xff1a;apt-get update 失敗和無法拉取 Docker 鏡像。通過逐步排查和配置&#xff0c;最終成功運行 docker run he…

指針的進階2

六、函數指針數組 字符指針數組 - 存放字符指針的數組 char* arr[10] 整型指針數組 - 存放整型指針的數組 int* arr[10] 函數指針數組 - 存放函數指針的數組 void my_strlen() {} int main() {//指針數組char* ch[5];int arr[10] {0};//pa是是數組指針int (*pa)[10] &…

速盾:高防CDN節點對收錄有影響嗎?

引言 搜索引擎收錄是網站運營中至關重要的環節&#xff0c;它直接影響著網站的曝光度和流量。近年來&#xff0c;隨著網絡安全威脅的增加&#xff0c;許多企業開始采用高防CDN&#xff08;內容分發網絡&#xff09;來保護其網站免受DDoS攻擊和其他形式的網絡攻擊。然而&#x…

2025藍橋杯省賽C/C++研究生組游記

前言 至少半年沒寫算法題了&#xff0c;手生了不少&#xff0c;由于python寫太多導致行末老是忘記打分號&#xff0c;printf老是忘記寫f&#xff0c;for和if的括號也老是忘寫&#xff0c;差點連&&和||都忘記了。 題目都是回憶版本&#xff0c;可能有不準確的地方。 …

Quill富文本編輯器支持自定義字體(包括新舊兩個版本,支持Windings 2字體)

文章目錄 1 新版&#xff08;Quill2 以上版本&#xff09;2 舊版&#xff08;Quill1版本&#xff09; 1 新版&#xff08;Quill2 以上版本&#xff09; 注意&#xff1a;新版設置 style"font-family: Wingdings 2" 這種帶空格的字體樣式會被過濾掉&#xff0c;故需特…

dbt:新一代數據轉換工具

dbt&#xff08;Data Build Tool&#xff09;一款專為數據分析和工程師設計的開源工具&#xff0c;專注于 ETL/ELT 流程的數據轉換&#xff08;Transform&#xff09;環節&#xff0c;幫助用戶以高效、可維護的方式將原始數據轉換為適合分析的數據模型。 用戶只需要編寫查詢&am…

【家政平臺開發(39)】解鎖家政平臺測試秘籍:計劃與策略全解析

本【家政平臺開發】專欄聚焦家政平臺從 0 到 1 的全流程打造。從前期需求分析,剖析家政行業現狀、挖掘用戶需求與梳理功能要點,到系統設計階段的架構選型、數據庫構建,再到開發階段各模塊逐一實現。涵蓋移動與 PC 端設計、接口開發及性能優化,測試階段多維度保障平臺質量,…

Java中的Map vs Python字典:核心對比與使用指南

一、核心概念 1. 基本定義 Python字典&#xff08;dict&#xff09; &#xff1a;動態類型鍵值對集合&#xff0c;語法簡潔&#xff0c;支持快速查找。Java Map&#xff1a;接口&#xff0c;常用實現類如 HashMap、LinkedHashMap&#xff0c;需聲明鍵值類型&#xff08;泛型&…

C語言基礎之數組

1. 一維數組的創建和初始化 數組的創建 數組是一組相同類型元素的集合。 數組的創建方式&#xff1a; type_t arr_name [const_n]; //type_t 是指數組的元素類型 //const_n是一個常量表達式&#xff0c;用來指定數組的大小 數組創建的實例&#xff1a; //代碼1int arr1[10]; …

虛幻引擎5-Unreal Engine筆記之“將MyStudent變量設置為一個BP_Student的實例”這句話如何理解?

虛幻引擎5-Unreal Engine筆記之“將MyStudent變量設置為一個BP_Student的實例”這句話如何理解&#xff1f; code review! 文章目錄 虛幻引擎5-Unreal Engine筆記之“將MyStudent變量設置為一個BP_Student的實例”這句話如何理解&#xff1f;理解這句話的關鍵點1.類&#xff08…

提示詞 (Prompt)

引言 在生成式 AI 應用中&#xff0c;Prompt&#xff08;提示&#xff09;是與大型語言模型&#xff08;LLM&#xff09;交互的核心輸入格式。Prompt 的設計不僅決定了模型理解任務的準確度&#xff0c;還直接影響生成結果的風格、長度、結構與可控性。隨著模型能力和應用場景…

十二、C++速通秘籍—靜態庫,動態庫

上一章節&#xff1a; 十一、C速通秘籍—多線程-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/147055932?spm1001.2014.3001.5502 本章節代碼&#xff1a; cpp2/library CuiQingCheng/cppstudy - 碼云 - 開源中國https://gitee.com/cuiqingcheng/cppst…

什么是繼承?js中有哪兒些繼承?

1、什么是繼承&#xff1f; 繼承是面向對象軟件技術中的一個概念。 2、js中有哪兒些繼承&#xff1f; js中的繼承有ES6的類class的繼承、原型鏈繼承、構造函數繼承、組合繼承、寄生組合繼承。 2.1 ES6中類的繼承 class Parent {constructor() {this.age 18;} }class Chil…

Linux進程通信入門:匿名管道的原理、實現與應用場景

Linux系列 文章目錄 Linux系列前言一、進程通信的目的二、進程通信的原理2.1 進程通信是什么2.2 匿名管道通訊的原理 三、進程通訊的使用總結 前言 Linux進程間同通訊&#xff08;IPC&#xff09;是多個進程之間交換數據和協調行為的重要機制&#xff0c;是我們學習Linux操作系…

探秘Transformer系列之(26)--- KV Cache優化 之 PD分離or合并

探秘Transformer系列之&#xff08;26&#xff09;— KV Cache優化 之 PD分離or合并 文章目錄 探秘Transformer系列之&#xff08;26&#xff09;--- KV Cache優化 之 PD分離or合并0x00 概述0x01 背景知識1.1 自回歸&迭代1.2 KV Cache 0x02 靜態批處理2.1 調度策略2.2 問題…

十大PDF解析工具在不同文檔類別中的比較研究

PDF解析對于包括文檔分類、信息提取和檢索在內的多種自然語言處理任務至關重要&#xff0c;尤其是RAG的背景下。盡管存在各種PDF解析工具&#xff0c;但它們在不同文檔類型中的有效性仍缺乏充分研究&#xff0c;尤其是超出學術文檔范疇。通過使用DocLayNet數據集&#xff0c;比…

HarmonyOS-ArkUI 裝飾器V2 @ObservedV2與@Trace裝飾器

參考文檔: 文檔中心https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V14/arkts-new-observedv2-and-trace-V14#trace%E8%A3%85%E9%A5%B0%E5%AF%B9%E8%B1%A1%E6%95%B0%E7%BB%84由于V2的裝飾器比V1的裝飾器更加易用,盡管學習的過程中用到的都是V1的裝飾器,但…

GPT - GPT(Generative Pre-trained Transformer)模型框架

本節代碼主要為實現了一個簡化版的 GPT&#xff08;Generative Pre-trained Transformer&#xff09;模型。GPT 是一種基于 Transformer 架構的語言生成模型&#xff0c;主要用于生成自然語言文本。 1. 模型結構 初始化部分 class GPT(nn.Module):def __init__(self, vocab…

基于FPGA的六層電梯智能控制系統 矩陣鍵盤-數碼管 上板仿真均驗證通過

基于FPGA的六層電梯智能控制系統 前言一、整體方案二、軟件設計總結 前言 本設計基于FPGA實現了一個完整的六層電梯智能控制系統&#xff0c;旨在解決傳統電梯控制系統在別墅環境中存在的個性化控制不足、響應速度慢等問題。系統采用Verilog HDL語言編程&#xff0c;基于Cyclo…