了解一下Java SPI的原理

了解一下Java SPI的原理

1 為什么寫這篇文章含懊?

近期,本人在學(xué)習(xí)dubbo相關(guān)的知識,但是在dubbo官網(wǎng)中有提到Java的 SPI,這個名詞之前未接觸過蚊伞,所以就去看了看,感覺還是有很多地方有使用的吮铭,比如jdbc、log相關(guān)的技術(shù)上均有使用颅停,還是很有用處的谓晌,就在這里總結(jié)一下自己的學(xué)習(xí)內(nèi)容!(本文有參考相關(guān)資料:比如dubbo官網(wǎng)癞揉、相關(guān)blog等)

2 SPI是什么纸肉?

Java SPI(Service Provider Interface)是JDK內(nèi)置的一種動態(tài)加載擴(kuò)展點的實現(xiàn)。在ClassPath的META-INF/services目錄下放置一個與接口同名的文本文件喊熟,文件的內(nèi)容為接口的實現(xiàn)類柏肪,多個實現(xiàn)類用換行符分隔。JDK中使用java.util.ServiceLoader來加載具體的實現(xiàn)芥牌。

Java SPI 實際上是“基于接口的編程+策略模式+配置文件”組合實現(xiàn)的動態(tài)加載機(jī)制烦味。

3 自定義一個SPI

3.1 創(chuàng)建工程

創(chuàng)建dubbo-spi的工程,這里展示一下完整的spi示例程序結(jié)構(gòu):

3.2 創(chuàng)建接口

在包top.flygrk.ishare.spi.service下創(chuàng)建接口: SPIService

package top.flygrk.ishare.spi.service;

/**
 * @Package top.flygrk.ishare.spi.service
 * @Version V1.0
 * @Description: SPIService 接口
 */
public interface SPIService {
    /**
     * 接口方法: say()
     */
    String say();
}

3.3 創(chuàng)建實現(xiàn)類: ASPIServiceImpl和BSPIServiceImpl

在包top.flygrk.ishare.spi.service.impl下創(chuàng)建ASPIServiceImpl和BSPIServiceImpl類壁拉,均實現(xiàn)SPIservice接口:

  • ASPIServiceImpl
package top.flygrk.ishare.spi.service.impl;

import top.flygrk.ishare.spi.service.SPIService;

/**
 * @Package top.flygrk.ishare.spi.service.impl
 * @Version V1.0
 * @Description: SPIService 實現(xiàn)類 ASPIServiceImpl
 */
public class ASPIServiceImpl implements SPIService {
    @Override
    public String say() {
        return "ASPIServiceImpl";
    }
}

  • BSPIServiceImpl
package top.flygrk.ishare.spi.service.impl;

import top.flygrk.ishare.spi.service.SPIService;

/**
 * @Package top.flygrk.ishare.spi.service.impl
 * @Version V1.0
 * @Description: SPIService 實現(xiàn)類 BSPIServiceImpl
 */
public class BSPIServiceImpl implements SPIService {
    @Override
    public String say() {
        return "BSPIServiceImpl";
    }
}

3.4 創(chuàng)建文件top.flygrk.ishare.spi.service.SPIService

在resource目錄下谬俄,創(chuàng)建META-INF/services目錄,并在該目錄下創(chuàng)建top.flygrk.ishare.spi.service.SPIService文件(該文件名為接口的全路徑弃理,需保持一致)溃论,并在該文件中配置兩個實現(xiàn)類的全路徑:

top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
top.flygrk.ishare.spi.service.impl.BSPIServiceImpl

3.5 創(chuàng)建測試類TestSPIService

在包top.flygrk.ishare.demo下創(chuàng)建TestSPIService類,用于測試該SPI服務(wù)

package top.flygrk.ishare.demo;

import top.flygrk.ishare.spi.service.SPIService;

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

/**
 * @Package top.flygrk.ishare.demo
 * @Version V1.0
 * @Description: 測試 SPIService
 */
public class TestSPIService {

    public static void main(String[] args) {
        // ServiceLoader實現(xiàn)了Iterable接口痘昌,可以遍歷出所有的服務(wù)實現(xiàn)者
        ServiceLoader<SPIService> serviceLoaders = ServiceLoader.load(SPIService.class);

        /*
         * 方法1: 迭代器
         */
        Iterator<SPIService> spiServiceIterator = serviceLoaders.iterator();
        while (spiServiceIterator != null && spiServiceIterator.hasNext()) {
            SPIService spiService = spiServiceIterator.next();
            System.out.println(spiService.getClass().getName() + " : " + spiService.say());
        }


        /*
         * 迭代方法2: foreach
         */
//        for (SPIService spiService : serviceLoaders) {
//            System.out.println(spiService.getClass().getName() + " : " + spiService.say());
//        }

    }

}

3.6 測試類運行結(jié)果

top.flygrk.ishare.spi.service.impl.ASPIServiceImpl : ASPIServiceImpl
top.flygrk.ishare.spi.service.impl.BSPIServiceImpl : BSPIServiceImpl

4 SPI原理分析

在我們閱讀源碼前钥勋,我們先提出以下幾個問題炬转,然后我們再去帶著問題去源碼中找答案:

    1. META-INF/services目錄下的文件有什么用?為什么要用接口的全路徑命名算灸?是否可以更改接口名稱扼劈?里面的內(nèi)容為什么要用實現(xiàn)類的全路徑?
  • 2) ServiceLoader 是如何獲取到SPIService的全部實現(xiàn)的乎婿?
  • 3) 如果我們只想取ASPIServiceImpl测僵,并不想去操作BSPIServiceImpl,如何去操作谢翎?

4.1 ServiceLoader結(jié)構(gòu)

我們先看一下ServiceLoader類的結(jié)構(gòu):

進(jìn)入ServiceLoader類的源碼捍靠,我們可以看到以下定義的一些常量:

各位肯定注意到了一點: private static final String PREFIX = "META-INF/services/";, 這個PREFIX后面的路徑不正是我們在上述示例中創(chuàng)建和接口保持一致的文件的目錄嗎森逮?還有services榨婆、loader、acc褒侧、lookupIterator和providers表達(dá)的意思在源碼上方的注釋中也進(jìn)行了描述良风,下面我將各個屬性的釋義標(biāo)注一下:

// 配置文件的目錄
private static final String PREFIX = "META-INF/services/";
// 要加載服務(wù)的類或者接口
// The class or interface representing the service being loaded
private final Class<S> service;
// 服務(wù)加載器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 訪問控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 服務(wù)實例的緩存
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懶加載的迭代器
// The current lazy-lookup iterator
private LazyIterator lookupIterator;

4.2 ServiceLoader的加載過程

看完了上面ServiceLoader的結(jié)構(gòu),下面我們再來看看ServiceLoader是如何一步步加載的闷供。我們在TestSPIService類上的main方法第一行打上斷點:

然后使用debug的方式調(diào)試烟央,進(jìn)入ServiceLoader的源碼,會依次進(jìn)入以下幾個函數(shù):

經(jīng)過這些步驟之后歪脏,serviceLoader內(nèi)部包含有一個Iterator迭代器疑俭,下面我們來仔細(xì)看一下這個迭代器的作用!

4.3 迭代器lookupIterator的操作

在上述4.2步驟加載完成之后婿失,serviceLoader內(nèi)的lookupIterator的內(nèi)容如下:

然后使用iterator()方法獲取Iterator迭代器時钞艇,執(zhí)行如下的程序:

file

在經(jīng)過上述過程之后,我們拿到了Iterator迭代器豪硅,這時我們看下spiServiceIterator的內(nèi)容:

是不是很奇怪哩照,還是只有SPIService,不要忘記了懒浮,他內(nèi)部的迭代器可是懶加載的飘弧!我們繼續(xù)跟進(jìn)代碼,進(jìn)入到hasNext()方法嵌溢。

從上面可以知道眯牧,acc一直為null的,所以這時候赖草,他進(jìn)入了hasNextService()方法:

重頭戲來了学少,我們可以看到其中的 PREFIX, 這個內(nèi)容就是我們配置的文件秧骑。再仔細(xì)的跟進(jìn)代碼版确,我們會進(jìn)入到parse()方法扣囊,該方法用于按照行讀取出文件中的內(nèi)容,并保存到Iterator<String> 中绒疗。

故而侵歇,再通過 nextName = pending.next(); 執(zhí)行后,獲取到top.flygrk.ishare.spi.service.impl.ASPIServiceImpl吓蘑,繼而進(jìn)行后續(xù)的next()方法操作惕虑。

然后進(jìn)入到nextService()方法:

再nextService()方法里,使用了反射的技術(shù)磨镶,根據(jù)前面從文件中讀取到的實現(xiàn)類全路徑top.flygrk.ishare.spi.service.impl.ASPIServiceImpl獲取到該實現(xiàn)類的對象溃蔫!走到這里,也就基本上了解了SPI琳猫,但是我們能只獲取ASPIServiceImpl伟叛,而不去獲取BSPIServiceImpl嗎?對不起脐嫂,這里不允許這樣统刮,只能通過迭代器遍歷出所有的內(nèi)容!除非人為干預(yù)(外層循環(huán)比對完成之后退出循環(huán))账千。接下來的步驟就和前面幾乎一致了侥蒙,這里不再細(xì)述~

5 SPI 優(yōu)缺點

我們評價一門思想往往需要從其優(yōu)缺點的方向進(jìn)行考慮。SPI同樣也是有一定的優(yōu)缺點存在的匀奏,下面我們來仔細(xì)的看下它有哪些優(yōu)缺點:

5.1 優(yōu)點

  • 解耦:最大的優(yōu)點也就是解耦了辉哥,通過SPI可以使第三方服務(wù)模塊的邏輯與業(yè)務(wù)代碼相分離,而不耦合在一起攒射。應(yīng)用程序可以根據(jù)實際業(yè)務(wù)進(jìn)行擴(kuò)展。

5.2 缺點

參考dubbo官方文檔

  • 需要遍歷所有的實現(xiàn)恒水,并實例化会放,然后我們在循環(huán)中才能找到我們需要的實現(xiàn)。
  • 配置文件中只是簡單的列出了所有的擴(kuò)展實現(xiàn)钉凌,而沒有給他們命名咧最。導(dǎo)致在程序中很難去準(zhǔn)確的引用它們。
  • 擴(kuò)展如果依賴其他的擴(kuò)展御雕,做不到自動注入和裝配
  • 不提供類似于Spring的IOC和AOP功能
  • 擴(kuò)展很難和其他的框架集成矢沿,比如擴(kuò)展里面依賴了一個Spring bean,原生的Java SPI不支持

6 SPI案例分析

在我們常用的框架中酸纲,有很多都是有使用SPI的方式捣鲸,其中包括JDBC加載不同類型數(shù)據(jù)庫的驅(qū)動、SLF4J加載不同提供商的日志實現(xiàn)類闽坡、Spring 框架栽惶、Dubbo框架愁溜。

這里需要注意,dubbo框架的SPI是對原生的Java SPI 進(jìn)行了擴(kuò)展的外厂。關(guān)于dubbo的SPI我們將在后面詳細(xì)講解∶嵯螅現(xiàn)在,我們來以JDBC加載的方式來簡單的看看其SPI的方式汁蝶。

我們先找到mysql的包渐扮,其結(jié)構(gòu)如下:

在META-INF/services 目錄下,存在 文件 java.sql.Driver掖棉,其內(nèi)容為:

通過這個路徑墓律,我們也可以找到 com.mysql.jdbc.Driver類,它實現(xiàn)了java.sql.Driver接口:

諸如Oracle啊片,同樣也有此機(jī)制只锻,這里就不再細(xì)述了,請自行驗證查看~


Blog:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末紫谷,一起剝皮案震驚了整個濱河市齐饮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌笤昨,老刑警劉巖祖驱,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瞒窒,居然都是意外死亡捺僻,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門崇裁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匕坯,“玉大人,你說我怎么就攤上這事拔稳「鹁” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵巴比,是天一觀的道長术奖。 經(jīng)常有香客問我,道長轻绞,這世上最難降的妖魔是什么采记? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮政勃,結(jié)果婚禮上唧龄,老公的妹妹穿的比我還像新娘。我一直安慰自己奸远,他們只是感情好选侨,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布掖鱼。 她就那樣靜靜地躺著,像睡著了一般援制。 火紅的嫁衣襯著肌膚如雪戏挡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天晨仑,我揣著相機(jī)與錄音褐墅,去河邊找鬼。 笑死洪己,一個胖子當(dāng)著我的面吹牛妥凳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播答捕,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼逝钥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拱镐?” 一聲冷哼從身側(cè)響起艘款,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沃琅,沒想到半個月后哗咆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡益眉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年晌柬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片郭脂。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡年碘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出展鸡,到底是詐尸還是另有隱情盛泡,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布娱颊,位于F島的核電站,受9級特大地震影響凯砍,放射性物質(zhì)發(fā)生泄漏箱硕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一悟衩、第九天 我趴在偏房一處隱蔽的房頂上張望剧罩。 院中可真熱鬧,春花似錦座泳、人聲如沸惠昔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镇防。三九已至啦鸣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間来氧,已是汗流浹背诫给。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留啦扬,地道東北人中狂。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像扑毡,于是被迫代替她去往敵國和親胃榕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

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