>  기사  >  Java  >  Java Spring Dubbo의 세 가지 SPI 메커니즘 간의 차이점은 무엇입니까?

Java Spring Dubbo의 세 가지 SPI 메커니즘 간의 차이점은 무엇입니까?

王林
王林앞으로
2023-05-16 08:34:051382검색

SPI는 어떤 용도로 사용되나요?

예를 들어, 이제 우리는 새로운 로깅 프레임워크인 "슈퍼 로거"를 설계했습니다. 기본적으로 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
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.jar

그런 다음 ServiceLoader를 전달하여 SPI 메커니즘 구성의 구현 클래스를 얻습니다.

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

마지막으로 LoggerFactory에서 구성을 초기화하는 방법이 현재 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();
}
}

"잠깐, 왜 그렇습니까? 여기서는 get 대신에 iterator를 사용하나요? "

🎜고정된 get 메서드라면 얻는 것이 고정된 인스턴스라는 것이 무슨 뜻인가요? 🎜🎜SPI의 목적은 확장성을 높이는 것입니다. 고정 구성을 추출하고 SPI 메커니즘을 통해 구성합니다. 이 경우 일반적으로 기본 구성이 있고 SPI 파일을 통해 서로 다른 구현이 구성되므로 하나의 인터페이스를 여러 개 구현하는 문제가 발생합니다. 여러 구현이 발견되면 어떤 구현이 최종 인스턴스로 사용됩니까? 🎜🎜여기에서는 반복자를 사용하여 모든 구현 클래스 구성을 가져옵니다. 기본 SuperLoggerConfiguration 구현이 🎜"super-logger"🎜 패키지에 추가되었습니다. 🎜🎜🎜YAML 구성을 지원하려면 이제 사용자/사용자 코드에 YAMLConfiguration SPI 구성을 추가하세요. 🎜🎜
@SPI("dubbo")
public interface Protocol {
......
}
🎜 이때 반복자 메서드를 통해 기본 XMLConfiguration과 확장된 YAMLConfiguration을 가져옵니다. 구현 구성 수업. 🎜🎜위의 로드된 코드에서는 반복자를 순회하고 마지막까지 마지막 구현 구성을 최종 인스턴스로 사용합니다. 🎜🎜🎜"잠깐만요? 마지막 항목? 마지막 항목을 어떻게 계산하나요?"🎜🎜🎜사용자/사용자가 정의한 이 YAML 구성이 반드시 마지막 항목인가요? 🎜🎜이것은 실제로 실행할 때 ClassPath 구성에 따라 달라지며 앞쪽에 로드된 항아리는 자연스럽게 앞쪽에 있고 마지막 항아리에 있는 항아리는 자연스럽게 뒤쪽에 있습니다. 따라서🎜"사용자 패키지가 Super-logger 패키지보다 ClassPath 뒤에 있으면 마지막 위치에 있게 됩니다. 사용자 패키지가 앞에 있으면 소위 마지막 패키지가 여전히 기본 XMLConfiguration입니다." 🎜 🎜🎜🎜예를 들어 프로그램의 시작 스크립트가 🎜🎜
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
🎜인 경우 기본 XMLConfiguration SPI 구성은 super-logger.jar에 있고 확장 YAMLConfiguration SPI 구성 파일은 main.jar인 경우 반복자가 얻은 마지막 요소는 YAMLConfiguration이어야 합니다. 🎜🎜하지만 클래스패스 순서가 반대라면 어떻게 될까요? main.jar이 앞쪽에 있고, super-logger.jar이 뒤쪽에 있습니다🎜
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol
🎜이런 식으로 iterator에서 얻은 마지막 요소가 기본 XMLConfiguration이 됩니다. JDK SPI를 사용하는 것은 의미가 없습니다. 얻은 요소는 다시 이거나 기본 XMLConfiguration입니다. 🎜🎜로딩 순서(클래스 경로)는 사용자가 지정하므로 첫 번째 로드이든 마지막 로드이든 사용자가 정의한 구성이 로드되지 않을 수 있습니다. 🎜🎜🎜"그래서 이것도 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 파일이 첫 번째임을 보장할 수 있으며 첫 번째 팩토리를 획득하여 사용자 정의 확장을 안정적으로 얻을 수 있습니다

기타

None

Dubbo 내부 종속성 주입 지원, 디렉터리를 통해 Dubbo 내장 SPI와 외부 SPI 구별, 내부 SPI를 먼저 로드하고 내부 SPI의 우선순위가 가장 높도록 보장

None

문서 완성도

문서 및 타사 자료가 충분히 풍부함

문서 및 타사 자료가 충분히 풍부함

문서가 충분히 풍부하지는 않지만 기능이 적기 때문에 사용이 매우 간단함

IDE 지원

None

None

IDEA 완벽한 지원, 구문 프롬프트 포함

3개의 S와 비교 PI 메커니즘, JDK 내장- in 메커니즘이 가장 약하지만 JDK에 내장되어 있기 때문에 여전히 특정 애플리케이션 시나리오가 있으므로 결국 추가 종속성이 필요하지 않습니다. Dubbo는 가장 풍부한 기능을 가지고 있지만 메커니즘은 약간 복잡하며 다음과 같습니다. Dubbo와 함께 사용되며 완전히 독립적인 모듈로 간주될 수 없습니다. Spring의 기능은 JDK의 기능과 거의 동일하며 가장 큰 차이점은 모든 확장 지점이 spring.factories 파일에 작성된다는 점입니다. IDEA는 구문 프롬프트를 완벽하게 지원합니다.

위 내용은 Java Spring Dubbo의 세 가지 SPI 메커니즘 간의 차이점은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제