龙空技术网

SPI-服务发现机制详解

编程少年 124

前言:

此时同学们对“服务发现和服务注册”大致比较关切,姐妹们都想要知道一些“服务发现和服务注册”的相关文章。那么小编也在网上收集了一些关于“服务发现和服务注册””的相关资讯,希望看官们能喜欢,你们一起来了解一下吧!

什么是SPI?

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

SPI 有什么作用?

在 Java 中,SPI(Service Provider Interface)机制是一种服务提供者注册的标准方式。该机制通过在类路径下查找可用的实现,从而实现了服务的动态发现和加载。它可以帮助开发人员实现“插拔式”编程,即在不修改代码的情况下,动态地替换掉某个接口的实现。这种设计方式可以使应用程序更加灵活、可扩展和可配置。

SPI 组成部分服务接口:定义服务(或者 API)的接口,实现这个接口需要提供一个或多个服务提供者。服务提供者:提供了服务实现的具体类,它们需要继承自服务接口,并且在 META-INF/services 目录中通过简单的文本文件来指定自己的实现了。服务加载器:运行时动态地查找并加载实现了指定服务接口的所有服务提供者。JDK SPI 如何使用服务接口: 定义需要实现或者扩展的接口。com.example.HelloService

public interface HelloService {    void sayHello();}
服务提供者:创建两个服务提供者类实现该接口,并在指定目录src/main/resources/META-INF/services下通过简单的文本文件来指定实现的接口。com.example.HelloServiceImpl1
public class HelloServiceImpl1 implements HelloService {    @Override    public void sayHello() {        System.out.println("Hello from HelloServiceImpl1");    }}
com.example.HelloServiceImpl2
public class HelloServiceImpl2 implements HelloService {    @Override    public void sayHello() {        System.out.println("Hello from HelloServiceImpl2");    }}
src/main/resources/META-INF/services 目录下文件 om.example.HelloService
com.example.HelloServiceImpl1com.example.HelloServiceImpl2

注意:文件名是接口全路径名称,文件内容是实现接口的全路径名称。

服务加载器:使用服务加载器加载并使用服务提供者,JDK中的调用java.util.ServiceLoader#load 方法来加载接口。

public static void main(String[] args) {        // 使用 ServiceLoader 加载 HelloService 接口的实现类        ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);  			Iterator<HelloService> iterator = serviceLoader.iterator();				HelloService helloService;         while(iterator.hasNext()) {            helloService = iterator.next();//加载并初始化实现类        }  			helloService.sayHello();   }

通过上述代码,我们可以看到,通过 SPI 机制,我们不需要显式地实例化具体的服务实现类,而是通过 ServiceLoader 类动态加载并实例化服务提供者。这种方式使得我们可以在不修改代码的情况下,扩展和替换服务的具体实现。

这里为什么是用 iterator ? 而不是get之类的只获取一个实例的方法?试想一下,如果是一个固定的get方法,那么get到的是一个固定的实例,SPI 还有什么意义呢?

但这样就带来一个问题,我如何确定我调用的 helloService 是 HelloServiceImpl1 还是 HelloServiceImpl2 呢?实际上这取决于我们运行时的 classpath 配置,由于这个加载顺序(classpath)是由用户指定的,所以无论我们加载第一个还是最后一个,都有可能会导致加载不到用户自定义的那个配置。

所以这也是JDK SPI机制的一个劣势,无法确认具体加载哪一个实现,也无法加载某个指定的实现,仅靠classpath的顺序是一个非常不严谨的方式。

Dubbo SPI 如何使用?

Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。

服务接口:定义实现或扩展的接口,这边和 JDK中定义的一样。com.example.HelloService

public interface HelloService {    void sayHello();}
服务提供者:根据接口定义,编写接口的具体实现类,这边我们实现内容和JDK中一样,区别是我们可以通过注解 @SPI 来指定某个类是我们的默认实现,加载器未指定名称时,就会加载我们的默认服务实现,并在指定目录META-INF/dubbo/下通过简单的文本文件来指定实现的接口。com.example.HelloServiceImpl1
@SPI("helloService1")public class HelloServiceImpl1 implements HelloService {    @Override    public void sayHello() {        System.out.println("Hello from HelloServiceImpl1");    }}
com.example.HelloServiceImpl2
public class HelloServiceImpl2 implements HelloService {    @Override    public void sayHello() {        System.out.println("Hello from HelloServiceImpl2");    }}
META-INF/dubbo/ 目录下文件 om.example.HelloService
helloService1=com.example.HelloServiceImpl1helloService2=com.example.HelloServiceImpl2
服务加载器:使用服务加载器加载并使用服务提供者,Dubbo中的调用org.apache.dubbo.common.extension.ExtensionLoader#getExtensionLoader 方法来加载接口。加载默认实现
ExtensionLoader<HelloService> extensionLoader = ExtensionLoader.getExtensionLoader(HelloService.class);//  加载默认实现HelloService helloService = extensionLoader.getDefaultExtension();
根据名称获取扩展实现
ExtensionLoader<HelloService> extensionLoader = ExtensionLoader.getExtensionLoader(HelloService.class);// 根据名称获取特定扩展实现HelloService helloService2 = extensionLoader.getExtension("helloService2");

Dubbo SPI 和 JDK SPI 最大的区别就在于支持“别名”,可以通过某个服务提供者的别名来获取指定的服务提供者或接口实现,Dubbo 的 SPI 中还有一个“加载优先级”,优先加载内置(internal)的,然后加载外部的(external),按优先级顺序加载,如果遇到重复就跳过不会加载了。

Spring SPI 如何使用?

Spring SPI(Service Provider Interface)是 Spring 框架提供的一种扩展机制,用于实现插件化的方式来扩展和替换 Spring 中的各种组件。

Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单:

/获取所有factories文件中配置的LoggingSystemFactoryList<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

下面是一段 Spring Boot 中 spring.factories 的配置

# Logging Systemsorg.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 Loadersorg.springframework.boot.env.PropertySourceLoader=\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader

Spring SPI 中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦。

和前面两种 SPI 机制一样,Spring 也是支持 ClassPath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 ArrayList 中。由于没有别名,所以也没有去重的概念,有多少就添加多少。

Spring的SPI 虽然属于spring-framework(core),但是目前主要用在spring boot中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个spring.factories文件,那么你项目中的文件会被第一个加载,得到的Factories中,项目中spring.factories里配置的那个实现类也会排在第一个。

如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个META-INF/spring.factories文件,只添加你要的那个配置,不要完整的复制一遍 Spring Boot 的 spring.factories 文件然后修改。

SPI 对比

标签: #服务发现和服务注册