寫在前面
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也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實現類全部加載並實例化一遍。