深入理解Java中SPI機(jī)制

背景

說到Java SPI,事情得追溯到數(shù)月前。那天打開webp-imageio-core的源碼準(zhǔn)備稍微看下屠凶,點(diǎn)開項(xiàng)目第一眼就看到了WebPImageWriterSpi.java遮咖,起初是對這個(gè)類的命名有點(diǎn)疑惑,為啥叫Spi而不叫Interface呢昵宇,帶著疑惑去問下了度娘磅崭,才了解到原來SPI是JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制。

簡介

SPI(Service Provider Interface)瓦哎,是JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制砸喻,可以用來啟用框架擴(kuò)展和替換組件柔逼,主要是被框架的開發(fā)人員使用,比如java.sql.Driver接口割岛,其他不同廠商可以針對同一接口做出不同的實(shí)現(xiàn)愉适,mysql和postgresql都有不同的實(shí)現(xiàn)提供給用戶,而Java的SPI機(jī)制可以為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)癣漆。Java中SPI機(jī)制主要思想是將裝配的控制權(quán)移到程序之外维咸,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要,其核心思想就是解耦惠爽。

SPI與API區(qū)別

  • API是您調(diào)用并用于實(shí)現(xiàn)目標(biāo)的類/接口/方法/ 等等的描述癌蓖;
  • SPI是您擴(kuò)展和實(shí)現(xiàn)以實(shí)現(xiàn)目標(biāo)的類/接口/方法/等等的描述;
    換句話說婚肆,API告訴您特定的類/方法為您做了什么费坊,SPI告訴您必須做什么來符合。

參考: https://stackoverflow.com/questions/2954372/difference-between-spi-and-api?answertab=votes#tab-top

SPI整體機(jī)制圖如下:

165d5e597615e978.jpg

當(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í)候两残,就可以通過查找這個(gè)jar包(一般都是以jar包做依賴)的META-INF/services/中的配置文件永毅,配置文件中有接口的具體實(shí)現(xiàn)類名,可以根據(jù)這個(gè)類名進(jìn)行加載實(shí)例化人弓,就可以使用該服務(wù)了沼死。JDK中查找服務(wù)的實(shí)現(xiàn)的工具類是:java.util.ServiceLoader

應(yīng)用場景

SPI擴(kuò)展機(jī)制應(yīng)用場景有很多崔赌,比如common-logging意蛀,jdbcdubbo等等健芭。
SPI流程: ①有關(guān)組織和公式定義接口標(biāo)準(zhǔn) ②第三方提供具體實(shí)現(xiàn): 實(shí)現(xiàn)具體方法, 配置 META-INF/services/${interface_name} 文件 ③開發(fā)者使用
比如jdbc場景下:

  • 首先在java中定義了接口java.sql.Driver县钥,并沒有具體的實(shí)現(xiàn),具體的實(shí)現(xiàn)都是由不同廠商來提供的慈迈。
  • 在mysql的jar包mysql-connector-java-6.0.6.jar中若贮,可以找到META-INF/services目錄,該目錄下會有一個(gè)名字為java.sql.Driver的文件痒留,文件內(nèi)容是com.mysql.cj.jdbc.Driver谴麦,這里面的內(nèi)容就是針對Java中定義的接口的實(shí)現(xiàn)。
  • 同樣在postgresql的jar包postgresql-42.0.0.jar中伸头,也可以找到同樣的配置文件匾效,文件內(nèi)容是org.postgresql.Driver,這是postgresql對Java的java.sql.Driver的實(shí)現(xiàn)恤磷。

使用demo

  • 定義一個(gè)接口HelloSPI面哼。
package com.vivo.study.spidemo.spi;
public interface HelloSPI {
    void sayHello();
}
  • 完成接口的多個(gè)實(shí)現(xiàn)雪侥。
package com.vivo.study.spidemo.spi.impl;
import com.vivo.study.spidemo.spi.HelloSPI;
public class ImageHello implements HelloSPI {
    public void sayHello() {
        System.out.println("Image Hello");
    }
}
package com.vivo.study.spidemo.spi.impl;
import com.vivo.study.spidemo.spi.HelloSPI;
public class TextHello implements HelloSPI {
    public void sayHello() {
        System.out.println("Text Hello");
    }
}
  • META-INF/services/目錄里創(chuàng)建一個(gè)以com.vivo.study.spidemo.spi.HelloSPI的文件,這個(gè)文件里的內(nèi)容就是這個(gè)接口的具體的實(shí)現(xiàn)類精绎。
    image.png

具體內(nèi)容如下:

com.vivo.study.spidemo.spi.impl.ImageHello
com.vivo.study.spidemo.spi.impl.TextHello
  • 使用 ServiceLoader 來加載配置文件中指定的實(shí)現(xiàn)
package com.vivo.study.spidemo.test;
import java.util.ServiceLoader;
import com.vivo.study.spidemo.spi.HelloSPI;
public class SPIDemo {
    public static void main(String[] args) {
        ServiceLoader<HelloSPI> serviceLoader = ServiceLoader.load(HelloSPI.class);
        // 執(zhí)行不同廠商的業(yè)務(wù)實(shí)現(xiàn)速缨,具體根據(jù)業(yè)務(wù)需求配置
        for (HelloSPI helloSPI : serviceLoader) {
            helloSPI.sayHello();
        }
    }
}

輸出結(jié)果如下:

Image Hello
Text Hello

源碼分析

// ServiceLoader實(shí)現(xiàn)了Iterable接口,可以遍歷所有的服務(wù)實(shí)現(xiàn)者
public final class ServiceLoader<S> implements Iterable<S>
{
    // 查找配置文件的目錄
    private static final String PREFIX = "META-INF/services/";
    // 表示要被加載的服務(wù)的類或接口
    private final Class<S> service;
    // 這個(gè)ClassLoader用來定位代乃,加載旬牲,實(shí)例化服務(wù)提供者
    private final ClassLoader loader;
    // 訪問控制上下文
    private final AccessControlContext acc;
    // 緩存已經(jīng)被實(shí)例化的服務(wù)提供者,按照實(shí)例化的順序存儲
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 迭代器
    private LazyIterator lookupIterator;  
}
// 服務(wù)提供者查找的迭代器
public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        // hasNext方法
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
        // next方法
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
    };
}
// 服務(wù)提供者查找的迭代器
private class LazyIterator implements Iterator<S> {
    // 服務(wù)提供者接口
    Class<S> service;
    // 類加載器
    ClassLoader loader;
    // 保存實(shí)現(xiàn)類的url
    Enumeration<URL> configs = null;
    // 保存實(shí)現(xiàn)類的全名
    Iterator<String> pending = null;
    // 迭代器中下一個(gè)實(shí)現(xiàn)類的全名
    String nextName = null;

    public boolean hasNext() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

    public S next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,"Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service, "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service, "Provider " + cn + " could not be instantiated: " + x, x);
        }
        throw new Error();          // This cannot happen
    }
}
  • 首先搁吓,ServiceLoader實(shí)現(xiàn)了Iterable接口原茅,所以他有迭代器的屬性,這里主要都是實(shí)現(xiàn)了迭代器的hasNext和next方法這里主要都是調(diào)用的lookupIterator的相應(yīng)hasNext和next方法堕仔,lookupIterator是懶加載迭代器擂橘。
  • 其次,LazyIterator中的hasNext方法摩骨,靜態(tài)變量PREFIX就是”META-INF/services/”目錄通贞,這也就是為什么需要在classpath下的META-INF/services/目錄里創(chuàng)建一個(gè)以服務(wù)接口命名的文件。
  • 最后恼五,通過反射方法Class.forName()加載類對象昌罩,并用newInstance方法將類實(shí)例化,并把實(shí)例化后的類緩存到providers對象中灾馒,(LinkedHashMap<String,S>類型) 然后返回實(shí)例對象茎用。

不足

  • 不能按需加載,需要遍歷所有的實(shí)現(xiàn)睬罗,并實(shí)例化轨功,然后我們在循環(huán)中才能找到我們需要的實(shí)現(xiàn)。如果你不想用某些實(shí)現(xiàn)類容达,或者某些類實(shí)例化很耗時(shí)古涧,它也被載入并實(shí)例化了,這就造成了浪費(fèi)董饰。
  • 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活蒿褂,只能通過 Iterator 形式獲取圆米,不能根據(jù)某個(gè)參數(shù)來獲取對應(yīng)的實(shí)現(xiàn)類卒暂。
  • 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。

規(guī)避

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末近速,一起剝皮案震驚了整個(gè)濱河市诈嘿,隨后出現(xiàn)的幾起案子堪旧,更是在濱河造成了極大的恐慌,老刑警劉巖奖亚,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淳梦,死亡現(xiàn)場離奇詭異,居然都是意外死亡昔字,警方通過查閱死者的電腦和手機(jī)爆袍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來作郭,“玉大人陨囊,你說我怎么就攤上這事〖性埽” “怎么了蜘醋?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長咏尝。 經(jīng)常有香客問我压语,道長,這世上最難降的妖魔是什么编检? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任无蜂,我火速辦了婚禮,結(jié)果婚禮上蒙谓,老公的妹妹穿的比我還像新娘斥季。我一直安慰自己,他們只是感情好累驮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布酣倾。 她就那樣靜靜地躺著,像睡著了一般谤专。 火紅的嫁衣襯著肌膚如雪躁锡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天置侍,我揣著相機(jī)與錄音映之,去河邊找鬼。 笑死蜡坊,一個(gè)胖子當(dāng)著我的面吹牛杠输,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播秕衙,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蠢甲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了据忘?” 一聲冷哼從身側(cè)響起鹦牛,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤搞糕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后曼追,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窍仰,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年礼殊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辈赋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡膏燕,死狀恐怖钥屈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情坝辫,我是刑警寧澤篷就,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站近忙,受9級特大地震影響竭业,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜及舍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一未辆、第九天 我趴在偏房一處隱蔽的房頂上張望西轩。 院中可真熱鬧师崎,春花似錦、人聲如沸通孽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至歼郭,卻和暖如春遗契,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背病曾。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工牍蜂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泰涂。 一個(gè)月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓鲫竞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親负敏。 傳聞我的和親對象是個(gè)殘疾皇子贡茅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

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

  • SPI,Service Provider Interface其做,主要是被框架的開發(fā)人員使用顶考,比如java.sql.D...
    加大裝益達(dá)閱讀 1,692評論 2 12
  • SPI簡介 如何使用SPI 應(yīng)用舉例1. 組織方制定接口2. 實(shí)現(xiàn)方根據(jù)SPI規(guī)范實(shí)現(xiàn)接口3. 組織方加載實(shí)現(xiàn)類 ...
    齊晉閱讀 901評論 0 5
  • 本文通過探析JDK提供的,在開源項(xiàng)目中比較常用的Java SPI機(jī)制妖泄,希望給大家在實(shí)際開發(fā)實(shí)踐驹沿、學(xué)習(xí)開源項(xiàng)目提供參...
    簡祥閱讀 1,130評論 0 0
  • 本文通過探析JDK提供的,在開源項(xiàng)目中比較常用的Java SPI機(jī)制蹈胡,希望給大家在實(shí)際開發(fā)實(shí)踐渊季、學(xué)習(xí)開源項(xiàng)目提供參...
    caison閱讀 125,732評論 25 156
  • 主人身病常在臥 不問家事已多年 一朝病愈重登堂 豪奴悍婢竟當(dāng)權(quán) 胡須一捋手拍案 大罵三聲汝豈敢 宵小奴婢跪堂前 三...
    S無邪閱讀 276評論 0 3