Heim  >  Artikel  >  Java  >  Was sind die Unterschiede zwischen den drei SPI-Mechanismen von Java Spring Dubbo?

Was sind die Unterschiede zwischen den drei SPI-Mechanismen von Java Spring Dubbo?

王林
王林nach vorne
2023-05-16 08:34:051368Durchsuche

Wofür wird SPI verwendet?

Zum Beispiel haben wir jetzt ein neues Protokollierungs-Framework entworfen: „Super-Logger“. Standardmäßig wird eine XML-Datei als Konfigurationsdatei unseres Protokolls verwendet, und eine Schnittstelle zum Parsen der Konfigurationsdatei wird entworfen:

package com.github.kongwu.spisamples;

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

Dann gibt es eine Standard-XML-Implementierung:

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

Dann, wenn wir initialisieren und analysieren die Konfiguration, Sie müssen nur diese XMLConfiguration aufrufen, um die XML-Konfigurationsdatei zu analysieren

package com.github.kongwu.spisamples;

public class LoggerFactory {
static {
SuperLoggerConfiguration configuration = new XMLConfiguration();
configuration.configure(configFile);
}

public static getLogger(Class clazz){
......
}
}

Auf diese Weise ist ein Grundmodell fertiggestellt und es scheint kein Problem zu geben. Allerdings ist die Skalierbarkeit nicht sehr gut, denn wenn ich die Parsing-Funktion anpassen/erweitern/neu schreiben möchte, muss ich den Eingabecode neu definieren und die LoggerFactory neu schreiben. Dies ist nicht flexibel genug und zu aufdringlich.

Wenn der Benutzer/Benutzer nun beispielsweise eine yml-Datei als Protokollkonfigurationsdatei hinzufügen möchte, muss er nur eine neue YAMLConfiguration erstellen und SuperLoggerConfiguration implementieren. Aber ... wie fügt man es ein, wie verwendet man die neu erstellte YAMLConfiguration in LoggerFactory? Ist es möglich, dass sogar LoggerFactory neu geschrieben wurde?

Wenn Sie den SPI-Mechanismus verwenden, ist dieser Vorgang sehr einfach und die Erweiterungsfunktion dieses Eingangs kann problemlos abgeschlossen werden.

Werfen wir zunächst einen Blick darauf, wie der SPI-Mechanismus von JDK zur Lösung des oben genannten Skalierbarkeitsproblems verwendet werden kann.

JDK SPI

JDK bietet eine SPI-Funktion und die Kernklasse ist java.util.ServiceLoader. Seine Funktion besteht darin, über den Klassennamen mehrere Konfigurationsimplementierungsdateien unter „META-INF/services/“ abzurufen.

Um das obige Erweiterungsproblem zu lösen, erstellen wir jetzt eine com.github.kongwu.spisamples.SuperLoggerConfiguration-Datei (ohne Suffix) unter META-INF/services/ . Es gibt nur eine Codezeile in der Datei, die unsere Standard-com.github.kongwu.spisamples.XMLConfiguration ist (beachten Sie, dass auch mehrere Implementierungen in eine Datei geschrieben werden können, getrennt durch Wagenrückläufe)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

java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main

Dann erhält ServiceLoader die Implementierungsklasse unserer SPI-Mechanismuskonfiguration:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

Schließlich wird die Art und Weise zum Initialisieren der Konfiguration in der LoggerFactory an die aktuelle SPI-Methode angepasst:

@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();
}
}
„Warten Sie, warum? Wird hier ein Iterator verwendet? Eine Methode einer Klasse, die nur eine Instanz erhält? „Stellen Sie sich vor, wenn es sich um eine feste Get-Methode handelt, was bedeutet dann eine feste Instanz?

🎜Der Zweck von SPI besteht darin, die Skalierbarkeit zu verbessern. Extrahieren Sie die feste Konfiguration und konfigurieren Sie sie über den SPI-Mechanismus. In diesem Fall gibt es normalerweise eine Standardkonfiguration und dann werden unterschiedliche Implementierungen über SPI-Dateien konfiguriert, sodass das Problem mehrerer Implementierungen einer Schnittstelle auftritt. Wenn mehrere Implementierungen gefunden werden, welche Implementierung wird als letzte Instanz verwendet? 🎜🎜Daher wird hier ein Iterator verwendet, um alle Implementierungsklassenkonfigurationen abzurufen. Die standardmäßige SuperLoggerConfiguration-Implementierung wurde gerade zu unserem 🎜„Super-Logger“🎜-Paket hinzugefügt. 🎜🎜🎜Um die YAML-Konfiguration zu unterstützen, fügen Sie nun eine YAMLConfiguration-SPI-Konfiguration im Benutzer-/Benutzercode hinzu: 🎜🎜
@SPI("dubbo")
public interface Protocol {
......
}
🎜Zu diesem Zeitpunkt erhalten Sie über die Iteratormethode die Standard-XML-Konfiguration und die von uns erweiterte YAMLConfiguration. Konfigurieren Sie die Implementierung Klasse. 🎜🎜Im oben geladenen Code durchlaufen wir den Iterator und verwenden bis zum Ende die letzte Implementierungskonfiguration als letzte Instanz. 🎜🎜🎜"Moment mal? Der letzte? Wie zählt man den letzten?"🎜🎜🎜Ist diese vom Benutzer/Benutzer definierte YAMLConfiguration unbedingt die letzte? 🎜🎜Das ist wirklich nicht unbedingt wahr. Es hängt von der ClassPath-Konfiguration ab, wenn wir die Gläser vorne laden, und die Gläser im letzten Glas natürlich hinten. Also🎜"Wenn sich das Paket des Benutzers später im ClassPath als das Super-Logger-Paket befindet, befindet es sich an der letzten Position. Wenn sich das Paket des Benutzers vorne befindet, ist das sogenannte letzte Paket immer noch die Standard-XML-Konfiguration." 🎜 🎜🎜🎜Zum Beispiel, wenn das Startskript unseres Programms lautet: 🎜🎜
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
🎜Die Standard-XMLConfiguration-SPI-Konfiguration befindet sich in super-logger.jar und die erweiterte YAMLConfiguration-SPI-Konfigurationsdatei befindet sich in main.jar, dann muss das letzte vom Iterator erhaltene Element YAMLConfiguration sein. 🎜🎜Aber was ist, wenn die Reihenfolge der Klassenpfade umgekehrt ist? main.jar steht vorne, super-logger.jar steht hinten🎜
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol
🎜Auf diese Weise wird das letzte vom Iterator erhaltene Element zur Standard-XML-Konfiguration. Es macht für uns keinen Sinn, JDK SPI zu verwenden, und das erste Das erhaltene Element ist wieder oder die Standard-XMLConfiguration. 🎜🎜Da die Ladereihenfolge (Klassenpfad) vom Benutzer festgelegt wird, kann es dazu führen, dass die benutzerdefinierte Konfiguration nicht geladen wird, unabhängig davon, ob wir die erste oder die letzte laden. 🎜🎜🎜„Das ist also auch ein Nachteil des JDK-SPI-Mechanismus. Es ist unmöglich zu bestätigen, welche Implementierung geladen wird, und es ist auch unmöglich, eine bestimmte Implementierung zu laden. Sich ausschließlich auf die Reihenfolge von ClassPath zu verlassen, ist eine sehr ungenaue Methode.“ 🎜🎜

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

IDE-UnterstützungIm Vergleich zu drei SPI-Mechanismen, das JDK-integrierte Der Mechanismus ist der schwächste, aber da er in JDK integriert ist, gibt es immer noch bestimmte Anwendungsszenarien. Schließlich sind keine zusätzlichen Abhängigkeiten erforderlich. Dubbo verfügt über die umfangreichsten Funktionen, aber der Mechanismus ist etwas kompliziert, und das kann nur sein Wird mit Dubbo verwendet und kann nicht vollständig als unabhängiges Modul betrachtet werden. Die Funktionen von Spring sind fast die gleichen wie die von JDK, und der größte Unterschied besteht darin, dass alle Erweiterungspunkte in eine spring.factories-Datei geschrieben werden, was ebenfalls eine Verbesserung darstellt. und IDEA unterstützt perfekt Syntax-Eingabeaufforderungen.
 


文件方式

每个扩展点单独一个文件

每个扩展点单独一个文件

所有的扩展点在一个文件

获取某个固定的实现

Nicht unterstützt, Sie können nur alle Implementierungen in der richtigen Reihenfolge abrufen

Es gibt das Konzept des „Alias“, Sie können eine feste Implementierung des Erweiterungspunkts anhand des Namens erhalten, es ist sehr praktisch, mit Dubbo SPI-Annotationen zusammenzuarbeiten

Nicht unterstützt. Alle Implementierungen können nur in der angegebenen Reihenfolge bezogen werden. Da Spring Boot ClassLoader jedoch dem Laden von Dateien im Benutzercode Priorität einräumt, kann sichergestellt werden, dass die benutzerdefinierte Datei spring.factoires an erster Stelle steht und die benutzerdefinierte Erweiterung durch den Erhalt der ersten Factory fest abgerufen werden kann

Andere

Keine

Unterstützt die Abhängigkeitsinjektion innerhalb von Dubbo, unterscheidet Dubbo integriertes SPI und externes SPI anhand von Verzeichnissen, lädt die internen zuerst und stellt sicher, dass die internen die höchste Priorität haben

Keine

Vollständigkeit der Dokumente

Artikel und Materialien von Drittanbietern sind umfangreich genug.

Dokumente und Materialien von Drittanbietern sind umfangreich genug sehr einfach zu bedienen

Keine

Keine

IDEA perfekte Unterstützung, mit Syntax-Eingabeaufforderungen

Das obige ist der detaillierte Inhalt vonWas sind die Unterschiede zwischen den drei SPI-Mechanismen von Java Spring Dubbo?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:yisu.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen