一踊兜、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)目:
場(chǎng)景:如果想在組件1中使用主工程或組件1使用組件2的方法或變量击吱,如何實(shí)現(xiàn)
- 常見方式泼返,在組件1聲明一些接口,由主工程實(shí)現(xiàn)姨拥。然后在初始化組件1的時(shí)候绅喉,通過注入方式傳遞給組件1。
- 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í)例化。
服務(wù)約定
定義好接口或抽象類作為服務(wù)服務(wù)實(shí)現(xiàn)
實(shí)現(xiàn)定義好的服務(wù)童谒,由于ServiceLoader可以更方便不同組件間通信,高度解耦沪羔。所以更常見的場(chǎng)景是服務(wù)可能是定義在底層組件或引入jar包饥伊,在上層業(yè)務(wù)代碼中具體實(shí)現(xiàn)。-
服務(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()方法等。