Home > Java > javaTutorial > body text

What are the differences between the three SPI mechanisms of Java Spring Dubbo

王林
Release: 2023-05-16 08:34:05
forward
1379 people have browsed it

What is SPI used for?

For example, now we have designed a new logging framework: "super-logger". By default, XML files are used as the configuration files of our log, and an interface for configuration file parsing is designed:

package com.github.kongwu.spisamples;

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

Then a default XML implementation:

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

Then when we initialize and parse the configuration, we only need to call this XMLConfiguration to parse the XML configuration file

package com.github.kongwu.spisamples;

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

public static getLogger(Class clazz){
......
}
}
Copy after login

In this way, a basic model is completed, and there seems to be no problem. However, the scalability is not very good, because if I want to customize/extend/rewrite the parsing function, I have to redefine the entry code and rewrite the LoggerFactory. It is not flexible enough and too intrusive.

For example, if the user/user now wants to add a yml file as a log configuration file, then he only needs to create a new YAMLConfiguration and implement SuperLoggerConfiguration. But...how to inject it, how to use the newly created YAMLConfiguration in LoggerFactory? Is it possible that even LoggerFactory has been rewritten?

If you use the SPI mechanism, this matter will be very simple, and the expansion function of this entry can be easily completed.

Let’s first take a look at how to use the SPI mechanism of JDK to solve the above scalability problem.

JDK SPI

JDK provides an SPI function, and the core class is java.util.ServiceLoader. Its function is to obtain multiple configuration implementation files under "META-INF/services/" through the class name.

In order to solve the above expansion problem, now we create a com.github.kongwu.spisamples.SuperLoggerConfiguration file under META-INF/services/ (no suffix ). There is only one line of code in the file, which is our default com.github.kongwu.spisamples.XMLConfiguration (note that multiple implementations can also be written in one file, separated by carriage returns)

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

com.github.kongwu.spisamples.XMLConfiguration
Copy after login

Then get the implementation class of our SPI mechanism configuration through ServiceLoader:

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);
Copy after login

Finally, adjust the initialization configuration method in LoggerFactory to the current SPI method:

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){
......
}
}
Copy after login

"Wait, why is iterator used here? Instead of a method like get that only obtains one instance?"

Just imagine, if it is a fixed get method , then what is obtained is a fixed instance, what is the meaning of SPI?

The purpose of SPI is to enhance scalability. Extract the fixed configuration and configure it through the SPI mechanism. In that case, there is usually a default configuration, and then different implementations are configured through SPI files, so there will be a problem of multiple implementations of one interface. If multiple implementations are found, which implementation is used as the final instance?

So iterator is used here to obtain all implementation class configurations. The default SuperLoggerConfiguration implementation has just been added to our "super-logger" package.

In order to support YAML configuration, now add a YAMLConfiguration SPI configuration in the user/user code:

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

com.github.kongwu.spisamples.ext.YAMLConfiguration
Copy after login

At this time, it will be obtained through the iterator method There are two configuration implementation classes: the default XMLConfiguration and the YAMLConfiguration we extended.

In the above loaded code, we traverse the iterator, and until the end, we use the last implementation configuration as the final instance.

"Wait a minute? The last one? How to count the last one?"

Is the user/user-defined YAMLConfiguration necessarily the last one?

This is really not necessarily true. It depends on the ClassPath configuration when we run. The jars loaded in the front are naturally in the front, and the jars in the last jar are naturally in the back. So "If the user's package is later in the ClassPath than the super-logger package, it will be in the last position; if the user's package is in the front, then the so-called last one is still the default XMLConfiguration. 》

For example, if the startup script of our program is:

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

The default XMLConfiguration SPI configuration is in super-logger.jar, the extended YAMLConfiguration SPI configuration file is in main.jar, then the last element obtained by the iterator must be YAMLConfiguration.

But what if the classpath order is reversed? main.jar in the front, super-logger.jar in the back

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

In this way, the last element obtained by the iterator becomes the default XMLConfiguration. It makes no sense for us to use JDK SPI, and the obtained Is it the first one, or the default XMLConfiguration.

Since the loading order (classpath) is specified by the user, whether we load the first or the last one, it may result in the user-defined configuration not being loaded.

"So this is also a disadvantage of the JDK SPI mechanism. It is impossible to confirm which implementation is loaded, and it is also impossible to load a specified implementation. Relying solely on the order of ClassPath is a very imprecise method."

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
Copy after login

与 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();
}
}
Copy after login

「Dubbo SPI 和 JDK SPI 最大的区别就在于支持“别名”」,可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取 Robot 多个 SPI 实现中别名为“optimusPrime”的实现,也可以获取别名为“bumblebee”的实现,这个功能非常有用!

通过 @SPI 注解的 value 属性,还可以默认一个“别名”的实现。比如在Dubbo 中,默认的是Dubbo 私有协议:「dubbo protocol - dubbo://」**

来看看Dubbo中协议的接口:

@SPI("dubbo")
public interface Protocol {
......
}
Copy after login

在 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
Copy after login

然后只需要通过getDefaultExtension,就可以获取到 @SPI 注解上value对应的那个扩展实现了

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol
Copy after login

还有一个 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);
Copy after login

下面是一段 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

......
Copy after login

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
Copy after login

对比

  • JDK SPI

  • DUBBO SPI

  • Spring SPI

 


文件方式

每个扩展点单独一个文件

每个扩展点单独一个文件

所有的扩展点在一个文件

获取某个固定的实现

Not supported, you can only get all implementations in order

There is the concept of "alias", you can get a fixed implementation of the extension point by name, It is very convenient to use the annotations of Dubbo SPI

is not supported, you can only get all implementations in order. However, since Spring Boot ClassLoader will give priority to loading files in user code, it can ensure that the user-defined spring.factoires file is first, and the custom extension can be fixedly obtained by obtaining the first factory

##Others

None

Supports dependency injection inside Dubbo through the directory Distinguish Dubbo's built-in SPI and external SPI, load the internal one first, and ensure that the internal one has the highest priority

None

Document Completeness

Articles & third-party information are rich enough

Documents& Third-party information are rich enough

The documentation is not rich enough, but due to the few functions, it is very simple to use

IDE support

None

None

IDEA perfect support, with syntax prompts

Comparison of three SPI mechanisms Among them, the built-in mechanism of JDK is the weakest, but because it is built-in in JDK, it still has certain application scenarios. After all, there is no need for additional dependencies; Dubbo has the richest functions, but the mechanism is a bit complicated, and it can only be used with Dubbo. , cannot be completely regarded as an independent module; the functions of Spring are almost the same as those of JDK. The biggest difference is that all extension points are written in a spring.factories file, which is also an improvement, and IDEA perfectly supports syntax prompts.

The above is the detailed content of What are the differences between the three SPI mechanisms of Java Spring Dubbo. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:yisu.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template