最近新寫了一箇中介軟體「執行時動態日誌等級開關」,其中使用Java SPI機制實現了自定義配置中心,保證良好的擴充套件性。
專案地址,走過路過可以點個star :)
https://github.com/saigu/LogLevelSwitch
在使用過程中,突然發現SPI其實和日常寫API介面,然後進行implements實現非常相似,那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,看起來基本一致呀。
區別在哪?各自的使用場景是啥?
別急,我們從頭梳理一下。
從「面向介面程式設計」的思想來看,「呼叫方」應該通過呼叫「介面」而不是「具體實現」來處理邏輯。那麼,對於「介面」的定義,應該在「呼叫方」還是「實現方」呢?
理論上來說,會有三種選擇:
- 「介面」定義在「實現方」
- 「介面」定義在「呼叫方」
- 「介面」定義在 獨立的包中
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…(歷史文章查閱非常方便)