Java SPI 和 API,傻傻分不清?

阿丸發表於2022-05-06

最近新寫了一箇中介軟體「執行時動態日誌等級開關」,其中使用Java SPI機制實現了自定義配置中心,保證良好的擴充套件性。

專案地址,走過路過可以點個star :)

https://github.com/saigu/LogLevelSwitch

在使用過程中,突然發現SPI其實和日常寫API介面,然後進行implements實現非常相似,那SPI到底和普通API實現有啥區別呢?

Java SPI 和 API,傻傻分不清?

 

帶著這個問題,我們一起來梳理下SPI機制吧。

本文預計閱讀時間10分鐘,將圍繞以下幾點展開:

  • 什麼是 SPI 機制?
  • SPI 實踐案例
  • SPI 和 API 有啥區別?

1、什麼是SPI機制?

SPI(Service Provider Interface) 字面意思是服務提供者介面,本質上是一種「服務擴充套件機制」。

為什麼需要這樣一種「服務擴充套件機制」呢?

因為系統裡抽象的各個模組,比如日誌模組、xml解析模組、jdbc模組等,往往有很多不同的實現方案。

為了滿足可拔插的原則,我們一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。這就需要一種「服務擴充套件機制」,然後就有了SPI。

SPI 機制為我們的程式提供擴充功能。而不必將框架的一些實現類寫死在程式碼裡面。我們在相應配置檔案中定義好某個介面的實現類全限定名,並由服務載入器讀取配置檔案,載入實現類。這樣可以在執行時,動態為介面替換實現類。

最常見的就是Java的SPI機制,另外,還有Dubbo和SpringBoot自定義的SPI機制。

2、SPI實踐案例

2.1 業界SPI實踐案例

簡單瞭解了SPI的概念,我們看看業界有哪些SPI實踐案例,如何利用SPI實現靈活擴充套件的。

  • JDBC驅動載入

最常見的SPI機制實踐案例就是JDBC的驅動載入。利用Java的SPI機制,我們可以根據不同的資料庫廠商來引入不同的JDBC驅動包。

  • SpringBoot的SPI機制

用過SpringBoot的同學應該都知道,我們可以在spring.factories中加上我們自定義的自動配置類,這個特性尤其在xxx-starter中應用廣泛。

  • Dubbo的SPI機制

Dubbo基本上自身的每個功能點都提供了擴充套件點,把SPI機制應用的淋漓盡致,比如提供了叢集擴充套件、路由擴充套件和負載均衡擴充套件等差不多接近30個擴充套件點。
如果Dubbo的某個內建實現不符合我們的需求,那麼我們只要利用其SPI機制將我們的實現替換掉Dubbo的實現即可。

2.2 在實際專案中如何使用

以上三個例子是業界最常見的SPI機制的實現。下面,來看看我在實際專案中如何利用Java SPI機制實現了自定義配置中心,保證良好的擴充套件性。

專案地址,走過路過可以點個star :)

https://github.com/saigu/LogLevelSwitch

需求很簡單,中介軟體「執行時動態日誌等級開關」需要在應用執行時獲取開關狀態,然後動態改變應用日誌等級。

如何獲取開關狀態呢?我們一般需要配置中心來進行處理。作為一個開源中介軟體,使用它的應用可能有自己的不同的配置中心(比如Nacos、Apollo、spring cloud config、自研配置中心等),因此,必須支援自定義配置中心接入。

這時候就需要SPI機制來實現了!

1)定義介面interface

package io.github.saigu.log.level.sw.listener;

public interface ConfigListener<T> {
    /**
     * 獲取初始開關狀態
     * @return initial context of switch
     */
    SwitchContext getInitSwitch();

    /**
     * 獲取變化的配置
     * @param changedConfig changed config context
     */
    void listenChangedConfig(T changedConfig);
}

2)SPI載入

本專案通過Java SPI實現,不需要依賴額外的元件,通過ServiceLoader來動態載入

public class ChangeListenerFactory {
    public static ConfigListener getListener() {
        final ServiceLoader<ConfigListener> loader = ServiceLoader
                .load(ConfigListener.class);
        for (ConfigListener configListener : loader) {
            return configListener;
        }
        throw new IllegalArgumentException("please choose valid listener");
    }
}

3)應用自定義配置中心接入

使用這個中介軟體的應用,只需要三步即可接入自定義配置中心。

  • STEP 1: 應用中pom引入依賴
<dependency>
  <groupId>io.github.saigu</groupId>
  <artifactId>log-switch-core</artifactId>
  <version>1.0.0-beta</version>
</dependency>
  • STEP 2: 構建config Bean
@Configuration
public class LogLevelSwitchConfig {
    @Bean
    LogLevelSwitch logLevelSwitch() {
        return new LogLevelSwitch();
    }
}
  • STEP 3: 接入配置中心

宣告配置中心的SPI實現。

在resource路徑下新建 META-INF/services,建立檔名為
io.github.saigu.log.level.sw.listener.ConfigListener的檔案,並寫入自定義配置中心的「實現類名」。

3、SPI和API有啥區別?

我們已經介紹了什麼是SPI,怎麼使用SPI機制,現在,回頭來看看一開始提出的問題,SPI和API有啥區別呢?

它們都需要定義介面interface,然後自定義實現類implements,看起來基本一致呀。

區別在哪?各自的使用場景是啥?

別急,我們從頭梳理一下。

Java SPI 和 API,傻傻分不清?

 

從「面向介面程式設計」的思想來看,「呼叫方」應該通過呼叫「介面」而不是「具體實現」來處理邏輯。那麼,對於「介面」的定義,應該在「呼叫方」還是「實現方」呢?

理論上來說,會有三種選擇:

  • 「介面」定義在「實現方」
  • 「介面」定義在「呼叫方」
  • 「介面」定義在 獨立的包中

1)「介面」定義在「實現方」

先來看看「介面」定義在「實現方」的情況。這個很容易理解,實現方同時提供了「介面」和「實現類」,「呼叫方」可以引用介面來達到呼叫某實現類的功能,這就是我們日常使用的API。API的最顯著特徵就是:

實現和介面在一個包中。自己定義介面,自己實現類。

2)「介面」定義在「呼叫方」

再來看看「介面」屬於「呼叫方」的情況。這個其實就是SPI機制。以JDBC驅動為例,「呼叫方」(使用者或者說JDK)定義了java.sql.Driver介面,這個介面位於「呼叫方」JDK的包中,各個資料庫廠商實現了這個介面,比如mysql驅動com.mysql.jdbc.Driver。因此,SPI最顯著的特徵就是:

「介面」在「呼叫方」的包,「呼叫方」定義規則,而自定義實現類在「實現方」的包,然後把實現類載入到「呼叫方」中。

3)「介面」定義在獨立的包

最後一種情況,如果一個「介面」在一個上下文是API,在另一個上下文是SPI,那麼就可以把「介面」定義在獨立的包中。

4、小結

本文介紹了是SPI機制,然後結合業界案例與專案實踐來說明SPI的使用場景,最後對Java SPI和API的區別進行了分析。

本文不對SPI原理進行深入解析,下一篇文章會詳細分析下Java SPI的實現《大名鼎鼎的Java SPI機制,究竟有沒有破壞雙親委派呢?》,應該會挺有意思,歡迎關注。

 

都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜尋「阿丸筆記 」第一時間閱讀,回覆【筆記】獲取Canal、MySQL、HBase、JAVA實戰筆記,回覆【資料】獲取一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)

相關文章