首頁  >  文章  >  Java  >  Java Spring Dubbo三種SPI機制的差異是什麼

Java Spring Dubbo三種SPI機制的差異是什麼

王林
王林轉載
2023-05-16 08:34:051317瀏覽

SPI 有什麼用?

舉個栗子,現在我們設計了一個全新的日誌框架:「super-logger」。預設以XML檔案作為我們這款日誌的設定文件,並設計了一個設定檔解析的介面:

package com.github.kongwu.spisamples;

public interface SuperLoggerConfiguration {
void configure(String configFile);
}

然後來一個預設的XML實作:

package com.github.kongwu.spisamples;
public class XMLConfiguration implements SuperLoggerConfiguration{
public void configure(String configFile){
......
}
}

那麼我們在初始化,解析設定時,只需要呼叫這個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

 


文件方式

每个扩展点单独一个文件

每个扩展点单独一个文件

所有的扩展点在一个文件

获取某个固定的实现

不支持,只能依序取得所有實作

有「別名」的概念,可以透過名稱取得擴充點的某個固定實現,配合Dubbo SPI的註解很方便

不支持,只能依序取得所有實作。但由於Spring Boot ClassLoader會優先加載用戶代碼中的文件,所以可以保證用戶自定義的spring.factoires文件在第一個,通過獲取第一個factory的方式就可以固定獲取自定義的擴展

其他

#支援Dubbo內部的依賴注入,透過目錄來區分Dubbo 內建SPI和外部SPI,優先載入內部,確保內部的優先順序最高

#無

##文檔完整度

文章& 三方資料夠豐富

#文件& 三方資料夠豐富

文件不夠豐富,但由於功能少,使用非常簡單

#IDE支援

IDEA 完美支持,有語法提示

三種SPI 機制對比之下,JDK 內置的機制是最弱雞的,但是由於是JDK 內置,所以還是有一定應用場景,畢竟不用額外的依賴;Dubbo 的功能最豐富,但機制有點複雜了,而且只能配合Dubbo 使用,不能完全算是一個獨立的模組;Spring 的功能和JDK的相差無幾,最大的區別是所有擴展點寫在一個spring.factories 文件中,也算是一個改進,並且IDEA 完美支持語法提示。

以上是Java Spring Dubbo三種SPI機制的差異是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除