某廠面試:如何優(yōu)雅使用 SPI 機制
代碼不多,文章可能有點長。朋友面試某廠問到的 SPI 機制,聯(lián)想到自己項目最近寫到的 SPI 場景,文章簡要描述下 SPI 機制的發(fā)展歷程
產(chǎn)出背景
因為最近項目中使用分庫分表以及數(shù)據(jù)加密使用到了 ShardingSphere,所以決定這段時間看看源碼實現(xiàn)。問我為什么要讀源碼?不看源碼怎么提高逼格嘞,就是這么樸實無華~

Sharding-Jdbc SPI
看源碼的歷程,往往從點開 Jar 包的瞬間開始。好巧不巧,就看到源代碼包下有個 SPI 包,處于好奇心就點了一點,嗯~ 代碼果然很熟悉,還是那個配方原來的味道

看了許久,陷入深深的沉思。內心小九九:這玩意好像之前看過,但是在哪我忘了,這到底是個啥?
代碼還是那個代碼,只是它認識我,我不認識它了
這一塊的 SPI 接口是 shrding-jdbc 預留自定義加密器的接口
看到這里相信就遇到過絕大多數(shù)技術同學都會遇到的一個問題,那就是 認為自己會了,實際情況呢?不一定。所以,學習一門技術,一定要多看幾遍,嘗試去理解記憶。千萬不要看一遍之后,眼高手低認為技術 so easy,然后隔十天半個月就啥都不記的

繼續(xù)回過頭來說說今天的主角:SPI。首先回答這么一個問題,什么是 SPI 機制
SPI 全稱為 Service Provider Interface,是一種服務發(fā)現(xiàn)機制。為了被第三方實現(xiàn)或擴展的 API,它可以用于實現(xiàn)框架擴展或組件替換
SPI 機制本質是將 接口實現(xiàn)類的全限定名配置在文件中,并由服務加載器讀取配置文件,加載文件中的實現(xiàn)類,這樣運行時可以動態(tài)的為接口替換實現(xiàn)類
看文字描述介紹總是枯燥無味且空洞的。簡單一點來說,就是你在 META-INF/services 下面定義個文件,然后通過一個特殊的類加載器,啟動的時候加載你定義文件中的類,這樣就能擴展原有框架的功能
就這么簡單,那可能有讀者會問:我不定義在 META-INF/services 下面行不行?就想定義在別的地方

不行滴,請遏制住這么危險的想法,人家怎么定義你就怎么實現(xiàn)。這是 JDK 規(guī)定好的配置路徑,你隨便定義,類加載器怎么知道去哪里加載

看到這個 PREFIX 常量之后,想法比較活躍的小伙子不知道清醒點了么。簡單畫張圖來描述下 SPI 的運行機制

有點 SPI 基礎的同學看到圖之后應該又開始自信了,這不就是我之前看過的那玩意么?是的,技術還是那個技術,可以繼續(xù)往下看看,有沒有自己不知道的
為什么要有 SPI
了解一項技術的前提,一定要知道它為了解決什么樣的痛點而存在,JDK 作者也不會沒屁事加點代碼玩
引入了 SPI 機制后,服務接口與服務實現(xiàn)就會達成分離的狀態(tài),可以實現(xiàn) 解耦以及程序可擴展機制。服務提供者(比如 springboot starter)提供出 SPI 接口后,客戶端(平常的 springboot 項目)就可以通過本地注冊的形式,將實現(xiàn)類注冊到服務端,輕松實現(xiàn)可插拔
數(shù)據(jù)加密舉例
以實際項目舉個例子,就拿 sharding-jdbc 數(shù)據(jù)加密模塊來說,sharding-jdbc 本身支持 AES 和 MD5 兩種加密方式。但是,如果客戶端不想用內置的兩種加密,偏偏想用 RSA 算法呢?難道每加一種算法,sharding-jdbc 就要發(fā)個版本么

sharding-jdbc 可不會這么干,首先提供出 Encryptor 加密接口,并引入 SPI 的機制,做到服務接口與服務實現(xiàn)分離的效果。如果客戶端想要使用新的加密算法,只需要在客戶端項目 META-INF/services 目錄下定義接口的全限定名稱文件,并在文件內寫上加密實現(xiàn)類的全限定名,就像這樣式的

通過 SPI 的方式,就可以將客戶端提供的加密算法加載到 sharding-jdbc 加密規(guī)則中,這樣就可以在項目運行中選擇自定義算法來對數(shù)據(jù)進行加密存儲
通過 sharding-jdbc 的例子,可以很好的看出來,上面提到的 SPI 優(yōu)點,都體現(xiàn)了出來
客戶端(自己的項目)提供了服務端(sharding-jdbc)的接口自定義實現(xiàn),但是與服務端狀態(tài)分離,只有在客戶端提供了自定義接口實現(xiàn)時才會加載,其它并沒有關聯(lián);客戶端的新增或刪除實現(xiàn)類不會影響服務端 如果客戶端不想要 RSA 算法,又想要使用內置的 AES 算法,那么可以隨時刪掉實現(xiàn)類,可擴展性強,插件化架構
對象存儲舉例


實戰(zhàn)講解


var 是 lombok 的注解,可以自動識別對象類型
FileServiceFactory大家可以理解為文件服務對外的統(tǒng)一訪問入口。實現(xiàn)了 spirng 初始化的一個接口,可以在 bean 初始化時進行代碼邏輯操作bean 初始化時,通過 ServiceLoader類加載器負責加載對象存儲接口,這樣就能加載到客戶端存放到META-INF/services中的自定義對象存儲實現(xiàn)獲取到自定義對象存儲后,和服務端本身自帶的對象存儲一起存放至容器中,這樣就可以根據(jù)項目中的 fileStoreType獲取對應的服務了

深入解析 SPI
ServiceLoader 底層都做了什么事情
providers 對象中是否有實例對象LazyIterator#hasNextService讀取META-INF/services下的配置文件,獲得所有能被實例化的類的名稱,并完成 SPI 配置文件的解析LazyIterator#nextService負責實例化hasNextService()讀到的實現(xiàn)類,并將實例化后的對象存放到providers集合中緩存

ServiceLoader 底層執(zhí)行的方法,跟著下面這個程序敲一遍代碼就懂了
結言
SPI 機制優(yōu)勢就是解耦。將接口的定義以及具體業(yè)務實現(xiàn)分離,而不是和業(yè)務端全部耦合在一端。可以實現(xiàn) 運行時根據(jù)業(yè)務實際場景啟用或者替換具體組件 SPI 機制的場景就是 沒有統(tǒng)一實現(xiàn)標準的業(yè)務場景。一般就是,服務端有標準的接口,但是沒有統(tǒng)一的實現(xiàn),需要業(yè)務方提供其具體實現(xiàn)。比如說 JDBC 的 java.sql.Driver接口和不同云廠商提供的數(shù)據(jù)庫實現(xiàn)包
不能按需加載。雖然 ServiceLoader 做了延遲加載,但是只能通過遍歷的方式全部獲取。如果其中某些實現(xiàn)類很耗時,而且你也不需要加載它,那么就形成了資源浪費 獲取某個實現(xiàn)類的方式不夠靈活,只能通過迭代器的形式獲取。這兩點可以參考 Dubbo SPI 實現(xiàn)方式進行業(yè)務優(yōu)化
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
