一文搞懂Java的SPI機(jī)制(推薦)
1 簡(jiǎn)介
SPI,Service Provider Interface,一種服務(wù)發(fā)現(xiàn)機(jī)制。

有了SPI,即可實(shí)現(xiàn)服務(wù)接口與服務(wù)實(shí)現(xiàn)的解耦:
- 服務(wù)提供者(如 springboot starter)提供出 SPI 接口。身為服務(wù)提供者,在你無法形成絕對(duì)規(guī)范強(qiáng)制時(shí),適度"放權(quán)" 比較明智,適當(dāng)讓客戶端去自定義實(shí)現(xiàn)
- 客戶端(普通的 springboot 項(xiàng)目)即可通過本地注冊(cè)的形式,將實(shí)現(xiàn)類注冊(cè)到服務(wù)端,輕松實(shí)現(xiàn)可插拔
缺點(diǎn)
- 不能按需加載。
- 雖然 ServiceLoader 做了延遲加載,但是只能通過遍歷的方式全部獲取。如果其中某些實(shí)現(xiàn)類很耗時(shí),而且你也不需要加載它,那么就形成了資源浪費(fèi)獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過迭代器的形式獲取
Dubbo SPI 實(shí)現(xiàn)方式對(duì)以上兩點(diǎn)進(jìn)行了業(yè)務(wù)優(yōu)化。
源碼

應(yīng)用程序通過迭代器接口獲取對(duì)象實(shí)例,這里首先會(huì)判斷 providers 對(duì)象中是否有實(shí)例對(duì)象:
- 有實(shí)例,那么就返回
- 沒有,執(zhí)行類的裝載步驟,具體類裝載實(shí)現(xiàn)如下:
LazyIterator#hasNextService 讀取 META-INF/services 下的配置文件,獲得所有能被實(shí)例化的類的名稱,并完成 SPI 配置文件的解析
LazyIterator#nextService 負(fù)責(zé)實(shí)例化 hasNextService() 讀到的實(shí)現(xiàn)類,并將實(shí)例化后的對(duì)象存放到 providers 集合中緩存
使用
如某接口有3個(gè)實(shí)現(xiàn)類,那系統(tǒng)運(yùn)行時(shí),該接口到底選擇哪個(gè)實(shí)現(xiàn)類呢?
這時(shí)就需要SPI,根據(jù)指定或默認(rèn)配置,找到對(duì)應(yīng)實(shí)現(xiàn)類,加載進(jìn)來,然后使用該實(shí)現(xiàn)類實(shí)例。
如下系統(tǒng)運(yùn)行時(shí),加載配置,用實(shí)現(xiàn)A2實(shí)例化一個(gè)對(duì)象來提供服務(wù):

再如,你要通過jar包給某個(gè)接口提供實(shí)現(xiàn),就在自己jar包的META-INF/services/目錄下放一個(gè)接口同名文件,指定接口的實(shí)現(xiàn)是自己這個(gè)jar包里的某類即可:

別人用這個(gè)接口,然后用你的jar包,就會(huì)在運(yùn)行時(shí)通過你的jar包指定文件找到這個(gè)接口該用哪個(gè)實(shí)現(xiàn)類。這是JDK內(nèi)置提供的功能。
我就不定義在 META-INF/services 下面行不行?就想定義在別的地方可以嗎?
No!JDK 已經(jīng)規(guī)定好配置路徑,你若隨便定義,類加載器可就不知道去哪里加載了

假設(shè)你有個(gè)工程P,有個(gè)接口A,A在P無實(shí)現(xiàn)類,系統(tǒng)運(yùn)行時(shí)怎么給A選實(shí)現(xiàn)類呢?
可以自己搞個(gè)jar包,META-INF/services/,放上一個(gè)文件,文件名即接口名,接口A的實(shí)現(xiàn)類=com.javaedge.service.實(shí)現(xiàn)類A2。
讓P來依賴你的jar包,等系統(tǒng)運(yùn)行時(shí),P跑起來了,對(duì)于接口A,就會(huì)掃描依賴的jar包,看看有沒有META-INF/services文件夾:
有,再看看有無名為接口A的文件: 有,在里面查找指定的接口A的實(shí)現(xiàn)是你的jar包里的哪個(gè)類即可
適用場(chǎng)景
插件擴(kuò)展
比如你開發(fā)了一個(gè)開源框架,若你想讓別人自己寫個(gè)插件,安排到你的開源框架里中,擴(kuò)展功能時(shí)。
如JDBC。Java定義了一套JDBC的接口,但并未提供具體實(shí)現(xiàn)類,而是在不同云廠商提供的數(shù)據(jù)庫實(shí)現(xiàn)包。
但項(xiàng)目運(yùn)行時(shí),要使用JDBC接口的哪些實(shí)現(xiàn)類呢?
一般要根據(jù)自己使用的數(shù)據(jù)庫驅(qū)動(dòng)jar包,比如我們最常用的MySQL,其mysql-jdbc-connector.jar 里面就有:

系統(tǒng)運(yùn)行時(shí)碰到你使用JDBC的接口,就會(huì)在底層使用你引入的那個(gè)jar中提供的實(shí)現(xiàn)類。
案例
如sharding-jdbc 數(shù)據(jù)加密模塊,本身支持 AES 和 MD5 兩種加密方式。但若客戶端不想用內(nèi)置的兩種加密,偏偏想用 RSA 算法呢?難道每加一種算法,sharding-jdbc 就要發(fā)個(gè)版本?
sharding-jdbc 可不會(huì)這么蠢,首先提供出 EncryptAlgorithm 加密算法接口,并引入 SPI 機(jī)制,做到服務(wù)接口與服務(wù)實(shí)現(xiàn)分離的效果。
客戶端想要使用自定義加密算法,只需在客戶端項(xiàng)目 META-INF/services 的路徑下定義接口的全限定名稱文件,并在文件內(nèi)寫上加密實(shí)現(xiàn)類的全限定名


這就顯示了SPI的優(yōu)點(diǎn):
- 客戶端(自己的項(xiàng)目)提供了服務(wù)端(sharding-jdbc)的接口自定義實(shí)現(xiàn),但是與服務(wù)端狀態(tài)分離,只有在客戶端提供了自定義接口實(shí)現(xiàn)時(shí)才會(huì)加載,其它并沒有關(guān)聯(lián);客戶端的新增或刪除實(shí)現(xiàn)類不會(huì)影響服務(wù)端
- 如果客戶端不想要 RSA 算法,又想要使用內(nèi)置的 AES 算法,那么可以隨時(shí)刪掉實(shí)現(xiàn)類,可擴(kuò)展性強(qiáng),插件化架構(gòu)
到此這篇關(guān)于一文搞懂Java的SPI機(jī)制的文章就介紹到這了,更多相關(guān)Java的SPI機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot如何解決非controller類引用service的問題
這篇文章主要介紹了springboot如何解決非controller類引用service的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
SpringBoot在項(xiàng)目停止(服務(wù)停止/關(guān)閉退出)之后執(zhí)行的方法
這篇文章主要給大家介紹了SpringBoot在項(xiàng)目停止(服務(wù)停止/關(guān)閉退出)之后執(zhí)行的兩種方法,實(shí)現(xiàn)DisposableBean接口和使用@PreDestroy注解,文中有詳細(xì)的代碼講解,具有一定的參考價(jià)值,需要的朋友可以參考下2023-12-12
maven-surefire-plugin總結(jié)示例詳解
這篇文章主要介紹了maven-surefire-plugin總結(jié),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
JAVA實(shí)現(xiàn)長(zhǎng)連接(含心跳檢測(cè)Demo)
這篇文章主要介紹了JAVA實(shí)現(xiàn)長(zhǎng)連接(含心跳檢測(cè)Demo),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10
java根據(jù)當(dāng)前時(shí)間獲取yyyy-MM-dd?HH:mm:ss標(biāo)準(zhǔn)格式的時(shí)間代碼示例
在Java中可以使用java.time包中的LocalDateTime類和DateTimeFormatter類來獲取并格式化當(dāng)前時(shí)間為yyyy-MM-dd?HH:mm:ss的格式,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-10-10

