SPI:Java的高可擴展利器

摘要:JAVA SPI,基於介面的編程+策略模式+配置文件的動態載入機制。

本文分享自華為雲社區《一文講透Java核心技術之高可擴展利器SPI》,作者: 冰 河。

SPI的概念

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="//maven.apache.org/POM/4.0.0"
 xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //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總結

優點:

能夠實現項目解耦,使得第三方服務模組的裝配控制的邏輯與調用者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴展或替換框架組件。

缺點:

  • 多個並發多執行緒使用ServiceLoader類的實例是不安全的
  • 雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並實例化一遍。

參考:深入理解Java中的spi機制

 

點擊關注,第一時間了解華為雲新鮮技術~