(轉)SPI機制

前言

在查看Spring裝配Servlet時候有用到的SPI機制
查看原文章JAVA 拾遺 – 關於 SPI 機制

JDK提供的SPI(Service Provider Interface)機制,可能很多人不太熟悉,因爲這個機制是針對廠商或者插件的,也可以在一些框架的擴展中看到。其核心類 java.util.ServiceLoader可以在 jdk1.8 的文檔中看到詳細的介紹。雖然不太常見,但並不代表它不常用,恰恰相反,你無時無刻不在用它。玄乎了,莫急,思考一下你的項目中是否有用到第三方日誌包,是否有用到數據庫驅動?其實這些都和 SPI 有關。再來思考一下,現代的框架是如何加載日誌依賴,加載數據庫驅動的,你可能會對class.forName(“com.mysql.jdbc.Driver”)這段代碼不陌生,這是每個 java 初學者必定遇到過的,但如今的數據庫驅動仍然是這樣加載的嗎?你還能找到這段代碼嗎?這一切的疑問,將在本篇文章結束後得到解答。

首先介紹 SPI機制是個什麼東西

實現一個自定義的 SPI

項目結構


invoker是我們的用來測試的主項目。
interface是針對廠商和插件商定義的接口項目,只提供接口,不提供實現。
good-printer,bad-printer 分別是兩個廠商對 interface 的不同實現,所以他們會依賴於interface項目。
這個簡單的 demo 就是讓大家體驗,在不改變invoker代碼,只更改依賴的前提下,切換interface的實現廠商。

interface 模塊

  • 2.1
moe.cnkirito.spi.api.Printer
public interface Printer {
    void print();
}

interface 只定義一個接口,不提供實現。規範的制定方一般都是比較牛叉的存在,這些接口通常位於 javajavax 前綴的包中。這裏的 Printer就是模擬一個規範接口。

good-printer 模塊

  • 3.1 good-printer\pom.xml
<dependencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

規範的具體實現類必然要依賴規範接口

  • 3.2 moe.cnkirito.spi.api.GoodPrinter
public class GoodPrinter implements Printer {
    public void print() {
        System.out.println("你是個好人 ~");
    }
}

作爲 Printer規範接口的實現一

  • 3.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer
    moe.cnkirito.spi.api.GoodPrinter

這裏需要重點說明,每一個 SPI 接口都需要在自己項目的靜態資源目錄中聲明一個 services 文件,文件名爲實現規範接口的類名全路徑,在此例中便是 moe.cnkirito.spi.api.Printer,在文件中,則寫上一行具體實現類的全路徑,在此例中便是 moe.cnkirito.spi.api.GoodPrinter。

這樣一個廠商的實現便完成了。

4 bad-printer 模塊

我們在按照和 good-printer 模塊中定義的一樣的方式,完成另一個廠商對 Printer 規範的實現。

  • 4.1 bad-printer\pom.xml
<dependencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 4.2 moe.cnkirito.spi.api.BadPrinter
public class BadPrinter implements Printer {
    public void print() {
        System.out.println("我抽菸,喝酒,蹦迪,但我知道我是好女孩 ~");
    }
}
  • 4.3 resources\META-INF\services\moe.cnkirito.spi.api.Printer
    moe.cnkirito.spi.api.BadPrinter

這樣,另一個廠商的實現便完成了。

invoker 模塊

這裏的 invoker 便是我們自己的項目了。如果一開始我們想使用廠商 good-printer 的 Printer 實現,是需要將其的依賴引入。

<dependencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>good-printer</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 5.1 編寫調用主類
public class MainApp {


    public static void main(String[] args) {
        ServiceLoader<Printer> printerLoader = ServiceLoader.load(Printer.class);
        for (Printer printer : printerLoader) {
            printer.print();
        }
    }
}

ServiceLoaderjava.util提供的用於加載固定類路徑下文件的一個加載器,正是它加載了對應接口聲明的實現類。

  • 5.2 打印結果1
你是個好人 ~

如果在後續的方案中,想替換廠商的 Printer 實現,只需要將依賴更換

ndencies>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>moe.cnkirito</groupId>
        <artifactId>bad-printer</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

調用主類無需變更代碼,這符合開閉原則

  • 5.3 打印結果 2
我抽菸,喝酒,蹦迪,但我知道我是好女孩 ~

是不是很神奇呢?這一切對於調用者來說都是透明的,只需要切換依賴即可!

SPI 在實際項目中的應用
先總結下有什麼新知識,resources/META-INF/services 下的文件似乎我們之前沒怎麼接觸過,ServiceLoader 也沒怎麼接觸過。那麼現在我們打開自己項目的依賴,看看有什麼發現。

mysql-connector-java-xxx.jar 中發現了META-INF\services\java.sql.Driver文件,裏面只有兩行記錄:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

我們可以分析出,java.sql.Driver 是一個規範接口,com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver 則是 mysql-connector-java-xxx.jar 對這個規範的實現接口。

jcl-over-slf4j-xxxx.jar中發現了META-INF\services\org.apache.commons.logging.LogFactory文件,裏面只有一行記錄:

org.apache.commons.logging.impl.SLF4JLogFactory

相信不用我贅述,大家都能理解這是什麼含義了

更多的還有很多,有興趣可以自己翻一翻項目路徑下的那些 jar 包

既然說到了數據庫驅動,索性再多說一點,還記得一道經典的面試題:class.forName(“com.mysql.jdbc.Driver”) 到底做了什麼事?

先思考下:自己會怎麼回答?

都知道 class.forName 與類加載機制有關,會觸發執行 com.mysql.jdbc.Driver類中的靜態方法,從而使主類加載數據庫驅動。如果再追問,爲什麼它的靜態塊沒有自動觸發?可答:因爲數據庫驅動類的特殊性質,JDBC 規範中明確要求 Driver 類必須向 DriverManager註冊自己,導致其必須由class.forName 手動觸發,這可以在java.sql.Driver 中得到解釋。完美了嗎?還沒,來到最新的DriverManager源碼中,可以看到這樣的註釋, 翻譯如下:

DriverManager類的方法getConnectiongetDrivers 已經得到提高以支持 Java Standard Edition Service Provider機制。JDBC 4.0 Drivers 必須包括 META-INF/services/java.sql.Driver 文件。此文件包含java.sql.Driver 的 JDBC 驅動程序實現的名稱。例如,要加載 my.sql.Driver類,META-INF/services/java.sql.Driver 文件需要包含下面的條目:

my.sql.Driver

應用程序不再需要使用 Class.forName()顯式地加載JDBC驅動程序。當前使用 Class.forName()加載JDBC驅動程序的現有程序將在不作修改的情況下繼續工作。

可以發現,Class.forName 已經被棄用了,所以,這道題目的最佳回答,應當是和面試官牽扯到 JAVA 中的 SPI 機制,進而聊聊加載驅動的演變歷史。

java.sql.DriverManager
public Void run() {
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();

    try{
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
    } catch(Throwable t) {
    // Do nothing
    }
    return null;
}

當然那,本節的內容還是主要介紹 SPI,驅動這一塊這是引申而出,如果不太理解,可以多去翻一翻 jdk1.8 中 DriverDriverManager 的源碼,相信會有不小的收穫。

SPI 在擴展方面的應用
SPI 不僅僅是爲廠商指定的標準,同樣也爲框架擴展提供了一個思路。框架可以預留出 SPI 接口,這樣可以在不侵入代碼的前提下,通過增刪依賴來擴展框架。前提是,框架得預留出核心接口,也就是本例中 interface 模塊中類似的接口,剩下的適配工作便留給了開發者。

發佈了74 篇原創文章 · 獲贊 6 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章