你應該瞭解的 Java SPI 機制

crossoverJie發表於2020-02-24

前言

不知大家現在有沒有去公司復工,我已經在家辦公將近 3 周了,同時也在家呆了一個多月;還好工作並沒有受到任何影響,我個人一直覺得遠端工作和 IT 行業是非常契合的,這段時間的工作效率甚至比在辦公室還高,同時由於我們公司的業務在海外,所以疫情幾乎沒有造成太多影響。

扯遠了,這次主要是想和大家分享一下 JavaSPI 機制。週末沒啥事,我翻了翻我之前的寫的部落格 《設計一個可拔插的 IOC 容器》,發現當時的實現並不那麼優雅。

還沒看過的朋友的我先做個前景提要,當時的需求:

我實現了一個類似於的 SpringMVC 但卻很輕量的 http 框架 cicada,其中當然也需要一個 IOC 容器,可以存放所有的單例 bean。

這個 IOC 容器的實現我希望可以有多種方式,甚至可以提供一個介面供其他人實現;當然切換這個 IOC 容器的過程肯定是不能存在硬編碼的,也就是這裡所提到的可拔插。 當我想使用 A 的實現方式時,我就引入 A 的 jar 包,使用 B 時就引入 B 的包。

cicada8-spi.md---0082zybply1gc6sqv3gp4j30zm0u0n8c.jpg

先給大家看看兩次實現的區別,先從程式碼簡潔程度來說就是 SPI 更勝一籌。

什麼是 SPI

在具體分析之前還是先了解下 SPI 是什麼?

首先它其實是 Service provider interface 的簡寫,翻譯成中文就是服務提供發現介面。

不過這裡不要被這個名詞搞混了,這裡的服務發現和我們常聽到的微服務中的服務發現並不能劃等號。

就如同上文提到的對 IOC 容器的多種實現方式 A、B、C(可以把它們理解為服務),我需要在執行時知道應該使用哪一種具體的實現。

其實本質上來說這就是一種典型的面向介面程式設計,這一點在我們剛開始學習程式設計的時候就被反覆強調了。

SPI 實踐

接下來我們來如何來利用 SPI 實現剛才提到的可拔插 IOC 容器。

既然剛才都提到了 SPI 的本質就是面向介面程式設計,所以自然我們首先需要定義一個介面:

cicada8-spi.md---0082zybply1gc6tlhql39j31490u0wjj.jpg

其中包含了一些 Bean 容器所必須的操作:註冊、獲取、釋放 bean。

為了讓其他人也能實現自己的 IOC 容器,所以我們將這個介面單獨放到一個 Module 中,可供他人引入實現。

cicada8-spi.md---0082zybply1gc6tobsdgwj30u40ewdh1.jpg

所以當我要實現一個單例的 IOC 容器時,我只需要新建一個 Module 然後引入剛才的模組並實現 CicadaBeanFactory 介面即可。

當然其中最重要的則是需要在 resources 目錄下新建一個 META-INF/services/top.crossoverjie.cicada.base.bean.CicadaBeanFactory 檔案,檔名必須得是我們之前定義介面的全限定名(SPI 規範)。

cicada8-spi.md---0082zybply1gc6ts164zlj30uk0amq3x.jpg

其中的內容便是我們自己實現類的全限定名:

top.crossoverjie.cicada.bean.ioc.CicadaIoc
複製程式碼

可以想象最終會通過這裡的全限定名來反射建立物件。

只不過這個過程 Java 已經提供 API 遮蔽掉了:

    public static CicadaBeanFactory getCicadaBeanFactory() {
        ServiceLoader<CicadaBeanFactory> cicadaBeanFactories = ServiceLoader.load(CicadaBeanFactory.class);
        if (cicadaBeanFactories.iterator().hasNext()){
            return cicadaBeanFactories.iterator().next() ;
        }

        return new CicadaDefaultBean();
    }
複製程式碼

classpath 中存在我們剛才的實現類(引入實現類的 jar 包),便可以通過 java.util.ServiceLoader 工具類來找到所有的實現類(可以有多個實現類同時存在,只不過通常我們只需要一個)。


一些都準備好之後,使用自然就非常簡單了。

    <dependency>
        <groupId>top.crossoverjie.opensource</groupId>
        <artifactId>cicada-ioc</artifactId>
        <version>2.0.4</version>
    </dependency>
複製程式碼

我們只需要引入這個依賴便能使用它的實現,當我們想換一種實現方式時只需要更換一個依賴即可。

這樣就做到了不修改一行程式碼靈活的可拔插選擇 IOC 容器了。

SPI 的一些其他應用

雖然平時並不會直接使用到 SPI 來實現業務,但其實我們使用過的絕大多數框架都會提供 SPI 介面方便使用者擴充套件自己的功能。

比如 Dubbo 中提供一系列的擴充套件:

cicada8-spi.md---0082zybply1gc6ue6zubvj30gq0pymyq.jpg

同型別的 RPC 框架 motan 中也提供了響應的擴充套件:

cicada8-spi.md---0082zybply1gc6ufacqt5j30lm0j8q5j.jpg

他們的使用方式都和 Java SPI 非常類似,只不過原理略有不同,同時也新增了一些功能。

比如 motanspi 允許是否為單例等等。

再比如 MySQL 的驅動包也是利用 SPI 來實現自己的連線邏輯。

cicada8-spi.md---0082zybply1gc6uqg2ga2j30ii0bmdgz.jpg

總結

Java 自身的 SPI 其實也有點小毛病,比如:

  • 遍歷載入所有實現類效率較低。
  • 當多個 ServiceLoader 同時 load 時會有併發問題(雖然沒人這麼幹)。

最後總結一下,SPI 並不是某項高深的技術,本質就是面向介面程式設計,而面向介面本身在我們日常開發中也是必備技能,所以瞭解使用 SPI 也是很用處的。

本文所有原始碼:

github.com/TogetherOS/…

你的點贊與分享是對我最大的支援

你應該瞭解的 Java SPI 機制

相關文章