SPI技術-JDK實現

SPI是什麼

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啓用框架擴展和替換組件。
在這裏插入圖片描述

Java SPI 實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制。

系統設計的各個抽象,往往有很多不同的實現方案,在面向的對象的設計裏,一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。爲了實現在模塊裝配的時候能不在程序裏動態指明,這就需要一種服務發現機制。
Java SPI就是提供這樣的一個機制:爲某個接口尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。所以SPI的核心思想就是解耦。

使用場景

概括地說,適用於:調用者根據實際使用需要,啓用、擴展、或者替換框架的實現策略
比較常見的例子:

  • 數據庫驅動加載接口實現類的加載
    JDBC加載不同類型數據庫的驅動
  • 日誌門面接口實現類加載
    SLF4J加載不同提供商的日誌實現類
  • Spring
    Spring中大量使用了SPI,比如:對servlet3.0規範對ServletContainerInitializer的實現、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo
    Dubbo中也大量使用SPI的方式實現框架的擴展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴展實現Filter接口

實際案例

  1. 定義一個接口IPerson

package com.alioo.spi;

public interface IPerson {
    void sayHello();

}


  1. 編寫一個程序入口類SPIDemo
    在這個程序入口類裏獲取該接口的實現類,並進行調用

package com.alioo.spi;

import java.util.Iterator;
import java.util.ServiceLoader;

public class SPIDemo {
    public static void main(String[] args) {
        ServiceLoader<IPerson> shouts = ServiceLoader.load(IPerson.class);
        System.out.println("shouts:" + shouts);

        Iterator<IPerson> it = shouts.iterator();
        while (it.hasNext()) {
            IPerson s = it.next();

            System.out.println("s:" + s);
            s.sayHello();
        }

    }
}

備註:
事實上到目前爲止我們還沒有這個接口的任何實現類,然而上面的代碼不影響我們編寫、編譯打包的

  1. 編寫接口的實現類Man

package com.alioo.spi;

public class Man implements IPerson {
    @Override
    public void sayHello() {
        System.out.println("我是男人");
    }
}

還需要創建一個META-INF/services目錄,並在該目錄下創建一個文件,文件名爲接口IPerson的包路徑,即com.alioo.spi.IPerson,文件內容爲實現類的包路徑

│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── alioo
│           │           └── spi
│           │               └── Man.java
│           └── resources
│               └── META-INF
│                   └── services
│                       └── com.alioo.spi.IPerson


# more src/main/resources/META-INF/services/com.alioo.spi.IPerson 
com.alioo.spi.Man

備註:
這個實現類Man可以獨立於上面的程序入口類SPIDemo而存在,比如將Man.class和META-INF目錄單獨打到一個jar裏,只需要SPIDemo指定classpath包含這個jar可以

  1. 運行效果
shouts:java.util.ServiceLoader[com.alioo.spi.IPerson]
s:com.alioo.spi.Man@610455d6
我是男人
  1. 引入更多的實現類
    比如再增加一個實現類Woman

package com.alioo.spi;

public class Woman implements IPerson {
    @Override
    public void sayHello() {
        System.out.println("我是女人");
    }
}

同步驟4,增加META-INF/services目錄及文件com.alioo.spi.IPerson,文件內容爲當前實現類的包路徑

│   ├── pom.xml
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       └── alioo
│           │           └── spi
│           │               └── Woman.java
│           └── resources
│               └── META-INF
│                   └── services
│                       └── com.alioo.spi.IPerson

more src/main/resources/META-INF/services/com.alioo.spi.IPerson
com.alioo.spi.Woman

這個時候的運行效果如下:


shouts:java.util.ServiceLoader[com.alioo.spi.IPerson]
s:com.alioo.spi.Man@610455d6
我是男人
s:com.alioo.spi.Woman@60e53b93
我是女人

總結

優點

使用Java SPI機制的優勢是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一起。應用程序可以根據實際業務情況啓用框架擴展或替換框架組件。

相比使用提供接口jar包,供第三方服務模塊實現接口的方式,SPI的方式使得源框架,不必關心接口的實現類的路徑,可以不用通過下面的方式獲取接口實現類:

  • 代碼硬編碼import導入實現類
  • 指定類全路徑反射獲取:例如在JDBC4.0之前,JDBC中獲取數據庫驅動類需要通過Class.forName(“com.mysql.jdbc.Driver”),類似語句先動態加載數據庫相關的驅動,然後再進行獲取連接等的操作
  • 第三方服務模塊把接口實現類實例註冊到指定地方,源框架從該處訪問實例
  • 通過SPI的方式,第三方服務模塊實現接口後,在第三方的項目代碼的META-INF/services目錄下的配置文件指定實現類的全路徑名,源碼框架即可找到實現類

缺點

  • 雖然ServiceLoader也算是使用的延遲加載 ,但是基本只能通過遍歷全部獲取,也就是接口的實現類全部加載並實例化一遍。如果你並不想用某些實現類,它也被加載並實例化了,這就造成了浪費。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。
  • 多個併發多線程使用ServiceLoader類的實例是不安全的。

使用場景附加說明

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

java.sql.DriverManager 含有static代碼塊,當該類被加載時,會觸發調用loadInitialDrivers()方法

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

在loadInitialDrivers()方法中含有jdk spi的調用


ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

通過這種方式再也不用向以前那樣調用這句話了```Class.forName(“com.mysql.jdbc.Driver”)``

幾個問題

既然這麼方便,爲啥之前就得加Class.forName(…)了呢

因爲JDK SPI技術是從jdk6纔開始有的(閱讀下ServiceLoader類的註釋就可以看到了)

參考文章
作者:分佈式系統架構
鏈接:https://www.jianshu.com/p/46b42f7f593c
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章