開閉原則是面向?qū)ο蟪绦蛟O(shè)計(jì)的終極目標(biāo),它使軟件實(shí)體擁有一定的適應(yīng)性和靈活性的同時(shí)具備穩(wěn)定性和延續(xù)性。當(dāng)應(yīng)用的需求改變時(shí),在不修改軟件實(shí)體的源代碼或者二進(jìn)制代碼的前提下,可以擴(kuò)展模塊的功能,使其滿足新的需求。SPI 就是開閉原則的一種實(shí)現(xiàn)。本文將帶領(lǐng)同學(xué)們了解 SPI ,對比 Dubbo SPI 與 Java SPI ,同時(shí)用一個(gè) Dubbo SPI 替換 Java SPI 的實(shí)踐項(xiàng)目,來演示如何將 SPI 機(jī)制引入日常項(xiàng)目中。
什么是 SPI
SPI 全稱是 Service Provider Interface,是一種將服務(wù)接口與服務(wù)實(shí)現(xiàn)分離以達(dá)到解耦、可以提升程序可擴(kuò)展性的機(jī)制。引入服務(wù)提供者就是引入了 SPI 接口的實(shí)現(xiàn)者,通過本地的注冊發(fā)現(xiàn)獲取到具體的實(shí)現(xiàn)類,可以在運(yùn)行時(shí),動態(tài)為接口替換實(shí)現(xiàn)類,實(shí)現(xiàn)服務(wù)的熱插拔。
Java SPI
Java SPI 中有四個(gè)重要的組件:
- 服務(wù)接口:一個(gè)定義了服務(wù)提供者實(shí)現(xiàn)類契約方法的接口或者抽象類。
- 服務(wù)實(shí)現(xiàn):實(shí)際提供服務(wù)的實(shí)現(xiàn)類。
- SPI 配置文件:文件名必須存在于 META-INF/services 目錄中。文件名應(yīng)與服務(wù)提供商接口完全限定名完全相同。文件中的每一行都有一個(gè)實(shí)現(xiàn)服務(wù)類詳細(xì)信息,即服務(wù)提供者類的完全限定名。
- ServiceLoader: Java SPI 關(guān)鍵類,用于加載服務(wù)提供者接口的服務(wù)。ServiceLoader 中有各種實(shí)用程序方法,用于獲取特定的實(shí)現(xiàn)、迭代它們或再次重新加載服務(wù)。
小試牛刀
服務(wù)接口
現(xiàn)有一個(gè)壓縮與解壓服務(wù)接口,有一個(gè)壓縮方法 compress ,一個(gè)解壓方法 decompress,入?yún)⒊鰠⒍际亲止?jié)數(shù)組:
package cn.ppphuang.demoserver.serviceproviders;
public interface Compresser {
byte[] compress(byte[] bytes);
byte[] decompress(byte[] bytes);
}
服務(wù)實(shí)現(xiàn)
有兩個(gè)實(shí)現(xiàn)類,假設(shè)一個(gè)使用Gzip算法來實(shí)現(xiàn):
package cn.ppphuang.demoserver.serviceproviders;
import java.nio.charset.StandardCharsets;
public class GzipCompresser implements Compresser{
@Override
public byte[] compress(byte[] bytes) {
return "compress by Gzip".getBytes(StandardCharsets.UTF_8);
}
@Override
public byte[] decompress(byte[] bytes) {
return "decompress by Gzip".getBytes(StandardCharsets.UTF_8);
}
}
另一個(gè)使用Zip算法來實(shí)現(xiàn):
package cn.ppphuang.demoserver.serviceproviders;
import java.nio.charset.StandardCharsets;
public class ZipCompresser implements Compresser {
@Override
public byte[] compress(byte[] bytes) {
return "compress by Zip".getBytes(StandardCharsets.UTF_8);
}
@Override
public byte[] decompress(byte[] bytes) {
return "decompress by Zip".getBytes(StandardCharsets.UTF_8);
}
}
SPI 配置文件
然后在項(xiàng)目 META-INF/services 文件夾(如果 resources 目錄下沒有,創(chuàng)建該目錄)下創(chuàng)建 cn.ppphuang.demoserver.serviceproviders.Compresser 文件,文件名是壓縮服務(wù)接口類的全限定類名,兩行內(nèi)容分別是剛剛兩個(gè)接口實(shí)現(xiàn)類的全限定類名,如下:
cn.ppphuang.demoserver.serviceproviders.GzipCompresser
cn.ppphuang.demoserver.serviceproviders.ZipCompresser
通過 ServiceLoader 加載服務(wù)
main 方法中通過 ServiceLoader.load(Compresser.class) 獲取該服務(wù)的所有實(shí)現(xiàn)類,遍歷實(shí)例調(diào)用方法。
public static void main(String[] args) {
ServiceLoader<Compresser> serviceLoader = ServiceLoader.load(Compresser.class);
for (Compresser service : serviceLoader) {
System.out.println(service.getClass().getClassLoader());
byte[] compress = service.compress("Hello".getBytes(StandardCharsets.UTF_8));
System.out.println(new String(compress));
byte[] decompress = service.decompress("Hello".getBytes(StandardCharsets.UTF_8));
System.out.println(new String(decompress));
}
}
輸出結(jié)果
sun.misc.Launcher$AppClassLoader@18b4aac2
compress by Gzip
decompress by Gzip
sun.misc.Launcher$AppClassLoader@18b4aac2
compress by Zip
decompress by Zip
由輸出結(jié)果可以看到,不需要我們自己去實(shí)例化兩個(gè)實(shí)現(xiàn)類,就可以直接調(diào)用。加載實(shí)現(xiàn)類的類加載器是 AppClassLoader。
如果你還有這個(gè)接口的其他實(shí)現(xiàn),你可以在別的包里實(shí)現(xiàn)這個(gè)接口,然后在實(shí)現(xiàn)類所在包的 META-INF/services 文件夾下創(chuàng)建 **cn.ppphuang.demoserver.serviceproviders.Compresser 文件,文件內(nèi)容為你的實(shí)現(xiàn)類的全限定類名。ServiceLoader **會去尋找所有包下的 META-INF/services/cn.ppphuang.demoserver.serviceproviders.Compresser 文件,加載并實(shí)例化文件內(nèi)容中每一行的實(shí)現(xiàn)類。
使用場景
SPI 機(jī)制使用的非常廣泛,我們以 JDBC 為例看 SPI 如何使用。
JDBC 使用 SPI 加載不同類型數(shù)據(jù)庫的驅(qū)動,下面是我們常用的使用 JDBC 操作 MySql 數(shù)據(jù)庫的示例代碼,沒有顯式指定使用哪種數(shù)據(jù)庫驅(qū)動,依然可以正常使用。
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
} catch (SQLException e) {
System.out.println("數(shù)據(jù)庫連接失敗");
}
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("select * from user where id = 1");
while (resultSet.next()) {
System.out.println(resultSet.getString(2));
}
}
來看 DriverManager 類的代碼,靜態(tài)代碼塊中調(diào)用 loadInitialDrivers 方法加載數(shù)據(jù)庫驅(qū)動,方法里使用 ServiceLoader.load(Driver.class) 加載驅(qū)動類。
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
return null;
}
});
}
}
我們打開 mysql-connector-java 包的 META-INF/services 文件夾,果然有 java.sql.Driver 類的 SPI 配置文件,文件內(nèi)容的第一行就是 MySQL 的連接驅(qū)動類的全限定類名。

再看 com.mysql.jdbc.Driver 驅(qū)動類,靜態(tài)方法中實(shí)例化了自己,并將自己注冊到 DriverManager 中。
以上就是為什么我們不指定驅(qū)動類還可以正常使用的原因。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
我們總結(jié)一下 JDBC 自適應(yīng)驅(qū)動的流程:
- 實(shí)現(xiàn)了 java.sql.Driver 的驅(qū)動包,按照 SPI 的約定,在 META-INF/services/java.sql.Driver 文件中指定具體的驅(qū)動類。
- DriverManager 利用 ServiceLoader 去掃描各個(gè) jar 包下的 META-INF/services/java.sql.Driver 文件,加載并初始化文件內(nèi)容中指定的驅(qū)動實(shí)現(xiàn)類。
- 初始化具體的實(shí)現(xiàn)類,就會自動向 DriverManager 注冊當(dāng)前實(shí)現(xiàn)類到 DriverManager 中的 registeredDrivers。
- 使用 DriverManager.getConnection 連接數(shù)據(jù)庫時(shí),getConnection 中會循環(huán) registeredDrivers 嘗試校驗(yàn)并連接數(shù)據(jù)庫
美中不足
使用 Java SPI 能方便得解耦模塊,使得接口的定義與具體業(yè)務(wù)實(shí)現(xiàn)分離。應(yīng)用程序可以根據(jù)實(shí)際業(yè)務(wù)情況啟用或替換具體組件。
但是也有一些缺點(diǎn):
- 不能按需加載。雖然 ServiceLoader 做了延遲載入,但是基本只能通過遍歷全部獲取,也就是接口的實(shí)現(xiàn)類得全部載入并實(shí)例化。如果你并不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。
- 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來獲取對應(yīng)的實(shí)現(xiàn)類。
- 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。
Dubbo SPI
為了使用更加完美的 SPI 機(jī)制,優(yōu)化 Java SPI 的弊端,很多廠商自己實(shí)現(xiàn)了一套 SPI 機(jī)制,比如 Dubbo 。
Dubbo SPI 擴(kuò)展能力的特性:
- 按需加載。Dubbo 的擴(kuò)展能力不會一次性實(shí)例化所有實(shí)現(xiàn),而是用哪個(gè)擴(kuò)展類則實(shí)例化哪個(gè)擴(kuò)展類,減少資源浪費(fèi)。
- 增加擴(kuò)展類的 IOC 能力。Dubbo 的擴(kuò)展能力并不僅僅只是發(fā)現(xiàn)擴(kuò)展服務(wù)實(shí)現(xiàn)類,而是在此基礎(chǔ)上更進(jìn)一步,如果該擴(kuò)展類的屬性依賴其他對象,則 Dubbo 會自動的完成該依賴對象的注入功能。
- 增加擴(kuò)展類的 AOP 能力。Dubbo 擴(kuò)展能力會自動的發(fā)現(xiàn)擴(kuò)展類的包裝類,完成包裝類的構(gòu)造,增強(qiáng)擴(kuò)展類的功能。
- 具備動態(tài)選擇擴(kuò)展實(shí)現(xiàn)的能力。Dubbo 擴(kuò)展會基于參數(shù),在運(yùn)行時(shí)動態(tài)選擇對應(yīng)的擴(kuò)展類,提高了 Dubbo 的擴(kuò)展能力。
- 可以對擴(kuò)展實(shí)現(xiàn)進(jìn)行排序。能夠基于用戶需求,指定擴(kuò)展實(shí)現(xiàn)的執(zhí)行順序。
- 提供擴(kuò)展點(diǎn)的 Adaptive 能力。該能力可以使的一些擴(kuò)展類在 consumer 端生效,一些擴(kuò)展類在 provider 端生效。
Dubbo SPI 加載擴(kuò)展的工作流程:

主要步驟為 4 個(gè):
- 讀取并解析配置文件。
- 緩存所有擴(kuò)展實(shí)現(xiàn)。
- 基于用戶執(zhí)行的擴(kuò)展名,實(shí)例化對應(yīng)的擴(kuò)展實(shí)現(xiàn)。
案例解析
在dubbo中,下面這種獲取擴(kuò)展類的方法很常見,通過指定接口類 ThreadPool.class 的實(shí)現(xiàn)類的別名 eager 即可獲取到對應(yīng)的實(shí)現(xiàn)類。
ThreadPool threadPool = ExtensionLoader.getExtensionLoader(ThreadPool.class).getExtension("eager")
//EagerThreadPool
相應(yīng)的配置文件在 META-INF/dubbo/internal/com.alibaba.dubbo.common.threadpool.ThreadPool ,內(nèi)容如下:
fixed=com.alibaba.dubbo.common.threadpool.support.fixed.FixedThreadPool
cached=com.alibaba.dubbo.common.threadpool.support.cached.CachedThreadPool
limited=com.alibaba.dubbo.common.threadpool.support.limited.LimitedThreadPool
eager=com.alibaba.dubbo.common.threadpool.support.eager.EagerThreadPool
來看與 Java SPI 配置文件的區(qū)別,配置文件都在 META-INF 文件夾下,但是具體路徑不同; Java SPI 配置文件的每一行只是實(shí)現(xiàn)類的全限定類名,Dubbo SPI 配置文件里全限定名前都有一個(gè)別名,可以通過別名獲取到該實(shí)現(xiàn)類。
來看 ThreadPool 接口,該接口有一個(gè) @SPI("fixed") 注解,注解的 value 是 **fixed **。表明該接口的默認(rèn)實(shí)現(xiàn)是別名為 fixed 的實(shí)現(xiàn)類,即 FixedThreadPool 。
@SPI("fixed")
public interface ThreadPool {
@Adaptive({Constants.THREADPOOL_KEY})
Executor getExecutor(URL url);
}
@SPI 注解:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
/**
* default extension name
*/
String value() default "";
}
那么可以這樣獲取該類的默認(rèn)實(shí)現(xiàn)類:
ThreadPool defaultExtension = ExtensionLoader.getExtensionLoader(ThreadPool.class).getDefaultExtension();
//FixedThreadPool
源碼分析
Dubbo SPI 的關(guān)鍵類是 ExtensionLoader 。先看類的幾個(gè)重要靜態(tài)屬性,看完就能知道為什么上例中的配置文件為什么在 META-INF/dubbo/internal/ 中了。還有幾個(gè) ConcurrentHashMap 用于緩存數(shù)據(jù),后續(xù)的方法中都會用到。
public class ExtensionLoader<T> {
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();
}
通過 getExtensionLoader 方法獲取某個(gè)接口類型的 ExtensionLoader 對象時(shí),會判斷是否是 interface ,也會通過 withExtensionAnnotation 方法判斷該接口是否有 @SPI 注解,沒有的話會拋出異常,表明該接口不是一個(gè)可以擴(kuò)展的接口。然后從 EXTENSION_LOADERS 中獲取實(shí)例,沒有就實(shí)例化一個(gè),然后返回。
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
private static <T> boolean withExtensionAnnotation(Class<T> type) {
return type.isAnnotationPresent(SPI.class);
}
有了 ExtensionLoader 實(shí)例就可以調(diào)用 getExtension 方法指定實(shí)現(xiàn)類的別名,來獲取該實(shí)現(xiàn)類的實(shí)例。
如果 "true".equals(name) 就返回該接口通過 @SPI 注解指定的默認(rèn)實(shí)現(xiàn)類。
判斷 cachedInstances 中是否有該實(shí)現(xiàn)類的緩存數(shù)據(jù),返回值是 Holder 對象,這個(gè)對象可以看作為一個(gè)數(shù)據(jù)承載對象,通過 holder.get() 可以獲取到對象里承載的數(shù)據(jù),這里就是接口實(shí)現(xiàn)類的實(shí)例化對象。如果 cachedInstances 中獲取不到 Holder 對象,就會調(diào)用 createExtension 方法獲取接口的具體實(shí)現(xiàn)類對象,放入承載對象中,然后就可以返回實(shí)現(xiàn)類的實(shí)例。(可以看到這里使用了常用的 double check方法)
public T getExtension(String name) {
if ("true".equals(name)) {
return getDefaultExtension();
}
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
createExtension 方法中通過 getExtensionClasses().get(name) 方法獲取到別名為 name 的接口實(shí)現(xiàn)類 Class,然后通過 clazz.newInstance() 實(shí)例化返回。
private T createExtension(String name) {
Class<?> clazz = getExtensionClasses().get(name);
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}
那么 getExtensionClasses().get(name) 方法如何獲取到別名指定的類呢?我們繼續(xù)追代碼會發(fā)現(xiàn)這樣的調(diào)用鏈:
getExtensionClasses() -> loadExtensionClasses() -> loadDirectory() -> loadResource() -> loadClass() 。
鑒于篇幅,筆者不一一貼出代碼,只拿重要的節(jié)點(diǎn)來描述:
在這整個(gè)調(diào)用鏈中會維護(hù)一個(gè) Map<String, Class<?>> extensionClasses,key 為實(shí)現(xiàn)類的別名, value 為該實(shí)現(xiàn)類。 getExtensionClasses().get(name) 就是從這個(gè) map 中獲取 name 別名的實(shí)現(xiàn)類。
loadDirectory() 會找到所有包中 META-INF/dubbo/internal/ 路徑下指定接口類名的文件。在上例中就是 META-INF/dubbo/internal/com.alibaba.dubbo.common.threadpool.ThreadPool 。
loadResource() 會解析每一個(gè)文件的內(nèi)容:
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
...
while ((line = reader.readLine()) != null) {
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
}
...
}
代碼中可以看見讀到的每一行內(nèi)容按照 = 號分隔,前面是實(shí)現(xiàn)類的別名,后面是實(shí)現(xiàn)類的全限定類名。有了別名的全限定類名就可以通過 Class.forName(line, true, classLoader) 獲取 Class ,然后將別名與 Class 傳給 loadClass() 。
loadClass() 中會將別名與 Class 寫入到上文中提到的 extensionClasses 這個(gè) map 中,這樣 getExtensionClasses().get(name) 就能方便獲取了。
錦上添花
講到這里, Dubbo SPI 的主要流程應(yīng)該已經(jīng)講完了,但是 Dubbo SPI 中對 Java SPI 的增強(qiáng)還沒有提及,比如增加擴(kuò)展類的 IOC 能力;增加擴(kuò)展類的 AOP 能力等。這些在 ExtensionLoader 類中都有體現(xiàn),感興趣的同學(xué)可以查看代碼,相信你一定能看到這些具體的實(shí)現(xiàn)。
實(shí)戰(zhàn)
如何將Dubbo SPI引入項(xiàng)目
了解了 Dubbo SPI 的實(shí)現(xiàn)原理,那怎么在我們的項(xiàng)目中使用 Dubbo SPI 呢?現(xiàn)在我們在一個(gè)現(xiàn)有使用 Java SPI 的項(xiàng)目中引入 Dubbo SPI ,通過這個(gè)實(shí)踐讓你更深入了解 Dubbo SPI 的原理。
這個(gè)項(xiàng)目是一個(gè)簡單 RPC 項(xiàng)目,原本用來序列化、和解壓縮的接口實(shí)現(xiàn)類都是通過 Java SPI 來加載到項(xiàng)目中的:
cn.ppphuang.rpcspringstarter.common.protocol.JavaSerializeMessageProtocol
cn.ppphuang.rpcspringstarter.common.protocol.KryoMessageProtocol
cn.ppphuang.rpcspringstarter.common.protocol.ProtoBufSerializeMessageProtocol
public static Map<String, MessageProtocol> buildSupportMessageProtocol() {
HashMap<String, MessageProtocol> supportMessageProtocol = new HashMap<>();
ServiceLoader<MessageProtocol> loader = ServiceLoader.load(MessageProtocol.class);
for (MessageProtocol messageProtocol : loader) {
MessageProtocolAno annotation = messageProtocol.getClass().getAnnotation(MessageProtocolAno.class);
Assert.notNull(annotation, "message protocol name can not be empty!");
supportMessageProtocol.put(annotation.value(), messageProtocol);
}
return supportMessageProtocol;
}
必須通過 ServiceLoader.load(MessageProtocol.class) 獲取所有的接口實(shí)現(xiàn)類,然后放入到 map 中以供后續(xù)取用,不能指定實(shí)例化某一個(gè)實(shí)現(xiàn)類。因?yàn)槲覀冃蛄谢蛘呓鈮嚎s實(shí)現(xiàn)類的選擇都是通過項(xiàng)目的啟動配置文件來決定的,項(xiàng)目啟動時(shí)只會選擇配置中指定的這個(gè)實(shí)現(xiàn)類,所以加載并實(shí)例化所有的實(shí)現(xiàn)類就會浪費(fèi)資源。
我們來用 Dubbo SPI 替換 Java SPI :
創(chuàng)建 @SPI 注解,這里因?yàn)槲覀兺ㄟ^配置文件決定默認(rèn)實(shí)現(xiàn)類,所有注解沒有 value 值:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SPI {
}
創(chuàng)建 Holder 對象承載類:
public class Holder<T> {
private volatile T value;
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
}
創(chuàng)建 ExtensionLoader 類,代碼較長建議查看附錄鏈接中的源代碼:
public class ExtensionLoader<T> {
private static final String SERVICES_DIRECTORY = "META-INF/services/";
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {}
public T getExtension(String name) {}
private T createExtension(String name) {}
private Map<String, Class<?>> getExtensionClasses() {}
private Map<String, Class<?>> loadExtensionClasses() {}
private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {}
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {
final int ei = line.indexOf('=');
//配置文件內(nèi)容格式兼容 Java SPI
if (ei > 0) {
String name = line.substring(0, ei).trim();
String clazzName = line.substring(ei + 1).trim();
if (name.length() > 0 && clazzName.length() > 0) {
Class<?> clazz = classLoader.loadClass(clazzName);
extensionClasses.put(name, clazz);
}
} else {
Class<?> clazz = classLoader.loadClass(line);
//使用類注解中的指定別名
SPIExtension annotation = clazz.getAnnotation(SPIExtension.class);
String name = annotation.value();
extensionClasses.put(name, clazz);
}
}
}
類中的主要流程與 Dubbo SPI 中基本類似,刪減了一些不需要的增強(qiáng)功能,主要實(shí)現(xiàn)類的選擇性加載。同時(shí)也加入了自己的一些修改:
- 配置文件路徑兼容 Java SPI ,也放到 META-INF/services/ 文件夾下。
- 配置文件內(nèi)容格式兼容 Java SPI ,通過 loadResource 方法中的改動來實(shí)現(xiàn) 。
- SPI 文件的格式為 xxxx 時(shí),按照實(shí)現(xiàn)類中
@SPIExtension注解的 value 名稱作為別名。com.alibaba.dubbo.common.compiler.support.JdkCompiler。 - SPI 文件的格式為 xxx=xxxx 時(shí),xxx 為別名。jdk=com.alibaba.dubbo.common.compiler.support.JdkCompiler。
- SPI 文件的格式為 xxxx 時(shí),按照實(shí)現(xiàn)類中
有了這樣的兼容處理,不需要改動配置文件就可以直接替換 Java SPI 。
@SPIExtension 注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SPIExtension {
String value();
}
@SPI
public interface MessageProtocol {
//表明該接口是支持 SPI 擴(kuò)展的接口
}
@SPIExtension("kryo")
public class KryoMessageProtocol implements MessageProtocol {
//表明該實(shí)現(xiàn)類的默認(rèn)別名是 kryo
//配置文件中可以使用 = 號設(shè)置新別名來覆蓋該別名
}
@SPIExtension("protobuf")
public class ProtoBufSerializeMessageProtocol implements MessageProtocol {
//表明該實(shí)現(xiàn)類的默認(rèn)別名是 protobuf
//配置文件中可以使用 = 號設(shè)置新別名來覆蓋該別名
}
這樣就可以使用 Dubbo SPI 加載 Java SPI 機(jī)制下的類,項(xiàng)目中的實(shí)現(xiàn)類按需加載,不需要像 Java SPI 那樣遍歷實(shí)例化的所有對象了:
MessageProtocol protocol = ExtensionLoader.getExtensionLoader(MessageProtocol.class).getExtension("kryo");
MessageProtocol protocol = ExtensionLoader.getExtensionLoader(MessageProtocol.class).getExtension("protobuf");
整個(gè)替換代碼比較簡單,容易看懂,而且項(xiàng)目中也保留了其他接口 Java SPI 擴(kuò)展的方式,可以對照項(xiàng)目中已經(jīng)替換的 Dubbo SPI 擴(kuò)展加載方式來閱讀理解。
附
https://github.com/PPPHUANG/rpc-spring-starter
參考
https://dubbo.apache.org/zh/docs/concepts/extensibility/
https://github.com/apache/dubbo
一位后端寫碼師,一位黑暗料理制造者。公眾號:DailyHappy