天天看點

Java SPI機制

SPI是什麼

SPI(Service Privoder Interface),是Java提供的一套用來被第三方實作或者擴充的API,它可以用來啟用架構擴充和替換元件。

整體機制圖如下:

Java SPI機制

Java SPI 實際上是“基于接口的程式設計+政策模式+配置檔案”組合實作的動态加載機制。

系統設計的各個抽象,往往有很多不同的實作方案,在面向的對象的設計裡,一般推薦子產品之間基于接口程式設計,子產品之間不對實作類進行寫死。

一旦代碼裡涉及具體的實作類,就違反了可拔插的原則,如果需要替換一種實作,就需要修改代碼。為了實作在子產品裝配的時候能不在程式裡動态指明,這就需要一種服務發現機制。

Java SPI就是提供這樣的一個機制:為某個接口尋找服務實作的機制。有點類似IOC的思想,就是将裝配的控制權移到程式之外,在子產品化設計中這個機制尤其重要。是以SPI的核心思想就是解耦。

使用場景

概括地說,适用于:調用者根據實際使用需要,啟用、擴充、或者替換架構的實作政策。

比較常見的例子:

  • 資料庫驅動加載。如:JDBC加載不同類型資料庫的驅動。
  • 日志門面接口實作類加載。如:SLF4J加載不同提供商的日志實作類。
  • Spring中大量使用了SPI,比如:對servlet3.0規範對

    ServletContainerInitializer

    的實作、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等。
  • 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,需要遵循如下約定:

  1. 當服務提供者提供了接口的一種具體實作後,在jar包的META-INF/services目錄下建立一個以“接口全限定名”為命名的檔案,内容為實作類的全限定名。
  2. 接口實作類所在的jar包放在主程式的classpath中。
  3. SPI的實作類必須攜帶一個不帶參數的構造方法。

最後,主程式通過

java.util.ServiceLoder

動态裝載實作子產品,它通過掃描

META-INF/services

目錄下的配置檔案找到實作類的全限定名,把類加載到

JVM

中。

JDBC驅動

我們知道了要想使用Java的SPI,就要滿足它的約定。mysql的驅動亦是如此。如下所示:

Java SPI機制

這個配置檔案裡定義了具體的服務實作類清單。和我們上面代碼列印出的實作類是一緻的。

至此,Java的SPI機制介紹完畢了。本文介紹了SPI是什麼,以及使用SPI的條件和場景。沒有涉及到具體SPI的底層實作。其實底層實作就在

ServiceLoader.load(Class<S> service)

方法裡。感興趣的可以讀讀該方法的底層邏輯實作,對深入了解SPI大有幫助。

繼續閱讀