Java - SPI

SPI簡(jiǎn)介

SPI,Service Provider Interface,主要是被框架的開(kāi)發(fā)人員使用,比如java.sql.Driver接口,其他不同廠商可以針對(duì)同一接口做出不同的實(shí)現(xiàn),mysql和postgresql都有不同的實(shí)現(xiàn)提供給用戶,而Java的SPI機(jī)制可以為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)。

當(dāng)服務(wù)的提供者提供了一種接口的實(shí)現(xiàn)之后,需要在classpath下的META-INF/services/目錄里創(chuàng)建一個(gè)以服務(wù)接口命名的文件,這個(gè)文件里的內(nèi)容就是這個(gè)接口的具體的實(shí)現(xiàn)類。當(dāng)其他的程序需要這個(gè)服務(wù)的時(shí)候,就可以通過(guò)查找這個(gè)jar包(一般都是以jar包做依賴)的META-INF/services/中的配置文件,配置文件中有接口的具體實(shí)現(xiàn)類名,可以根據(jù)這個(gè)類名進(jìn)行加載實(shí)例化,就可以使用該服務(wù)了。JDK中查找服務(wù)的實(shí)現(xiàn)的工具類是:java.util.ServiceLoader。

SPI可以認(rèn)為是一個(gè)規(guī)范,按照此規(guī)范實(shí)施,就能被服務(wù)正確識(shí)別自定義的功能。

Dubbo,

如何使用SPI

使用SPI的大致流程如下:

  • 有關(guān)組織或者公司定義標(biāo)準(zhǔn)。
  • 具體廠商或者框架開(kāi)發(fā)者實(shí)現(xiàn)。
  • 程序猿使用。

詳細(xì)來(lái)講,就是:
標(biāo)準(zhǔn)制定者的工作

  • 使用ServiceLoader.load(Class class); 動(dòng)態(tài)加載Service接口的實(shí)現(xiàn)類。

具體實(shí)施者的工作

  • 在META-INF/services/目錄中創(chuàng)建以Service接口全限定名命名的文件,該文件內(nèi)容為Service接口具體實(shí)現(xiàn)類的全限定名,文件編碼必須為UTF-8。
  • 如SPI的實(shí)現(xiàn)類為jar,則需要將其放在當(dāng)前程序的classpath下。
  • Service的具體實(shí)現(xiàn)類必須有一個(gè)不帶參數(shù)的構(gòu)造方法。

應(yīng)用舉例

這里以JDBC為例

在JDBC4.0之前,我們開(kāi)發(fā)有連接數(shù)據(jù)庫(kù)的時(shí)候,通常會(huì)用Class.forName("com.mysql.jdbc.Driver")這句先加載數(shù)據(jù)庫(kù)相關(guān)的驅(qū)動(dòng),然后再進(jìn)行獲取連接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")來(lái)加載驅(qū)動(dòng),直接使用DriverManager.getConnection(String url)就可以了。

注:

在以前,操作數(shù)據(jù)庫(kù)的時(shí)候,需要先加載驅(qū)動(dòng)。不同的數(shù)據(jù)庫(kù)需要不同的驅(qū)動(dòng)。如:

  • mysql - Class.forName("com.mysql.jdbc.Driver").newInstance()
  • oracle - Class.forName("oracle.jdbc.driver.OracleDriver").newInstance()
  • sql server - Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver").newInstance()
  • postgreSQL - Class.forNaem("org.postgresql.Driver").newInstance()
  • db2 - Class.froName("com.ibm.db2.jdbc.app.DB2Driver").newInstance()

如果要更改數(shù)據(jù)庫(kù),就需要更改驅(qū)動(dòng),也就是說(shuō)要修改代碼。修改代碼是個(gè)很麻煩的工作。使用SPI可以使我們無(wú)需修改代碼就可以更換數(shù)據(jù)庫(kù)。

通過(guò)SPI是如何實(shí)現(xiàn)的呢?

1. 組織方制定接口

這里的組織方是JDK組織。JDK中有定義驅(qū)動(dòng)接口Driver:

public interface Driver{
    //接口方法略。。。
}

2. 實(shí)現(xiàn)方根據(jù)SPI規(guī)范實(shí)現(xiàn)接口

這里的實(shí)現(xiàn)方是各數(shù)據(jù)庫(kù)廠商

msyql
對(duì)于MySQL,在mysql-connector-java.jar下,有META-INF/services目錄,該目錄下有SPI需要的文件java.sql.Driver:

image.png

文件內(nèi)容為:

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

postgreSQL
同理,對(duì)于postgreSQL,在postgresql.jar中,META-INF/services目錄下也有名為java.sql.Driver的文件:

image.png

文件內(nèi)容為:

org.postgresql.Driver

其他數(shù)據(jù)庫(kù)與之類似。具體接口實(shí)現(xiàn)略過(guò)。

3. 組織方加載實(shí)現(xiàn)類

各個(gè)數(shù)據(jù)庫(kù)驅(qū)動(dòng)的加載是在JDK的DriverManager類中實(shí)現(xiàn)的。該類中有如下一段靜態(tài)代碼,會(huì)在類初始化的時(shí)候執(zhí)行:

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

loadInitialDrivers()函數(shù)的內(nèi)容為:

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
         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;
         }
     });
    //下面代碼略。。。
}

這段代碼中,ServiceLoader.load()會(huì)查找classpath以及jar包中,META-INF/services目錄下的所有java.sql.Driver的實(shí)現(xiàn)類,并實(shí)例化。

使用SPI的第一步操作如下。這里沒(méi)有去META-INF/services目錄下查找配置文件,也沒(méi)有加載具體實(shí)現(xiàn)類,做的事情就是封裝了我們的接口類型和類加載器,并初始化了一個(gè)迭代器。

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

第二步操作為:遍歷使用SPI獲取到的具體實(shí)現(xiàn),實(shí)例化各個(gè)實(shí)現(xiàn)類

//獲取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍歷所有的驅(qū)動(dòng)實(shí)現(xiàn)
while(driversIterator.hasNext()) {
    driversIterator.next();
}

在遍歷的時(shí)候,首先調(diào)用driversIterator.hasNext()方法,這里會(huì)搜索classpath下以及jar包中所有的META-INF/services目錄下的java.sql.Driver文件,并找到文件中的實(shí)現(xiàn)類的名字,此時(shí)并沒(méi)有實(shí)例化具體的實(shí)現(xiàn)類。

第三步為:實(shí)例化各個(gè)驅(qū)動(dòng)。關(guān)鍵代碼為:

driversIterator.next();

到此為止,classpath下的所有數(shù)據(jù)庫(kù)驅(qū)動(dòng)都被加載并實(shí)例化了。

在測(cè)試項(xiàng)目中添加了兩個(gè)jar包,mysql-connector-java-6.0.6.jar和postgresql-42.0.0.0.jar,跟蹤到DriverManager中之后:


image.png

可以看到此時(shí)迭代器中有兩個(gè)驅(qū)動(dòng),mysql和postgresql的都被加載了。有關(guān)兩個(gè)驅(qū)動(dòng)都加載了,具體使用哪個(gè)驅(qū)動(dòng),請(qǐng)自行深入jdbc的源碼。這里不做過(guò)多解析。

誰(shuí)使用了SPI

有很多的SPI擴(kuò)展機(jī)制應(yīng)用的實(shí)例,比如大名鼎鼎的slf4j, common-logging, JDBC, tomcat等等

SPI的思想

SPI重要的不是其原理,而是其思想。
Dubbo就借鑒了SPI的思想,實(shí)現(xiàn)了自動(dòng)擴(kuò)展。詳情可參考下面這篇文章。
Dubbo中SPI擴(kuò)展機(jī)制詳解

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容