Java SPI 機制,「可插拔」的奧義所在~

蔡不菜丶發表於2022-01-10

大家好,我是小菜。
一個希望能夠成為 吹著牛X談架構 的男人!如果你也想成為我想成為的人,不然點個關注做個伴,讓小菜不再孤單!

本文主要介紹 SPI 機制

如有需要,可以參考

如有幫助,不忘 點贊

微信公眾號已開啟,菜農曰,沒關注的同學們記得關注哦!

我們上篇文章講到了 Java 中 Agent 用法,不少小夥伴都覺得該方式比較偏門,平常開發不常用(幾乎沒用)。其實不然,不常用是跟專案掛鉤,專案不常用不代表該方法機制不常用,因此很多時候我們學習不能坐井觀天,認為專案中沒用到就可以不學,跟著專案成長往往不能成長~!

上篇跳轉入口:Java 高階用法,寫個代理侵入你?

那麼這篇我們將繼續講 Java 中的另一個知識點,也就是 SPI 機制,乍聽感覺依然陌生,這時可別再打退堂鼓!往下看你就會發現原來平時開發中經常看到!

一、SPI

我們這篇文章以問題作為導向,用問題來驅動學習,小菜先丟擲幾個問題,下面將針對這幾個問題進行解釋並擴充套件

  • 什麼是 SPI ?
  • SPI 和 API 的區別?
  • 平常中有使用到 SPI 嗎?

1、什麼是 SPI

SPI 是三個單詞的縮寫 Service Provider Interface,字面意思:服務提供介面。它是 Java 提供的一套用來被第三方實現或者擴充套件的介面,它可以用來啟用框架擴充套件和替換元件。具體作用便是為這些被擴充套件的 API 尋找服務實現。

而Java SPI 便是 JDK 內建的一種服務提供發現機制,常用於建立可擴充套件、可替換元件的應用程式,是java中模組化外掛化的關鍵。

這裡我們提到了兩個概念,分別是 模組化外掛化。模組化很好理解,就是將一個專案分成多個模組,模組間可能存在相互依賴(也就是通過 maven 的方式),有使用微服務開發的同學就毫不陌生了,如果沒有使用微服務開發也不打緊,單體專案中為了界定 control,service,repository層,也會將每個領域單獨提取成模組,而不是以目錄的方式~

2、類載入機制

上面我們已經說到了 SPI 較為粗淺的概念,小菜這裡不打算直接深入 SPI,在深入 SPI 之前,我們先了解一下 Java 中的類載入機制。類載入機制可能實際開發中並不會去在意,但是它卻無處不在,而這個也是面試的一大熱點話題。

在JVM中,類載入器預設是使用雙親委派原則,預設的類載入器包括Bootstarp ClassLoaderExtension ClassLoaderSystem ClassLoader(Application ClassLoader),當然可能還有自定義類載入器~自定義類載入器可以通過繼承 java.lang.classloader 來實現

各個類載入器作用範圍如下:

  • Bootstrap ClassLoader:負責載入 JDK 自帶的 rt.jar 包中的類檔案,是所有類載入的父類
  • Extension ClassLoader:負責載入 java 的擴充套件類庫從 jre/lib/ectjava.ext.dirs 系統屬性指定的目錄下載入類
  • System ClassLoader:負責從 classpath 環境變數中載入類檔案

類載入繼承關係圖如下:

1)雙親委派模型

什麼是雙親委派模型?

當一個類載入器收到載入類的任務時,會先交給自己的父載入器去完成,一級一級往上,因此最後都會傳遞到 Bootstrap ClassLoader 進行載入,只有當父載入器無法完成載入任務的時候,才會嘗試自己進行載入

為什麼要這樣設計呢?

1、採用雙親委派原則可以避免相同類重複載入,每個載入器在進行類載入任務的時候都會委派給自己的父類載入器進行載入,如果父類載入無法載入才自己進行載入,避免重複載入的局面

2、可以保證類載入的安全性,不管是哪個載入器載入這個類,最終都是委託給頂層的載入器進行載入,保證任何載入器最終得到的都是同一個類物件

載入過程如下:

這樣做的缺陷?

子類載入器可以使用父類載入器已經載入過的類,而父類載入器無法使用子類載入器載入過的類(類似繼承的關係)。這裡就可以扯到 Java SPI 了,Java 提供了很多服務提供者介面(SPI),它可以允許第三方為這些介面提供實現,比如資料庫中的 SPI 服務 - JDBC,這些 SPI 的介面由Java核心類提供,實現者確實第三方,這樣就會存在問題,提供者由 Bootstrap ClassLoader載入,而實現者是由第三方自定義類載入器載入,而這個時候頂層類載入就無法使用子類載入器載入過的類

=

解決方法

想要解決這個問題就得打破雙親委派原則

可以使用執行緒上下文類載入器(ContextClassLoader)載入

Java 應用上下文載入器預設是使用AppClassLoader,想要在父類載入器使用到子類載入器載入的類可以使用 Thread.currentThread().getContextClassLoader()

比如我們想要載入資源可以使用以下方式:

// 使用執行緒上下文類載入器載入資源
public static void main(String[] args) throws Exception{
    String name = "java/sql/Array.class";
    Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        System.out.println(url.toString());
    }
}

3、Java SPI

說完類載入機制,我們再回到 Java SPI 來,我們先通過例子熟悉下 SPI 的使用方式

使用過程圖如下:

更加通俗的理解,SPI 實際上就是一種策略模式的實現,基於介面程式設計再配合上配置檔案來讀取。這也符合我們的程式設計方式:可插拔~

使用例子如下:

專案結構

  • ICustomSvc:服務提供介面(也就是 SPI)
  • CustomSvcOne/CustomSvcTwo:實現者(這裡直接在一個專案中簡單實現,也可以通過 jar 包匯入的方式實現)
  • cbuc.life.spi.service.ICustomSvc:配置檔案

檔案內容

然後我們啟動 CustomTest 檢視控制檯結果

可以看到是可以載入到我們的實現類的方法,而這也就意味著已經實現了SPI 的功能

1)實現原理

其實我們上面使用SPI的時候可以看到一個關鍵的類那就是ServiceLoader ,該類位於 java.util包下,我們直接點進 load() 方法檢視如何呼叫

點進 load() 方法我們首席那看到的以下程式碼

該塊程式碼只是簡單的宣告瞭使用執行緒上下文載入器,我們繼續跟進 ServiceLoader.load(service, cl)

該塊程式碼也沒啥內容,宣告返回了 ServiceLoader 物件,這個物件有什麼文章?我們可以檢視這個類宣告

public final class ServiceLoader<S> implements Iterable<S>{}

可以看到這個物件實現了 Iterable 介面,說明具有迭代的方法,可以猜測這樣是為了取出我們定義 SPI 的所有實現類。

該類的建構函式如下

重點在於 reload() 方法,我們繼續跟進

這裡將註釋一起擷取出來,我們可以看到這句話 方法將惰性查詢例項化,說明了上述說到實現 Iterable 介面的用處,我們這裡可以先點進 iterator() 方法檢視是如何實現的

可以看到有個關鍵的快取,該快取儲存 provider,每次操作的時候都會去該快取中查詢,如果存在則返回,否則採用 LazyIterator 進行查詢,我們進行進入到LazyIterator類中檢視如何實現,由於該類程式碼過長,我們直接擷取關鍵程式碼,有興趣的同學可以自行檢視完整程式碼:

看到該程式碼的實現頓時豁然開朗了,我們看到了熟悉的目錄名 META-INF/services/,該程式碼會去指定目錄下獲取檔案資源,然後通過上傳傳入的執行緒上下文類載入器進行類載入,這樣子我們的 SPI 實現類就可以供專案使用了~ 看完不得不感嘆 妙啊~

到這裡為止,我們就已經拆解了 JAVA SPI 的使用以及實現原理,看完後是不是覺得該技巧也沒有離我們很遠~!

4、小結

使用 Java SPI 機制更好的實現了 可插拔 的開發理念,使得第三方服務模組的裝配與呼叫者的業務程式碼相分離,也就是 解耦 的概念,我們應用程式可以根據實際業務需要進行動態插拔。

二、擴充套件

Spring SPI

當然 SPI 機制不僅僅在 JDK 中實現,我們日常開發用到的 Spring 以及 Dubbo 框架都有對應的 SPI 機制。在Spring Boot中好多配置和實現都有預設的實現,我們如果想要修改某些配置,我們只需要在配置檔案中寫上對應的配置,那麼專案應用的便是我們定義的配置內容,而這種方式就是採用 SPI 實現的。

Java SPI 與 Spring SPI 的區別

  • JDK 使用的載入工具類是 ServiceLoader,而 Spring 使用的是 SpringFactoriesLoader
  • JDK 目錄命名方式是META-INF/services/提供方介面全類名,而 Spring 使用的是 META-INF/spring-factories

在使用 Spring Boot 中我們會將想要注入 IOC 容器的類將全類限定名寫到 META-INF/spring.factories檔案中,在 Spring Boot 程式啟動的時候就會由 SpringFactoriesLoader 進行載入,掃描每個 jar 包 class-path 目錄下的 META-INF/spring.factories 配置檔案,然後解析 properties 檔案,找到指定名稱的配置後返回

所以說 SPI 在我們實際開發中隨處可見,不止 Spring ,比如JDBC載入資料庫驅動,SLF4J載入不同提供商的日誌實現還有 Dubbo 使用SPI的方式實現框架的擴充套件等等

不要空談,不要貪懶,和小菜一起做個吹著牛X做架構的程式猿吧~點個關注做個伴,讓小菜不再孤單。我們們下文見!

今天的你多努力一點,明天的你就能少說一句求人的話!
我是小菜,一個和你一起變強的男人。 ?
微信公眾號已開啟,菜農曰,沒關注的同學們記得關注哦!

相關文章