Java的SPI機制

SPI是什么

SPI的英文名稱是Service Provider Interface蒜埋,是Java 內(nèi)置的服務發(fā)現(xiàn)機制真慢。

在開發(fā)過程中,將問題進抽象成API理茎,可以為API提供各種實現(xiàn)黑界。如果現(xiàn)在需要對API提供一種新的實現(xiàn),我們可以不用修改原來的代碼皂林,直接生成新的Jar包朗鸠,在包里提供API的新實現(xiàn)。通過Java的SPI機制础倍,可以實現(xiàn)了框架的動態(tài)擴展烛占,讓第三方的實現(xiàn)能像插件一樣嵌入到系統(tǒng)中。

Java的SPI類似于IOC的功能,將裝配的控制權移到了程序之外忆家,實現(xiàn)在模塊裝配的時候不用在程序中動態(tài)指明犹菇。所以SPI的核心思想就是解耦,這在模塊化設計中尤其重要芽卿。

SPI使用示例

SPI使用方法并不復雜揭芍,只需要簡單的3步就可以搞定。

  • 定義一個接口
  • 提供方''META-INF/services''目錄下新建一個名稱為接口全限定名的文件卸例,內(nèi)容為接口實現(xiàn)類的全限定名称杨。
  • 調(diào)用方通過ServiceLoader.load方法加載接口的實現(xiàn)類實例

下面通過一個例子來具體演示下Java SPI是如何使用的,項目的工程結(jié)構(gòu)如下:

(1) 「spi-api」項目

  • 定義接口類筷转,SpiService.java:
package com.gallenzhang.spi;

/**
 * @author : zhangxq
 * @date : 2019/01/25
 * @description :
 */
public interface SpiService {

    void sayHello(String name);
}

(2) 「spi-service-a」項目

  • SpiService實現(xiàn)類SpiServiceImplA.java
package com.gallenzhang.spi.service;

import com.gallenzhang.spi.SpiService;

/**
 * @author : zhangxq
 * @date : 2019/01/25
 * @description :
 */
public class SpiServiceImplA implements SpiService {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "! from service-a");
    }
}

  • resources下創(chuàng)建 "META-INF/service"目錄姑原,并在該目錄下新建com.gallenzhang.spi.SpiService文件,文件內(nèi)容為:
com.gallenzhang.spi.service.SpiServiceImplA

「spi-service-b」項目與「spi-service-a」項目類似呜舒,這里就不再贅述锭汛。

(3) 「spi-application」項目

  • SpiMain.java
package com.gallenzhang.spi;

import java.sql.SQLException;
import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * @author : zhangxq
 * @date : 2019/01/25
 * @description :Spi測試類
 */
public class SpiMain {
    public static void main(String[] args) throws SQLException {
        ServiceLoader<SpiService> loadedParsers = ServiceLoader.load(SpiService.class);
        Iterator<SpiService> iterator = loadedParsers.iterator();
        while (iterator.hasNext()){
            SpiService spiService = iterator.next();
            spiService.sayHello("gallenzhang");
        }
    }
}

(4) 運行結(jié)果如圖:



可以看到通過Java SPI成功的加載到了「spi-service-a」和「spi-service-b」中接口的實現(xiàn)類

應用場景

比較常見的應用場景:
JDK提供一個數(shù)據(jù)庫驅(qū)動接口類,JDBC加載不同的數(shù)據(jù)庫驅(qū)動實現(xiàn)類

日志門面接口實現(xiàn)類加載袭蝗,SLF4J加載不同廠商提供的日志實現(xiàn)類店乐。

這里以JDBC為例,看看SPI是如何自動加載驅(qū)動的呻袭。下面一段代碼大家都應該很熟悉了,首先加載驅(qū)動程序腺兴,然后獲取數(shù)據(jù)庫連接左电。

//加載驅(qū)動程序
//Class.forName("com.mysql.jdbc.Driver");
//獲取數(shù)據(jù)庫連接
Connection conn = DriverManager.getConnection(url, user, password);

這里首先要說明一下,使用mysql-connector-java連接數(shù)據(jù)庫页响,在5.1.6之前的版本都需要加上Class.forName("com.mysql.jdbc.Driver"); 但是從5.1.6版本以及后面的版本篓足,這句代碼就可以去掉了。這是為什么呢闰蚕?下面通過代碼來一探究竟栈拖。

public class DriverManager {
    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    
    private DriverManager(){}

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    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;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        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;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        registerDriver(driver, null);
    }

    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);
    }
    
    @CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        if (user != null) {
            info.put("user", user);
        }
        if (password != null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

    //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
}

DriverManager.getConnection(url, user, password); 執(zhí)行時首先執(zhí)行靜態(tài)代碼塊,會調(diào)用loadInitialDrivers(); 這個方法能清楚看到這兒有段代碼没陡,就是通過Java的SPI加載Driver接口的所有實例涩哟,并將實例初始化。mysql-connector-java 包中META-INF/services目錄下有個名為java.sql.Driver的文件盼玄,內(nèi)容就是Driver接口的實現(xiàn)類贴彼。
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
        while(driversIterator.hasNext()) {
          driversIterator.next();
        }
  } catch(Throwable t) {
        // Do nothing
  }

當Driver 和 FabricMySQLDriver實例化的時候,會先執(zhí)行靜態(tài)代碼塊埃儿,向DriverManager注冊一個自己的實例器仗,在DriverManager中注冊的驅(qū)動信息都保存在registeredDrivers中。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

DriverManager.getConnection方法真正調(diào)用的時候,就是遍歷registeredDrivers 中驅(qū)動信息精钮,找到可以使用的驅(qū)動威鹿,拿到數(shù)據(jù)庫連接轨香。

這里通過SPI機制成功的進行解耦忽你,代碼中不再強制指定使用哪個驅(qū)動實現(xiàn),而是將裝配的控制權移到了程序外弹沽,成功的做到了業(yè)務代碼和與第三方裝配邏輯分離檀夹。

優(yōu)缺點

優(yōu)點:使用Java SPI機制的優(yōu)勢是實現(xiàn)了解耦,使第三方模塊的裝配邏輯與業(yè)務代碼分離策橘。應用程序可以根據(jù)實際業(yè)務情況使用新的框架拓展或者替換原有組件炸渡。

缺點:ServiceLoader在加載實現(xiàn)類的時候會全部加載并實例化,假如不想使用某些實現(xiàn)類丽已,它也會被加載示例化的蚌堵,這就造成了浪費。另外獲取某個實現(xiàn)類只能通過迭代器迭代獲取沛婴,不能根據(jù)某個參數(shù)來獲取吼畏,使用方式上不夠靈活。

Dubbo框架中大量使用了SPI來進行框架擴展嘁灯,但它是重新對SPI進行了實現(xiàn)泻蚊,完美的解決上面提到的問題。

示例代碼

github地址:spi-parent

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丑婿,一起剝皮案震驚了整個濱河市性雄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌羹奉,老刑警劉巖秒旋,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诀拭,居然都是意外死亡迁筛,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門耕挨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來细卧,“玉大人,你說我怎么就攤上這事筒占【频椋” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵赋铝,是天一觀的道長插勤。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么农尖? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任析恋,我火速辦了婚禮,結(jié)果婚禮上盛卡,老公的妹妹穿的比我還像新娘助隧。我一直安慰自己,他們只是感情好滑沧,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布并村。 她就那樣靜靜地躺著,像睡著了一般滓技。 火紅的嫁衣襯著肌膚如雪哩牍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天令漂,我揣著相機與錄音膝昆,去河邊找鬼。 笑死叠必,一個胖子當著我的面吹牛荚孵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纬朝,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼收叶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了共苛?” 一聲冷哼從身側(cè)響起判没,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俄讹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绕德,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡患膛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年鲸睛,在試婚紗的時候發(fā)現(xiàn)自己被綠了挥等。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搅窿。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡喇肋,死狀恐怖撤奸,靈堂內(nèi)的尸體忽然破棺而出跨琳,到底是詐尸還是另有隱情枷颊,我是刑警寧澤暇仲,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布夺蛇,位于F島的核電站疚漆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜娶聘,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一闻镶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丸升,春花似錦铆农、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至夷狰,卻和暖如春岭皂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背孵淘。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工蒲障, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘫证。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓揉阎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親背捌。 傳聞我的和親對象是個殘疾皇子毙籽,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 最近在閱讀Dubbo框架源代碼時,經(jīng)痴鼻欤看到@Spi,查了一下SPI: Service Provider Inter...
    kobe0429閱讀 313評論 0 0
  • SPI的概念 英文全稱為Service Provider Interface 是JDK內(nèi)置的一種服務提供發(fā)現(xiàn)機制 ...
    孫先森不可不弘毅閱讀 241評論 0 0
  • 當服務的提供者坑赡,提供了服務接口的一種實現(xiàn)之后,在jar包的META-INF/services/目錄里同時創(chuàng)建一個以...
    男人三餅閱讀 267評論 0 2
  • 本文通過探析JDK提供的么抗,在開源項目中比較常用的Java SPI機制毅否,希望給大家在實際開發(fā)實踐、學習開源項目提供參...
    caison閱讀 125,723評論 25 156
  • 本文通過探析JDK提供的蝇刀,在開源項目中比較常用的Java SPI機制螟加,希望給大家在實際開發(fā)實踐、學習開源項目提供參...
    簡祥閱讀 1,130評論 0 0