寫在前面
SPI機制能夠非常方便的為某個介面動態指定其實現類,在某種程度上,這也是某些框架具有高度可擴充套件性的基礎。今天,我們就從原始碼級別深入探討下Java中的SPI機制。
SPI的概念
SPI在Java中的全稱為Service Provider Interface,是JDK內建的一種服務提供發現機制,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。
JAVA SPI = 基於介面的程式設計+策略模式+配置檔案的動態載入機制
SPI的使用場景
Java是一種面嚮物件語言,雖然Java8開始支援函數語言程式設計和Stream,但是總體來說,還是物件導向的語言。在使用Java進行物件導向開發時,一般會推薦使用基於介面的程式設計,程式的模組與模組之前不會直接進行實現類的硬編碼。而在實際的開發過程中,往往一個介面會有多個實現類,各實現類要麼實現的邏輯不同,要麼使用的方式不同,還有的就是實現的技術不同。為了使呼叫方在呼叫介面的時候,明確的知道自己呼叫的是介面的哪個實現類,或者說為了實現在模組裝配的時候不用在程式裡動態指明,這就需要一種服務發現機制。Java中的SPI載入機制能夠滿足這樣的需求,它能夠自動尋找某個介面的實現類。
大量的框架使用了Java的SPI技術,如下:
(1)JDBC載入不同型別的資料庫驅動
(2)日誌門面介面實現類載入,SLF4J載入不同提供商的日誌實現類
(3)Spring中大量使用了SPI
- 對servlet3.0規範
- 對ServletContainerInitializer的實現
- 自動型別轉換Type Conversion SPI(Converter SPI、Formatter SPI)等
(4)Dubbo裡面有很多個元件,每個元件在框架中都是以介面的形成抽象出來!具體的實現又分很多種,在程式執行時根據使用者的配置來按需取介面的實現
SPI的使用
當服務的提供者,提供了介面的一種實現後,需要在Jar包的META-INF/services/目錄下,建立一個以介面的名稱(包名.介面名的形式)命名的檔案,在檔案中配置介面的實現類(完整的包名+類名)。
當外部程式通過java.util.ServiceLoader類裝載這個介面時,就能夠通過該Jar包的META/Services/目錄裡的配置檔案找到具體的實現類名,裝載例項化,完成注入。同時,SPI的規範規定了介面的實現類必須有一個無參構造方法。
SPI中查詢介面的實現類是通過java.util.ServiceLoader,而在java.util.ServiceLoader類中有一行程式碼如下:
// 載入具體實現類資訊的字首,也就是以介面命名的檔案需要放到Jar包中的META-INF/services/目錄下
private static final String PREFIX = "META-INF/services/";
這也就是說,我們必須將介面的配置檔案寫到Jar包的META/Services/目錄下。
SPI例項
這裡,給出一個簡單的SPI使用例項,演示在Java程式中如何使用SPI動態載入介面的實現類。
注意:例項是基於Java8進行開發的。
1.建立Maven專案
在IDEA中建立Maven專案spi-demo,如下:
2.編輯pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<artifactId>spi-demo</artifactId>
<groupId>io.binghe.spi</groupId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.建立類載入工具類
在io.binghe.spi.loader包下建立MyServiceLoader,MyServiceLoader類中直接呼叫JDK的ServiceLoader類載入Class。程式碼如下所示。
package io.binghe.spi.loader;
import java.util.ServiceLoader;
/**
* @author binghe
* @version 1.0.0
* @description 類載入工具
*/
public class MyServiceLoader {
/**
* 使用SPI機制載入所有的Class
*/
public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) {
return ServiceLoader.load(clazz);
}
}
4.建立介面
在io.binghe.spi.service包下建立介面MyService,作為測試介面,介面中只有一個方法,列印傳入的字串資訊。程式碼如下所示:
package io.binghe.spi.service;
/**
* @author binghe
* @version 1.0.0
* @description 定義介面
*/
public interface MyService {
/**
* 列印資訊
*/
void print(String info);
}
5.建立介面的實現類
(1)建立第一個實現類MyServiceA
在io.binghe.spi.service.impl包下建立MyServiceA類,實現MyService介面。程式碼如下所示:
package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;
/**
* @author binghe
* @version 1.0.0
* @description 介面的第一個實現
*/
public class MyServiceA implements MyService {
@Override
public void print(String info) {
System.out.println(MyServiceA.class.getName() + " print " + info);
}
}
(2)建立第二個實現類MyServiceB
在io.binghe.spi.service.impl包下建立MyServiceB類,實現MyService介面。程式碼如下所示:
package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;
/**
* @author binghe
* @version 1.0.0
* @description 介面第二個實現
*/
public class MyServiceB implements MyService {
@Override
public void print(String info) {
System.out.println(MyServiceB.class.getName() + " print " + info);
}
}
6.建立介面檔案
在專案的src/main/resources目錄下建立META/Services/目錄,在目錄中建立io.binghe.spi.service.MyService檔案,注意:檔案必須是介面MyService的全名,之後將實現MyService介面的類配置到檔案中,如下所示:
io.binghe.spi.service.impl.MyServiceA
io.binghe.spi.service.impl.MyServiceB
7.建立測試類
在專案的io.binghe.spi.main包下建立Main類,該類為測試程式的入口類,提供一個main()方法,在main()方法中呼叫ServiceLoader類載入MyService介面的實現類。並通過Java8的Stream將結果列印出來,如下所示:
package io.binghe.spi.main;
import io.binghe.spi.loader.MyServiceLoader;
import io.binghe.spi.service.MyService;
import java.util.ServiceLoader;
import java.util.stream.StreamSupport;
/**
* @author binghe
* @version 1.0.0
* @description 測試的main方法
*/
public class Main {
public static void main(String[] args){
ServiceLoader<MyService> loader = MyServiceLoader.loadAll(MyService.class);
StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
}
}
8.測試例項
執行Main類中的main()方法,列印出的資訊如下所示:
io.binghe.spi.service.impl.MyServiceA print Hello World
io.binghe.spi.service.impl.MyServiceB print Hello World
Process finished with exit code 0
通過列印資訊可以看出,通過Java SPI機制正確載入出介面的實現類,並呼叫介面的實現方法。
原始碼解析
這裡,主要是對SPI的載入流程涉及到的java.util.ServiceLoader的原始碼的解析。
進入java.util.ServiceLoader的原始碼,可以看到ServiceLoader類實現了java.lang.Iterable介面,如下所示。
public final class ServiceLoader<S> implements Iterable<S>
說明ServiceLoader類是可以遍歷迭代的。
java.util.ServiceLoader類中定義瞭如下的成員變數:
// 載入具體實現類資訊的字首,也就是以介面命名的檔案需要放到Jar包中的META-INF/services/目錄下
private static final String PREFIX = "META-INF/services/";
// 需要載入的介面
private final Class<S> service;
// 類載入器,用於載入以介面命名的檔案中配置的介面的實現類
private final ClassLoader loader;
// 建立ServiceLoader時採用的訪問控制上下文環境
private final AccessControlContext acc;
// 用來快取已經載入的介面實現類,其中,Key是介面實現類的完整類名,Value為實現類物件
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用於延遲載入實現類的迭代器
private LazyIterator lookupIterator;
可以看到ServiceLoader類中定義了載入字首為“META-INF/services/”,所以,介面檔案必須要在專案的src/main/resources目錄下的META-INF/services/目錄下建立。
從MyServiceLoader類呼叫ServiceLoader.load(clazz)方法進入原始碼,如下所示:
//根據類的Class物件載入指定的類,返回ServiceLoader物件
public static <S> ServiceLoader<S> load(Class<S> service) {
//獲取當前執行緒的類載入器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//動態載入指定的類,將類載入到ServiceLoader中
return ServiceLoader.load(service, cl);
}
方法中呼叫了ServiceLoader.load(service, cl)方法,繼續跟蹤程式碼,如下所示:
//通過ClassLoader載入指定類的Class,並將返回結果封裝到ServiceLoader物件中
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
可以看到ServiceLoader.load(service, cl)方法中,呼叫了ServiceLoader類的構造方法,繼續跟進程式碼,如下所示:
//構造ServiceLoader物件
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果傳入的Class物件為空,則判處空指標異常
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果傳入的ClassLoader為空,則通過ClassLoader.getSystemClassLoader()獲取,否則直接使用傳入的ClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
繼續跟reload()方法,如下所示。
//重新載入
public void reload() {
//清空儲存載入的實現類的LinkedHashMap
providers.clear();
//構造延遲載入的迭代器
lookupIterator = new LazyIterator(service, loader);
}
繼續跟進懶載入迭代器的建構函式,如下所示。
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
可以看到,會將需要載入的介面的Class物件和類載入器賦值給LazyIterator的成員變數。
當我們在程式中迭代獲取物件例項時,首先在成員變數providers中查詢是否有快取的例項物件。如果存在則直接返回,否則呼叫lookupIterator延遲載入迭代器進行載入。
迭代器進行邏輯判斷的程式碼如下所示:
//迭代ServiceLoader的方法
public Iterator<S> iterator() {
return new Iterator<S>() {
//獲取儲存實現類的LinkedHashMap<String,S>的迭代器
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
//判斷是否有下一個元素
public boolean hasNext() {
//如果knownProviders存在元素,則直接返回true
if (knownProviders.hasNext())
return true;
//返回延遲載入器是否存在元素
return lookupIterator.hasNext();
}
//獲取下一個元素
public S next() {
//如果knownProviders存在元素,則直接獲取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//獲取延遲迭代器lookupIterator中的元素
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
LazyIterator載入類的流程如下程式碼所示
//判斷是否擁有下一個例項
private boolean hasNextService() {
//如果擁有下一個例項,直接返回true
if (nextName != null) {
return true;
}
//如果實現類的全名為null
if (configs == null) {
try {
//獲取全檔名,檔案相對路徑+檔名稱(包名+介面名)
String fullName = PREFIX + service.getName();
//類載入器為空,則通過ClassLoader.getSystemResources()方法獲取
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()) {
//如果configs中沒有更過的元素,則直接返回false
if (!configs.hasMoreElements()) {
return false;
}
//解析包結構
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
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 {
//通過c.newInstance()生成物件例項
S p = service.cast(c.newInstance());
//將生成的物件例項儲存到快取中(LinkedHashMap<String,S>)
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
最後,給出整個java.util.ServiceLoader的類,如下所示:
package java.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
public final class ServiceLoader<S> implements Iterable<S> {
// 載入具體實現類資訊的字首,也就是以介面命名的檔案需要放到Jar包中的META-INF/services/目錄下
private static final String PREFIX = "META-INF/services/";
// 需要載入的介面
private final Class<S> service;
// 類載入器,用於載入以介面命名的檔案中配置的介面的實現類
private final ClassLoader loader;
// 建立ServiceLoader時採用的訪問控制上下文環境
private final AccessControlContext acc;
// 用來快取已經載入的介面實現類,其中,Key是介面實現類的完整類名,Value為實現類物件
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用於延遲載入實現類的迭代器
private LazyIterator lookupIterator;
//重新載入
public void reload() {
//清空儲存載入的實現類的LinkedHashMap
providers.clear();
//構造延遲載入的迭代器
lookupIterator = new LazyIterator(service, loader);
}
//構造ServiceLoader物件
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果傳入的Class物件為空,則判處空指標異常
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果傳入的ClassLoader為空,則通過ClassLoader.getSystemClassLoader()獲取,否則直接使用傳入的ClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
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;
}
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 inner class implementing fully-lazy provider lookupload
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
//判斷是否擁有下一個例項
private boolean hasNextService() {
//如果擁有下一個例項,直接返回true
if (nextName != null) {
return true;
}
//如果實現類的全名為null
if (configs == null) {
try {
//獲取全檔名,檔案相對路徑+檔名稱(包名+介面名)
String fullName = PREFIX + service.getName();
//類載入器為空,則通過ClassLoader.getSystemResources()方法獲取
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()) {
//如果configs中沒有更過的元素,則直接返回false
if (!configs.hasMoreElements()) {
return false;
}
//解析包結構
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
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 {
//通過c.newInstance()生成物件例項
S p = service.cast(c.newInstance());
//將生成的物件例項儲存到快取中(LinkedHashMap<String,S>)
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
//迭代ServiceLoader的方法
public Iterator<S> iterator() {
return new Iterator<S>() {
//獲取儲存實現類的LinkedHashMap<String,S>的迭代器
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
//判斷是否有下一個元素
public boolean hasNext() {
//如果knownProviders存在元素,則直接返回true
if (knownProviders.hasNext())
return true;
//返回延遲載入器是否存在元素
return lookupIterator.hasNext();
}
//獲取下一個元素
public S next() {
//如果knownProviders存在元素,則直接獲取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//獲取延遲迭代器lookupIterator中的元素
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
//通過ClassLoader載入指定類的Class,並將返回結果封裝到ServiceLoader物件中
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
//根據類的Class物件載入指定的類,返回ServiceLoader物件
public static <S> ServiceLoader<S> load(Class<S> service) {
//獲取當前執行緒的類載入器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//動態載入指定的類,將類載入到ServiceLoader中
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
/**
* Returns a string describing this service.
*
* @return A descriptive string
*/
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
SPI總結
最後,對Java提供的SPI機制進行簡單的總結。
優點:
能夠實現專案解耦,使得第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴充套件或替換框架元件。
缺點:
- 多個併發多執行緒使用ServiceLoader類的例項是不安全的
- 雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並例項化一遍。