Android ServiceLoader使用詳解和源碼分析

一踊兜、SPI(Service Provider Interface)

在介紹ServiceLoader之前,需要先說下SPI (Service Provider Interface)這個(gè)概念跋选。

SPI屬于動(dòng)態(tài)加載接口實(shí)現(xiàn)類的的一項(xiàng)技術(shù)锁孟,是JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制,使用ServiceLoader去加載接口對(duì)應(yīng)的實(shí)現(xiàn)仿贬,這樣我們就不用關(guān)注實(shí)現(xiàn)類纽竣,ServiceLoader會(huì)告訴我們。官方文檔描述為:為某個(gè)接口尋找服務(wù)的機(jī)制茧泪,類似IOC思想蜓氨,將裝配的控制權(quán)交給ServiceLoader。

使用場(chǎng)景

只提供服務(wù)接口队伟,具體服務(wù)由其他組件實(shí)現(xiàn)穴吹,接口和具體實(shí)現(xiàn)分離(類似橋接),同時(shí)能夠通過系統(tǒng)的ServiceLoader拿到這些實(shí)現(xiàn)類的集合嗜侮,統(tǒng)一處理港令,這樣在組件化中往往會(huì)帶來很多便利,SPI機(jī)制可以實(shí)現(xiàn)不同模塊之間方便的面向接口編程锈颗,拒絕了硬編碼的方式顷霹,解耦效果很好。

例如有如下工程結(jié)構(gòu)的項(xiàng)目:

image.png

場(chǎng)景:如果想在組件1中使用主工程或組件1使用組件2的方法或變量击吱,如何實(shí)現(xiàn)

  1. 常見方式泼返,在組件1聲明一些接口,由主工程實(shí)現(xiàn)姨拥。然后在初始化組件1的時(shí)候绅喉,通過注入方式傳遞給組件1。
  2. ServiceLoader方式叫乌,如果都通過依賴注入的方式柴罐,組件間耦合較重。ServiceLoader方式也是組件1聲明接口憨奸,主工程實(shí)現(xiàn)革屠。但是無須注入,這部分工作由ServiceLoader自動(dòng)實(shí)現(xiàn)排宰。

二似芝、使用方式

ServiceLoader的使用流程遵循:服務(wù)約定 -> 服務(wù)實(shí)現(xiàn) -> 服務(wù)注冊(cè) -> 服務(wù)發(fā)現(xiàn)/使用。
首先約定幾個(gè)概念名詞板甘,并且后文中党瓮,以這些名詞行文。
概念說明備注服務(wù)接口或(通常是)抽象類出于加載的目的盐类,服務(wù)由單一類型表示寞奸,即單一接口或抽象類呛谜。 (可以使用具體類,但不建議這樣做枪萄。)服務(wù)提供者服務(wù)(接口和抽象類)的具體實(shí)現(xiàn)隐岛。服務(wù)提供者可以以擴(kuò)展形式引入,例如jar包瓷翻;也可以通過將它們添加到應(yīng)用程序的類路徑或通過其他一些特定于平臺(tái)的方式來提供聚凹。給定服務(wù)提供者包含一個(gè)或多個(gè)具體類,這些類使用特定于提供者的數(shù)據(jù)和代碼擴(kuò)展該服務(wù)類型齐帚。 此工具強(qiáng)制執(zhí)行的唯一要求是提供程序類必須具有零參數(shù)構(gòu)造函數(shù)妒牙,以便它們可以在加載期間實(shí)例化。

  1. 服務(wù)約定
    定義好接口或抽象類作為服務(wù)

  2. 服務(wù)實(shí)現(xiàn)
    實(shí)現(xiàn)定義好的服務(wù)童谒,由于ServiceLoader可以更方便不同組件間通信,高度解耦沪羔。所以更常見的場(chǎng)景是服務(wù)可能是定義在底層組件或引入jar包饥伊,在上層業(yè)務(wù)代碼中具體實(shí)現(xiàn)。

  3. 服務(wù)注冊(cè)
    約定和實(shí)現(xiàn)了服務(wù)后蔫饰,需要對(duì)服務(wù)進(jìn)行注冊(cè)琅豆,系統(tǒng)才能定位到該服務(wù)。注冊(cè)方式是在java同級(jí)目錄篓吁,創(chuàng)建一個(gè)resources/META-INF/services的目錄茫因,在該目錄下,以服務(wù)的全限定名創(chuàng)建一個(gè)SPI描述文件杖剪。目錄層級(jí)圖如下:

    image.png

    有了該文件冻押,即可將服務(wù)提供者(接口實(shí)現(xiàn)類)的全限定名分行寫入該文件,即完成服務(wù)注冊(cè)盛嘿。
    PS. 注冊(cè)目錄路徑是固定洛巢,至于為什么后,下文代碼部分將會(huì)說明次兆。

示例:

package com.example;
// 聲明服務(wù)
public interface IHello {
 String sayHello();
}
---------------------------------------------------------------
// 實(shí)現(xiàn)服務(wù)
public class Hello implements IHello{
 @Override
 public String sayHello(){
 System.out.println("hello, world");
 }
}

---------------------------------------------------------------
// 使用服務(wù)
ServiceLoader<Hello> loader = ServiceLoader.load(IHello.class);
mIterator =loader.iterator(); 
while(mIterator.hasNext()){
 mIterator.next().sayHello();
}

三稿茉、代碼邏輯

ServiceLoader成員變量說明

字段類型說明serviceClass<S>ServiceLoader加載的接口或抽象類loaderClassLoader類加載器providersLinkedHashMap<String,S>緩存加載的接口或抽象類(即service對(duì)象)lookupIteratorLazyIterator迭代器

3.1、ServiceLoader的創(chuàng)建

//ServiceLoader.class
 public static <S> ServiceLoader<S> load(Class<S> service,
 ClassLoader loader)
 {
 return new ServiceLoader<>(service, loader);
 }

 private ServiceLoader(Class<S> svc, ClassLoader cl) {
 service = Objects.requireNonNull(svc, "Service interface cannot be null");
 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
 // Android-changed: Do not use legacy security code.
 // On Android, System.getSecurityManager() is always null.
 // acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
 reload();
 }

 /**
 * Clear this loader's provider cache so that all providers will be
 * reloaded.
 *
 * <p> After invoking this method, subsequent invocations of the {@link
 * #iterator() iterator} method will lazily look up and instantiate
 * providers from scratch, just as is done by a newly-created loader.
 *
 * <p> This method is intended for use in situations in which new providers
 * can be installed into a running Java virtual machine.
 */
 public void reload() {
 providers.clear();
 lookupIterator = new LazyIterator(service, loader);
 }

可以看出ServiceLoader#load方法創(chuàng)建了ServiceLoader對(duì)象芥炭,并且初始化了service漓库、loader對(duì)象(含義見上表)。同時(shí)調(diào)用了reload方法清空了緩存园蝠,并且創(chuàng)建了LazyIterator對(duì)象渺蒿,用來遍歷加載的服務(wù)。

這個(gè)階段發(fā)現(xiàn)只是做了初始化工作彪薛,但并沒有加載注冊(cè)的服務(wù)蘸嘶,所以這是一個(gè)懶加載過程(LazyIterator的命名也透露了這點(diǎn))良瞧。

3.2、服務(wù)的注冊(cè)

3.1小節(jié)中說了load方法只是做了一些初始化的工作训唱,并沒有注冊(cè)服務(wù)褥蚯。那么服務(wù)具體是在什么位置進(jìn)行注冊(cè)的呢?在使用時(shí)况增,會(huì)獲取ServiceLoader的迭代器來遍歷服務(wù)赞庶,看下ServiceLoader#iterator方法:

public Iterator<S> iterator() {
 return new Iterator<S>() {
 Iterator<Map.Entry<String,S>> knownProviders
 = providers.entrySet().iterator();

 public boolean hasNext() {
 if (knownProviders.hasNext())
 return true;
 return lookupIterator.hasNext();
 }

 public S next() {
 if (knownProviders.hasNext())
 return knownProviders.next().getValue();
 return lookupIterator.next();
 }

 public void remove() {
 throw new UnsupportedOperationException();
 }
 };
 }

可見就是創(chuàng)建了一個(gè)迭代器對(duì)象,實(shí)現(xiàn)了3個(gè)方法hasNext(用以判斷是否還有為遍歷的服務(wù))澳骤、next(獲取服務(wù))和remove歧强。在內(nèi)部用knownProviders緩存了已注冊(cè)服務(wù),每次調(diào)用hasNext或next方法時(shí)为肮,先從緩存中的服務(wù)中取摊册,沒有再調(diào)用lookupIterator的對(duì)應(yīng)方法

對(duì)于首次創(chuàng)建的情況颊艳,緩存中沒有注冊(cè)好的服務(wù)茅特,如果調(diào)用hasNext,就會(huì)調(diào)用lookupIterator.hasNext()棋枕,代碼如下

private class LazyIterator implements Iterator<S> {
 Class<S> service;
 ClassLoader loader;
 Enumeration<URL> configs = null;
 Iterator<String> pending = null;
 String nextName = null;
 public boolean hasNext() {
 return hasNextService();
 }

 private boolean hasNextService() {
 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;
 }

}

①:判斷nextName是否為null白修,表示下一個(gè)待注冊(cè)服務(wù)的全稱(目錄路徑+服務(wù)名),不為null表示有服務(wù)重斑,直接返回true兵睛;否則繼續(xù)執(zhí)行
②:configs保存所有指定名稱的資源,在這里就是我們聲明的resources/META-INF/services/<package name>文件窥浪。如果為null祖很,表示還未加載該資源文件。
③:構(gòu)造要加載的資源文件全名漾脂,PREFIX的值為:

這就是為什么我們要?jiǎng)?chuàng)建resources/META-INF/services目錄突琳,并在該目錄聲明一個(gè)注冊(cè)文件。

④:如果注冊(cè)成功了符相,configs變量就儲(chǔ)存了所有聲明的服務(wù)全名拆融。這一步才是真正注冊(cè)了所有服務(wù)的位置,懶加載就是在這里進(jìn)行的啊终。
⑤:首次加載镜豹,pending迭代器對(duì)象為null,進(jìn)入循環(huán)蓝牲。如果configs里沒有值趟脂,直接返回false,表明沒有可注冊(cè)的服務(wù)例衍。否則進(jìn)入⑥處
⑥:調(diào)用parse方法昔期,它打開注冊(cè)文件開始讀取文件內(nèi)容已卸,每讀到一行服務(wù)全名,如果它沒有緩存過(即不在providers里)硼一。就把它添加到一個(gè)列表中累澡。直到文件內(nèi)容全部讀取完成,最后返回該列表的迭代器對(duì)象般贼。

private Iterator<String> parse(Class<?> service, URL u)
 throws ServiceConfigurationError
 {
 InputStream in = null;
 BufferedReader r = null;
 ArrayList<String> names = new ArrayList<>();
 try {
 in = u.openStream();
 r = new BufferedReader(new InputStreamReader(in, "utf-8"));
 int lc = 1;
 while ((lc = parseLine(service, u, r, lc, names)) >= 0);
 } catch (IOException x) {
 fail(service, "Error reading configuration file", x);
 } finally {
 try {
 if (r != null) r.close();
 if (in != null) in.close();
 } catch (IOException y) {
 fail(service, "Error closing configuration file", y);
 }
 }
 return names.iterator();
 }

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
 List<String> names)
 throws IOException, ServiceConfigurationError
 {
 String ln = r.readLine();
 if (ln == null) {
 return -1;
 }
 int ci = ln.indexOf('#');
 if (ci >= 0) ln = ln.substring(0, ci);
 ln = ln.trim();
 int n = ln.length();
 if (n != 0) {
 if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
 fail(service, u, lc, "Illegal configuration-file syntax");
 int cp = ln.codePointAt(0);
 if (!Character.isJavaIdentifierStart(cp))
 fail(service, u, lc, "Illegal provider-class name: " + ln);
 for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
 cp = ln.codePointAt(i);
 if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
 fail(service, u, lc, "Illegal provider-class name: " + ln);
 }
 if (!providers.containsKey(ln) && !names.contains(ln))
 names.add(ln);
 }
 return lc + 1;
 }

3.3 服務(wù)的使用

通過3.2小節(jié)講解的步驟愧哟,現(xiàn)在服務(wù)已經(jīng)全部注冊(cè)了,可以獲取各個(gè)服務(wù)實(shí)例并使用了哼蛆。通常是通過next()方法獲取服務(wù)的實(shí)例對(duì)象蕊梧,代碼如下:

 public S next() {
 return nextService();
 }

 private S nextService() {
 if (!hasNextService())// ①
 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", x);
 }
 if (!service.isAssignableFrom(c)) {
 ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
 fail(service,"Provider " + cn  + " not a subtype", cce);
 }
 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);
 }
 throw new Error();          // This cannot happen
 }

①:沒有服務(wù),直接拋出異常
②:根據(jù)服務(wù)的全限定名字加載其class對(duì)象腮介。
③:調(diào)用newInstance創(chuàng)建服務(wù)的實(shí)例肥矢,并且將該實(shí)例類型裝換成聲明的服務(wù)類型(接口或抽象類)。
④:緩存一份叠洗,提升訪問效率甘改,避免每次都反射加載。
至此惕味,就拿到了服務(wù)的實(shí)例楼誓,使用者就可以通過該實(shí)例對(duì)象去調(diào)用各種實(shí)例方法了玉锌。

4名挥、總結(jié)

1、本文詳細(xì)說明了SPI的概念和ServiceLoader的具體用法主守,其使用包括服務(wù)約定 -> 服務(wù)實(shí)現(xiàn) -> 服務(wù)注冊(cè) -> 服務(wù)發(fā)現(xiàn)/使用等過程
2禀倔、通過代碼分析,說明了ServiceLoader的底層實(shí)現(xiàn)参淫,包括服務(wù)注冊(cè)的懶加載機(jī)制救湖、服務(wù)注冊(cè)為什么固定目錄以及服務(wù)使用時(shí)的hasNext()next()方法等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末涎才,一起剝皮案震驚了整個(gè)濱河市鞋既,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌耍铜,老刑警劉巖邑闺,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異棕兼,居然都是意外死亡陡舅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門伴挚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來靶衍,“玉大人灾炭,你說我怎么就攤上這事÷簦” “怎么了蜈出?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)帚呼。 經(jīng)常有香客問我掏缎,道長(zhǎng),這世上最難降的妖魔是什么煤杀? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任眷蜈,我火速辦了婚禮,結(jié)果婚禮上沈自,老公的妹妹穿的比我還像新娘酌儒。我一直安慰自己,他們只是感情好枯途,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布忌怎。 她就那樣靜靜地躺著,像睡著了一般酪夷。 火紅的嫁衣襯著肌膚如雪榴啸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天晚岭,我揣著相機(jī)與錄音鸥印,去河邊找鬼。 笑死坦报,一個(gè)胖子當(dāng)著我的面吹牛库说,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播片择,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼潜的,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了字管?” 一聲冷哼從身側(cè)響起啰挪,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嘲叔,沒想到半個(gè)月后亡呵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡借跪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年政己,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡歇由,死狀恐怖卵牍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情沦泌,我是刑警寧澤糊昙,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站谢谦,受9級(jí)特大地震影響释牺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜回挽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一没咙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧千劈,春花似錦祭刚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至喜滨,卻和暖如春捉捅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背虽风。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工棒口, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人焰情。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓陌凳,卻偏偏與公主長(zhǎng)得像剥懒,于是被迫代替她去往敵國(guó)和親内舟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359