SPI是什麼
SPI(Service Privoder Interface),是Java提供的一套用來被第三方實作或者擴充的API,它可以用來啟用架構擴充和替換元件。
整體機制圖如下:
Java SPI 實際上是“基于接口的程式設計+政策模式+配置檔案”組合實作的動态加載機制。
系統設計的各個抽象,往往有很多不同的實作方案,在面向的對象的設計裡,一般推薦子產品之間基于接口程式設計,子產品之間不對實作類進行寫死。
一旦代碼裡涉及具體的實作類,就違反了可拔插的原則,如果需要替換一種實作,就需要修改代碼。為了實作在子產品裝配的時候能不在程式裡動态指明,這就需要一種服務發現機制。
Java SPI就是提供這樣的一個機制:為某個接口尋找服務實作的機制。有點類似IOC的思想,就是将裝配的控制權移到程式之外,在子產品化設計中這個機制尤其重要。是以SPI的核心思想就是解耦。
使用場景
概括地說,适用于:調用者根據實際使用需要,啟用、擴充、或者替換架構的實作政策。
比較常見的例子:
- 資料庫驅動加載。如:JDBC加載不同類型資料庫的驅動。
- 日志門面接口實作類加載。如:SLF4J加載不同提供商的日志實作類。
- Spring中大量使用了SPI,比如:對servlet3.0規範對
的實作、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等。ServletContainerInitializer
- Dubbo中也大量使用SPI的方式實作架構的擴充,不過它對Java提供的原生SPI做了封裝,允許使用者擴充實作Filter接口。
使用SPI
我們已經知道JDBC就是一個SPI的典型實作。關于資料庫連接配接,JDK提供了一整套相關的類和接口的完整體系,而具體的實作是由相應的資料庫廠商來提供的。看下面的例子,通過Java的SPI去加載mysql驅動。
import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SPITest {
public static void main(String[] args) {
// 這個Driver類就是jdk内置的接口
// 我們通過JDK内置的ServiceLoader元件去加載相應接口的實作類
ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
Iterator<Driver> iterable = serviceLoader.iterator();
while (iterable.hasNext()) {
Driver driver = iterable.next();
System.out.println("加載mysql驅動:" + driver.getClass());
}
}
}
當然,運作此程式前,需要先把mysql驅動放到類路徑下。我們得到的結果是:
加載mysql驅動:class com.mysql.jdbc.Driver
加載mysql驅動:class com.mysql.fabric.jdbc.FabricMySQLDriver
顯然,我們已經加載到了mysql的驅動。上面代碼很簡單,僅僅一行代碼
ServiceLoader.load(Driver.class)
如何定義一個SPI
步驟1:定義一組接口 (假設是
org.foo.demo.IShout
),并寫出接口的一個或多個實作,(假設是
org.foo.demo.animal.Dog
、
org.foo.demo.animal.Cat
)。
public interface IShout {
void shout();
}
public class Cat implements IShout {
@Override
public void shout() {
System.out.println("miao miao");
}
}
public class Dog implements IShout {
@Override
public void shout() {
System.out.println("wang wang");
}
}
步驟2:在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以接口命名的檔案 (
org.foo.demo.IShout
檔案),内容是要應用的實作類(這裡是
org.foo.demo.animal.Dog
和
org.foo.demo.animal.Cat
,每行一個類)。
org.foo.demo.animal.Dog
org.foo.demo.animal.Cat
步驟3:使用
ServiceLoader
來加載配置檔案中指定的實作。
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
for (IShout s : shouts) {
s.shout();
}
}
}
代碼輸出:
wang wang
miao miao
我們就能拿到所有的具體實作類了。是以想要弄懂Java的SPI,我們需要從
ServcieLoader
這個類入手。
ServiceLoader
以下内容介紹來源ServiceLoader的Java doc。建議大家自己去讀一遍。
ServiceLoader
是一個簡單的服務提供者加載元件。(注意,從JDK1.6開始,Java平台才提供了該元件。)
Service和Service-Provider
所謂服務,就是一個熟知的接口和類(通常為抽象類)的集合。提供者中的類通常實作服務接口,或者繼承這個服務抽象類。服務提供者可以以擴充的形式安裝在 Java 平台的實作中,也就是将 jar 檔案放入任意常用的擴充目錄中。也可通過将提供者加入應用程式類路徑下。
在上面的例子裡, java.sql.Driver
就是服務接口,mysql驅動的jar包就是服務提供者。
加載的機制
為了達到加載的目的,服務應該由單個類型表示,也就是單個接口或抽象類(可以使用具體類,但建議不要這樣做)。一個指定服務的提供者包含一個或多個具體類,這些類擴充了這個服務類型,具有特定于提供者的資料和代碼。提供者類的細節是與特定的服務高度相關的。使用該元件的唯一強制要求的是,提供者類必須具有不帶參數的構造方法,以便它們可以在加載中被執行個體化。
通過在資源目錄
META-INF/services
中放置提供者配置檔案來辨別服務提供者。配置檔案的名稱必須是服務類型的全限定的二進制名稱。該檔案包含一個具體提供者類的全限定二進制名稱的清單。然後檔案必須使用 UTF-8 編碼。
由此我們知道,要使用Java的SPI,需要遵循如下約定:
- 當服務提供者提供了接口的一種具體實作後,在jar包的META-INF/services目錄下建立一個以“接口全限定名”為命名的檔案,内容為實作類的全限定名。
- 接口實作類所在的jar包放在主程式的classpath中。
- SPI的實作類必須攜帶一個不帶參數的構造方法。
最後,主程式通過
java.util.ServiceLoder
動态裝載實作子產品,它通過掃描
META-INF/services
目錄下的配置檔案找到實作類的全限定名,把類加載到
JVM
中。
JDBC驅動
我們知道了要想使用Java的SPI,就要滿足它的約定。mysql的驅動亦是如此。如下所示:
這個配置檔案裡定義了具體的服務實作類清單。和我們上面代碼列印出的實作類是一緻的。
至此,Java的SPI機制介紹完畢了。本文介紹了SPI是什麼,以及使用SPI的條件和場景。沒有涉及到具體SPI的底層實作。其實底層實作就在
ServiceLoader.load(Class<S> service)
方法裡。感興趣的可以讀讀該方法的底層邏輯實作,對深入了解SPI大有幫助。