Maison  >  Article  >  Java  >  Quelles sont les différences entre les trois mécanismes SPI de Java Spring Dubbo

Quelles sont les différences entre les trois mécanismes SPI de Java Spring Dubbo

王林
王林avant
2023-05-16 08:34:051381parcourir

A quoi sert le SPI ?

Par exemple, nous avons maintenant conçu un nouveau framework de journalisation : 「super-logger」. Par défaut, le fichier XML est utilisé comme fichier de configuration de notre journal, et une interface pour l'analyse du fichier de configuration est conçue :

package com.github.kongwu.spisamples;

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

Ensuite, un XML par défaut Implémentation :

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

Ensuite, lorsque nous initialisons et analysons la configuration, il nous suffit d'appeler cette XMLConfiguration pour analyser le fichier de configuration XML

package com.github.kongwu.spisamples;

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

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

Ceci complète un le modèle de base semble bien. Cependant, l'évolutivité n'est pas très bonne, car si je souhaite personnaliser/étendre/réécrire la fonction d'analyse, je dois redéfinir le code d'entrée et réécrire le LoggerFactory. Ce n'est pas assez flexible et trop intrusif.

Par exemple, si l'utilisateur souhaite maintenant ajouter un fichier yml en tant que fichier de configuration du journal, il lui suffit alors de créer un nouveau YAMLConfiguration et d'implémenter SuperLoggerConfiguration. Mais... comment l'injecter, comment utiliser la configuration YAML nouvellement créée dans LoggerFactory ? Est-il possible que même LoggerFactory ait été réécrit ?

Si vous utilisez le mécanisme SPI, cette affaire sera très simple et la fonction d'expansion de cette entrée pourra être facilement complétée.

Voyons d'abord comment utiliser le mécanisme SPI du JDK pour résoudre le problème d'évolutivité ci-dessus.

JDK SPI

JDK fournit une fonction SPI et la classe principale est java.util.ServiceLoader. Sa fonction est d'obtenir plusieurs fichiers d'implémentation de configuration sous "META-INF/services/" via le nom de la classe.

Afin de résoudre le problème d'expansion ci-dessus, nous créons maintenant un com.github.kongwu.spisamples.SuperLoggerConfiguration sous META-INF/services/ fichier (sans suffixe). Il n'y a qu'une seule ligne de code dans le fichier, qui est notre com.github.kongwu.spisamples.XMLConfiguration par défaut (notez que plusieurs implémentations peuvent également être écrites dans un seul fichier, séparées par des retours chariot) # 🎜🎜#

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<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;

while(iterator.hasNext()) {
//加载并初始化实现类
configuration = iterator.next();
}

//对最后一个configuration类调用configure方法
configuration.configure(configFile);

然后通过 ServiceLoader 获取我们的 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){
......
}
}

最后在调整LoggerFactory中初始化配置的方式为现在的SPI方式:

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:

com.github.kongwu.spisamples.ext.YAMLConfiguration

「等等,这里为什么是用 iterator ? 而不是get之类的只获取一个实例的方法?」

试想一下,如果是一个固定的get方法,那么get到的是一个固定的实例,SPI 还有什么意义呢?

SPI 的目的,就是增强扩展性。将固定的配置提取出来,通过 SPI 机制来配置。那既然如此,一般都会有一个默认的配置,然后通过 SPI 的文件配置不同的实现,这样就会存在一个接口多个实现的问题。要是找到多个实现的话,用哪个实现作为最后的实例呢?

所以这里使用iterator来获取所有的实现类配置。刚才已经在我们这个 「super-logger」 包里增加了默认的SuperLoggerConfiguration 实现。

为了支持 YAML 配置,现在在使用方/用户的代码里,增加一个YAMLConfiguration的 SPI 配置:

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

此时通过iterator方法,就会获取到默认的XMLConfiguration和我们扩展的这个YAMLConfiguration两个配置实现类了。

在上面那段加载的代码里,我们遍历iterator,遍历到最后,我们**使用最后一个实现配置作为最终的实例。

「再等等?最后一个?怎么算最后一个?」

使用方/用户自定义的的这个 YAMLConfiguration 一定是最后一个吗?

这个真的不一定,取决于我们运行时的 ClassPath 配置,在前面加载的jar自然在前,最后的jar里的自然当然也在后面。所以「如果用户的包在ClassPath中的顺序比super-logger的包更靠后,才会处于最后一个位置;如果用户的包位置在前,那么所谓的最后一个仍然是默认的XMLConfiguration。」

举个栗子,如果我们程序的启动脚本为:

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

默认的XMLConfiguration SPI配置在super-logger.jar,扩展的YAMLConfiguration SPI配置文件在main.jarEnsuite, récupérez la classe d'implémentation de la configuration de notre mécanisme SPI via ServiceLoader :

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

Enfin, ajustez la manière d'initialiser la configuration dans LoggerFactory à la méthode SPI actuelle :

@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();
}
}

"Attendez, pourquoi l'itérateur est-il utilisé ici ? Au lieu d'une méthode comme get qui n'obtient qu'une seule instance ?" ##🎜🎜 #Imaginez, s'il s'agit d'une méthode get fixe, alors ce qui est obtenu est une instance fixe. Quelle est la signification de SPI ?

Le but de SPI est d'améliorer l'évolutivité. Extrayez la configuration fixe et configurez-la via le mécanisme SPI. Dans ce cas, il existe généralement une configuration par défaut, puis différentes implémentations sont configurées via des fichiers SPI, il y aura donc un problème de plusieurs implémentations d'une même interface. Si plusieurs implémentations sont trouvées, quelle implémentation est utilisée comme instance finale ? #🎜🎜##🎜🎜#L'itérateur est donc utilisé ici pour obtenir toutes les configurations de classe d'implémentation. L'implémentation par défaut de SuperLoggerConfiguration vient d'être ajoutée à notre package #🎜🎜#『super-logger』#🎜🎜#. #🎜🎜##🎜🎜##🎜🎜#Afin de prendre en charge la configuration YAML, ajoutez maintenant une configuration YAMLConfiguration SPI dans le code utilisateur/utilisateur : #🎜🎜##🎜🎜#
@SPI("dubbo")
public interface Protocol {
......
}
#🎜🎜#Passé à ce moment-là par l'itérateur méthode, vous obtiendrez deux classes d'implémentation de configuration, la XMLConfiguration par défaut et la YAMLConfiguration que nous avons étendue. #🎜🎜##🎜🎜#Dans le code chargé ci-dessus, nous parcourons l'itérateur jusqu'à la fin et nous utilisons la dernière configuration d'implémentation comme instance finale. #🎜🎜##🎜🎜##🎜🎜#"Attendez une minute ? Le dernier ? Comment compter le dernier ?" #🎜🎜##🎜🎜##🎜🎜#Cette configuration YAML définie par l'utilisateur/utilisateur doit être le dernier ? #🎜🎜##🎜🎜#Ce n'est vraiment pas nécessairement vrai. Cela dépend de la configuration de ClassPath lorsque nous exécutons. Les pots chargés à l'avant sont naturellement à l'avant, et les pots du dernier pot sont naturellement à l'arrière. Donc #🎜🎜# "Si le package de l'utilisateur est plus tard dans le ClassPath que le package du super-logger, il sera en dernière position ; si le package de l'utilisateur est au premier plan, alors le soi-disant dernier est toujours la valeur par défaut XMLConfiguration ”#🎜🎜##🎜🎜##🎜🎜##🎜🎜#Par exemple, si le script de démarrage de notre programme est : #🎜🎜##🎜🎜#
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
#🎜🎜#La configuration SPI XMLConfiguration par défaut est super-logger.jar, le fichier de configuration étendu YAMLConfiguration SPI est dans main.jar, alors le dernier élément obtenu par l'itérateur doit être YAMLConfiguration. #🎜🎜##🎜🎜#Mais que se passe-t-il si l'ordre des chemins de classe est inversé ? main.jar est devant, super-logger.jar est à l'arrière #🎜🎜#
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol
#🎜🎜#De cette façon, le dernier élément obtenu par l'itérateur devient la configuration XML par défaut pour nous d'utiliser JDK SPI. . Get C'est encore le premier, toujours la configuration XML par défaut. #🎜🎜##🎜🎜#Étant donné que l'ordre de chargement (classpath) est spécifié par l'utilisateur, que nous chargions le premier ou le dernier, cela peut entraîner le non-chargement de la configuration définie par l'utilisateur. #🎜🎜##🎜🎜##🎜🎜#"C'est donc aussi un inconvénient du mécanisme JDK SPI. Il ne peut pas confirmer quelle implémentation est chargée, ni charger une implémentation spécifiée. L'ordre de ClassPath seul est très imprécis. façon" #🎜🎜##🎜🎜#

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

 


文件方式

每个扩展点单独一个文件

每个扩展点单独一个文件

所有的扩展点在一个文件

获取某个固定的实现

Non pris en charge, vous ne pouvez obtenir toutes les implémentations que dans l'ordre

Il y a le concept "alias", vous pouvez obtenir une implémentation fixe du point d'extension par son nom, il est très pratique de coopérer avec les annotations Dubbo SPI

Non pris en charge, toutes les implémentations ne peuvent être obtenues que dans l'ordre. Cependant, étant donné que Spring Boot ClassLoader donnera la priorité au chargement des fichiers dans le code utilisateur, il peut garantir que le fichier spring.factoires défini par l'utilisateur est le premier et que l'extension personnalisée peut être obtenue de manière fixe en obtenant la première usine

Autres

Aucun

Prend en charge l'injection de dépendances dans Dubbo, distingue le SPI intégré de Dubbo et le SPI externe via des répertoires, charge les internes en premier et s'assure que les internes ont la priorité la plus élevée

Aucun

Exhaustivité du document

Les articles et les documents tiers sont suffisamment riches

Les documents et les documents tiers sont suffisamment riches

Les documents ne sont pas assez riches, mais en raison du peu de fonctions, il est très simple à utiliser

Support IDE

Aucun

Aucun

Support parfait IDEA, avec des invites de syntaxe

Par rapport aux trois Mécanismes SPI, le JDK construit -in est le plus faible, mais comme il est intégré au JDK, il existe encore certains scénarios d'application. Après tout, aucune dépendance supplémentaire n'est nécessaire. Dubbo a les fonctions les plus riches, mais le mécanisme est un peu compliqué et il ne peut que le faire. être utilisé avec Dubbo et ne peut pas être complètement considéré comme un module indépendant ; les fonctions de Spring sont presque les mêmes que celles du JDK, et la plus grande différence est que tous les points d'extension sont écrits dans un fichier spring.factories, ce qui est également une amélioration , et IDEA prend parfaitement en charge les invites de syntaxe.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer