JDK-Dubbo-Spring 三種 SPI 機制,誰更好?
作者:Corwien
來源:SegmentFault 思否社區
SPI 全稱爲 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態爲接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制爲我們的程序提供拓展功能。
那麼我們在初始化,解析配置時,只需要調用這個 XMLConfiguration 來解析 XML 配置文件即可。
package com.github.kongwu.spisamples;
public class LoggerFactory {
static {
SuperLoggerConfiguration configuration = new XMLConfiguration();
configuration.configure(configFile);
}
public static getLogger(Class clazz){
......
}
}
這樣就完成了一個基礎的模型,看起來也沒什麼問題。不過擴展性不太好,因爲如果想定製 / 擴展 / 重寫解析功能的話,我還得重新定義入口的代碼,LoggerFactory 也得重寫,不夠靈活,侵入性太強了。
比如現在用戶 / 使用方想增加一個 yml 文件的方式,作爲日誌配置文件,那麼只需要新建一個 YAMLConfiguration,實現 SuperLoggerConfiguration 就可以。但是…… 怎麼注入呢,怎麼讓 LoggerFactory 中使用新建的這個 YAMLConfiguration ?難不成連 LoggerFactory 也重寫了?
如果藉助 SPI 機制的話,這個事情就很簡單了,可以很方便的完成這個入口的擴展功能。
下面就先來看看,利用 JDK 的 SPI 機制怎麼解決上面的擴展性問題。
JDK SPI
JDK 中 提供了一個 SPI 的功能,核心類是 java.util.ServiceLoader。其作用就是,可以通過類名獲取在 "META-INF/services/" 下的多個配置實現文件。
爲了解決上面的擴展問題,現在我們在META-INF/services/
下創建一個com.github.kongwu.spisamples.SuperLoggerConfiguration
文件(沒有後綴)。文件中只有一行代碼,那就是我們默認的com.github.kongwu.spisamples.XMLConfiguration
(注意,一個文件裏也可以寫多個實現,回車分隔)
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:
com.github.kongwu.spisamples.XMLConfiguration
然後通過 ServiceLoader 獲取我們的 SPI 機制配置的實現類:
ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;
while(iterator.hasNext()) {
//加載並初始化實現類
configuration = iterator.next();
}
//對最後一個configuration類調用configure方法
configuration.configure(configFile);
最後在調整 LoggerFactory 中初始化配置的方式爲現在的 SPI 方式:
package com.github.kongwu.spisamples;
public class LoggerFactory {
static {
ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;
while(iterator.hasNext()) {
configuration = iterator.next();//加載並初始化實現類
}
configuration.configure(configFile);
}
public static getLogger(Class clazz){
......
}
}
等等,這裏爲什麼是用 iterator ? 而不是 get 之類的只獲取一個實例的方法?
試想一下,如果是一個固定的 get 方法,那麼 get 到的是一個固定的實例,SPI 還有什麼意義呢?
SPI 的目的,就是增強擴展性。將固定的配置提取出來,通過 SPI 機制來配置。那既然如此,一般都會有一個默認的配置,然後通過 SPI 的文件配置不同的實現,這樣就會存在一個接口多個實現的問題。要是找到多個實現的話,用哪個實現作爲最後的實例呢?
所以這裏使用 iterator 來獲取所有的實現類配置。剛纔已經在我們這個 super-logger 包裏增加了默認的 SuperLoggerConfiguration 實現。
爲了支持 YAML 配置,現在在使用方 / 用戶的代碼裏,增加一個 YAMLConfiguration 的 SPI 配置:
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:
com.github.kongwu.spisamples.ext.YAMLConfiguration
此時通過 iterator 方法,就會獲取到默認的 XMLConfiguration 和我們擴展的這個 YAMLConfiguration 兩個配置實現類了。
在上面那段加載的代碼裏,我們遍歷 iterator,遍歷到最後,我們 ** 使用最後一個實現配置作爲最終的實例。
再等等?最後一個?怎麼算最後一個?
使用方 / 用戶自定義的的這個 YAMLConfiguration 一定是最後一個嗎?
這個真的不一定,取決於我們運行時的 ClassPath 配置,在前面加載的 jar 自然在前,最後的 jar 裏的自然當然也在後面。所以如果用戶的包在 ClassPath 中的順序比 super-logger 的包更靠後,纔會處於最後一個位置;如果用戶的包位置在前,那麼所謂的最後一個仍然是默認的 XMLConfiguration。
舉個栗子,如果我們程序的啓動腳本爲:
java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main
默認的 XMLConfiguration SPI 配置在super-logger.jar
,擴展的 YAMLConfiguration SPI 配置文件在main.jar
,那麼 iterator 獲取的最後一個元素一定爲 YAMLConfiguration。
但這個 classpath 順序如果反了呢?main.jar 在前,super-logger.jar 在後
java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main
這樣一來,iterator 獲取的最後一個元素又變成了默認的 XMLConfiguration,我們使用 JDK SPI 沒啥意義了,獲取的又是第一個,還是默認的 XMLConfiguration。
由於這個加載順序(classpath)是由用戶指定的,所以無論我們加載第一個還是最後一個,都有可能會導致加載不到用戶自定義的那個配置。
所以這也是 JDK SPI 機制的一個劣勢,無法確認具體加載哪一個實現,也無法加載某個指定的實現,僅靠 ClassPath 的順序是一個非常不嚴謹的方式
Dubbo SPI
Dubbo 就是通過 SPI 機制加載所有的組件。不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模塊。基於 SPI,我們可以很容易的對 Dubbo 進行拓展。如果大家想要學習 Dubbo 的源碼,SPI 機制務必弄懂。接下來,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然後再來分析 Dubbo SPI 的源碼。
Dubbo 中實現了一套新的 SPI 機制,功能更強大,也更復雜一些。相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實現類。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內容如下(以下 demo 來自 dubbo 官方文檔)。
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
與 Java SPI 實現類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需加載指定的實現類。另外在使用時還需要在接口上標註 @SPI 註解。下面來演示 Dubbo SPI 的用法:
@SPI
public interface Robot {
void sayHello();
}
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
Dubbo SPI 和 JDK SPI 最大的區別就在於支持 “別名”,可以通過某個擴展點的別名來獲取固定的擴展點。就像上面的例子中,我可以獲取 Robot 多個 SPI 實現中別名爲 “optimusPrime” 的實現,也可以獲取別名爲 “bumblebee” 的實現,這個功能非常有用!
通過 @SPI 註解的 value 屬性,還可以默認一個 “別名” 的實現。比如在 Dubbo 中,默認的是 Dubbo 私有協議:dubbo protocol - dubbo://
**
來看看 Dubbo 中協議的接口:
@SPI("dubbo")
public interface Protocol {
......
}
在 Protocol 接口上,增加了一個 @SPI 註解,而註解的 value 值爲 Dubbo ,通過 SPI 獲取實現時就會獲取 Protocol SPI 配置中別名爲 dubbo 的那個實現,com.alibaba.dubbo.rpc.Protocol
文件如下:
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper
然後只需要通過 getDefaultExtension,就可以獲取到 @SPI 註解上 value 對應的那個擴展實現了
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol
還有一個 Adaptive 的機制,雖然非常靈活,但…… 用法並不是很 “優雅”,這裏就不介紹了
Dubbo 的 SPI 中還有一個 “加載優先級”,優先加載內置(internal)的,然後加載外部的(external),按優先級順序加載,如果遇到重複就跳過不會加載了。
所以如果想靠 classpath 加載順序去覆蓋內置的擴展,也是個不太理智的做法,原因同上 - 加載順序不嚴謹
Spring SPI
Spring 的 SPI 配置文件是一個固定的文件 - META-INF/spring.factories
,功能上和 JDK 的類似,每個接口可以有多個擴展實現,使用起來非常簡單:
//獲取所有factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories =
SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);
下面是一段 Spring Boot 中 spring.factories 的配置
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver
......
Spring SPI 中,將所有的配置放到一個固定的文件中,省去了配置一大堆文件的麻煩。至於多個接口的擴展配置,是用一個文件好,還是每個單獨一個文件好這個,這個問題就見仁見智了(個人喜歡 Spring 這種,乾淨利落)。
Spring 的 SPI 雖然屬於 spring-framework(core),但是目前主要用在 spring boot 中……
和前面兩種 SPI 機制一樣,Spring 也是支持 ClassPath 中存在多個 spring.factories 文件的,加載時會按照 classpath 的順序依次加載這些 spring.factories 文件,添加到一個 ArrayList 中。由於沒有別名,所以也沒有去重的概念,有多少就添加多少。
但由於 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 會優先加載項目中的文件,而不是依賴包中的文件。所以如果在你的項目中定義個 spring.factories 文件,那麼你項目中的文件會被第一個加載,得到的 Factories 中,項目中 spring.factories 裏配置的那個實現類也會排在第一個
如果我們要擴展某個接口的話,只需要在你的項目(spring boot)裏新建一個META-INF/spring.factories
文件,只添加你要的那個配置,不要完整的複製一遍 Spring Boot 的 spring.factories 文件然後修改
**
比如我只想添加一個新的 LoggingSystemFactory 實現,那麼我只需要新建一個META-INF/spring.factories
文件,而不是完整的複製 + 修改:
org.springframework.boot.logging.LoggingSystemFactory=\
com.example.log4j2demo.Log4J2LoggingSystem.Factory
對
JDK SPI
DUBBO SPI
Spring SPI
參考
-
Introduction to the Service Provider Interfaces - Oracle
-
Dubbo SPI - Apache Dubbo
-
Creating Your Own Auto-configuration - Spring
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/SI5q53dSa5GIuTC9l0E-CA