作者:京東物流 楊葦葦
1.SPI簡介
SPI(Service Provicer Interface)是Java語言提供的一種接口發現機制,用來實現接口和接口實現的解耦。簡單來說,就是系統只需要定義接口規范以及可以發現接口實現的機制,而不需要實現接口。
SPI機制在Java中應用廣泛。例如:JDBC中的數據庫連接驅動使用SPI機制,只定義了數據庫連接接口的規范,而具體實現由各大數據庫廠商實現,不同數據庫的實現不同,我們常用的mysql的驅動也實現了其接口規范,通過這種方式,JDBC數據庫連接可以適配不同的數據庫。
SPI機制在各種框架中也有應用,例如:springboot的自動裝配中查找spring.factories文件的步驟就是應用了SPI機制;dubbo也對Java的SPI機制進行擴展,實現了自己的SPI機制。
2.SPI入門案例
2.1.創建工程
我們剛才在介紹中說過了,SPI機制需要定義接口規范,這里我們以一個簡單的接口案例來說明。
首先我們需要創建四個工程:
?spi-interface,這里定義SPI的接口類:Person
?spi-impl1,這里定義接口的第一個實現類:Teacher
?spi-impl2,這里定義接口的第二個實現類:Student
?spi-test,這里通過SPI機制加載所有實現類進行測試

??
2.2.創建SPI接口規范
接口如下所示:
package com.jd.spi;
public interface Person {
String favorite();
}
2.3.創建實現類1項目
2.3.1.創建接口
接口如下所示:
package com.jd.spi;
public class Teacher implements Person {
public String favorite() {
return "老師喜歡給學生上課";
}
}
2.3.2.創建spi配置文件
如下圖所示,在項目的resources文件夾下創建兩個文件夾META-INF/services,然后在文件夾下面創建名稱為com.jd.spi.Person的文件,其文件的內容為當前項目的接口實現類com.jd.spi.Teacher。

??
2.4.創建實現類2項目
2.4.1.創建實現類2
接口如下所示:
package com.jd.spi;
public class Student implements Person {
public String favorite() {
return "學生喜歡努力學習";
}
}
2.4.2.創建spi配置文件
如下圖所示,在項目的resources文件夾下創建兩個文件夾META-INF/services,然后在文件夾下面創建名稱為com.jd.spi.Person的文件,其文件的內容為當前項目的接口實現類com.jd.spi.Student。

??
2.5.創建測試項目
2.5.1.引入3個maven依賴
這里需要引入接口定義項目和兩個接口實現項目。
如下所示:
org.example/groupId?>
spi-interface/artifactId?>
1.0-SNAPSHOT/version?>
/dependency?>
org.example/groupId?>
spi-impl1/artifactId?>
1.0-SNAPSHOT/version?>
/dependency?>
org.example/groupId?>
spi-impl2/artifactId?>
1.0-SNAPSHOT/version?>
/dependency?>
/dependencies?>
2.5.2.創建測試類
如下所示:
package com.jd.spi; import java.util.Iterator; import java.util.ServiceLoader; public class SPITest { public static void main(String[] args) { ServiceLoader loader = ServiceLoader.load(Person.class); for(Iterator it = loader.iterator(); it.hasNext();){ Person person = it.next(); System.out.println(person.favorite());; } } }
運行測試類,其結果如下所示:

??
我們發現,Java的SPI機制獲取了所有Person類的實現類,并執行其對應的favorite方法。
3.SPI機制的原理
3.1.ServiceLoader的核心屬性
其核心機制就是ServiceLoader類的load方法,下面我們將從源碼來分析其原理。
首先我們先看下ServiceLoader的核心屬性:
public final class ServiceLoader
implements Iterable
{
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class service;
// 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;
// Cached providers, in instantiation order
private LinkedHashMap providers = new LinkedHashMap?>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
這個PREFIX屬性、providers屬性和lookupIterator屬性將在后續的代碼中使用到,我們發現PREFIX屬性就是示例中說的META-INF/services路徑。
3.2.ServiceLoader的遍歷器
示例中,我們會獲取serviceLoader的遍歷器iterator,其方法如下所示:
public Iterator iterator() {
return new Iterator() {
Iterator> 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();
}
};
}
然后需要執行遍歷器的next方法獲取元素,其next方法執行的是lookupIterator.next()。
接下來我們來看下lookupIterator的next方法:
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction action = new PrivilegedAction() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
其執行的是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");
}
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);
}
throw new Error(); // This cannot happen
}
nextService方法首先執行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;
}
這個方法會執行String fullName = PREFIX + service.getName(),而PREFIX就是我們前面剛才說的非常重要的屬性,其值為META-INF/services/,service就是接口類,其最終的fullName指的就是META-INF/services文件夾下的名稱為com.jd.spi.Person的文件。
接著會執行configs = loader.getResources(fullName)方法,這個方法這里不做詳細描述,其主要功能就是獲取類路徑下所有相對路徑為fullName的所有文件的URL對象。
然后會執行pending = parse(service, configs.nextElement())方法,這個方法這里也不詳細描述,其主要功能是讀取文件,將文件內容變成字符串,然后nextName就被賦值為當前文件的內容,即實現類的接口全限定名。
因此,執行hasNextService()方法后,nextName被賦值為一個實現類的全限定名。
我們繼續看上面的nextService()方法,其最終會執行c = Class.forName(cn, false, loader)方法,這個方法很明顯就是通過反射實例化一個對象。通過一系列操作,最終返回了對應實現類的對象。
3.3.流程總結
我們將其總結為以下幾個步驟:
1.創建ServiceLoader對象
2.創建迭代器lookupIterator
3.通過迭代器的hasNextService方法讀取類路徑下META-INF/services目錄的所有名稱為接口全限定名的文件,將其內容存入configs對象中
4.從configs對象中獲取實現類的全限定名,然后通過反射實例化對象
從上述流程,我們也可以總結實現SPI的幾點重要信息:
1.實現工程必須在類路徑下的META-INF/services目錄下創建接口全限定名的文件,其文件內容必須是接口實現類的全限定名
2.實現類必須有一個無參構造方法,因為SPI默認是使用無參構造方法實例化對象的
4.總結
本文首先概述了Java的SPI機制,隨后闡述了其基本使用方法,最后深入探討了其實現原理。SPI在Java語言體系中具有廣泛應用,能夠有效地實現系統解耦,眾多框架基于此機制進行了拓展和優化,從而實現了更為強大的SPI機制。掌握SPI的使用技巧可以幫助我們設計出更為靈活的系統,而深入理解其原理則有助于提升我們的技術水平。
審核編輯 黃宇
-
JAVA
+關注
關注
20文章
2997瀏覽量
115799 -
SPI
+關注
關注
17文章
1868瀏覽量
100004
發布評論請先 登錄
java類加載機制圖文詳解
詳解java并發機制
java的動態代理機制和作用
java動態代理機制詳解的類和接口描述
Java反射機制到底是什么?有什么作用
源碼級深度理解Java SPI
基于spring的SPI擴展機制是如何實現的?
Java、Spring、Dubbo三者SPI機制的原理和區別
SPI是什么?Java SPI的使用介紹

Java的SPI機制詳解
評論