一文搞懂Java的SPI機制 1 簡介 源碼 使用 適用場景

1 簡介

SPI,Service Provider Interface,一種服務發現機制。



有了SPI,即可實現服務接口與服務實現的解耦:

  • 服務提供者(如 springboot starter)提供出 SPI 接口。身爲服務提供者,在你無法形成絕對規範強制時,適度"放權" 比較明智,適當讓客戶端去自定義實現
  • 客戶端(普通的 springboot 項目)即可通過本地註冊的形式,將實現類註冊到服務端,輕鬆實現可插拔

缺點

  • 不能按需加載。雖然 ServiceLoader 做了延遲加載,但是隻能通過遍歷的方式全部獲取。如果其中某些實現類很耗時,而且你也不需要加載它,那麼就形成了資源浪費
  • 獲取某個實現類的方式不夠靈活,只能通過迭代器的形式獲取

Dubbo SPI 實現方式對以上兩點進行了業務優化。

源碼


應用程序通過迭代器接口獲取對象實例,這裏首先會判斷 providers 對象中是否有實例對象:

  • 有實例,那麼就返回
  • 沒有,執行類的裝載步驟,具體類裝載實現如下:

LazyIterator#hasNextService 讀取 META-INF/services 下的配置文件,獲得所有能被實例化的類的名稱,並完成 SPI 配置文件的解析

LazyIterator#nextService 負責實例化 hasNextService() 讀到的實現類,並將實例化後的對象存放到 providers 集合中緩存

使用

如某接口有3個實現類,那系統運行時,該接口到底選擇哪個實現類呢?
這時就需要SPI,根據指定或默認配置,找到對應實現類,加載進來,然後使用該實現類實例

如下系統運行時,加載配置,用實現A2實例化一個對象來提供服務:


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

別人用這個接口,然後用你的jar包,就會在運行時通過你的jar包指定文件找到這個接口該用哪個實現類。這是JDK內置提供的功能。

我就不定義在 META-INF/services 下面行不行?就想定義在別的地方可以嗎?

No!JDK 已經規定好配置路徑,你若隨便定義,類加載器可就不知道去哪裏加載了


假設你有個工程P,有個接口A,A在P無實現類,系統運行時怎麼給A選實現類呢?
可以自己搞個jar包,META-INF/services/,放上一個文件,文件名即接口名,接口A的實現類=com.javaedge.service.實現類A2
讓P來依賴你的jar包,等系統運行時,P跑起來了,對於接口A,就會掃描依賴的jar包,看看有沒有META-INF/services文件夾:

  • 有,再看看有無名爲接口A的文件:
    • 有,在裏面查找指定的接口A的實現是你的jar包裏的哪個類即可

適用場景

插件擴展

比如你開發了一個開源框架,若你想讓別人自己寫個插件,安排到你的開源框架裏中,擴展功能時。

如JDBC。Java定義了一套JDBC的接口,但並未提供具體實現類,而是在不同雲廠商提供的數據庫實現包。

但項目運行時,要使用JDBC接口的哪些實現類呢?

一般要根據自己使用的數據庫驅動jar包,比如我們最常用的MySQL,其mysql-jdbc-connector.jar 裏面就有:


系統運行時碰到你使用JDBC的接口,就會在底層使用你引入的那個jar中提供的實現類。

案例

如sharding-jdbc 數據加密模塊,本身支持 AES 和 MD5 兩種加密方式。但若客戶端不想用內置的兩種加密,偏偏想用 RSA 算法呢?難道每加一種算法,sharding-jdbc 就要發個版本?

sharding-jdbc 可不會這麼蠢,首先提供出 EncryptAlgorithm 加密算法接口,並引入 SPI 機制,做到服務接口與服務實現分離的效果。
客戶端想要使用自定義加密算法,只需在客戶端項目 META-INF/services 的路徑下定義接口的全限定名稱文件,並在文件內寫上加密實現類的全限定名



這就顯示了SPI的優點:

  • 客戶端(自己的項目)提供了服務端(sharding-jdbc)的接口自定義實現,但是與服務端狀態分離,只有在客戶端提供了自定義接口實現時纔會加載,其它並沒有關聯;客戶端的新增或刪除實現類不會影響服務端
  • 如果客戶端不想要 RSA 算法,又想要使用內置的 AES 算法,那麼可以隨時刪掉實現類,可擴展性強,插件化架構
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章