SPI机制

SPI机制

SPI (Service Provider Interface),主要适用于扩展作用,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。

JDK的SPI机制-ServiceLoader

ServiceLoader是Java提供的一种简单的SPI机制的实现,Java的SPI实现约定了以下两件事:

  • 文件必须放在META-INF/services/目录底下
  • 文件名必须为接口的全限定名,内容为接口实现的全限定名

案例实现:

  • 定义接口
public interface SpiInterface {
    String spi()
}
  • 实现接口
public class SpiInterfaceTest1 implements SpiInterface {
    @Override
    public String spi() {
        return "SPI实现类test1";
    }
}
public class SpiInterfaceTest2 implements SpiInterface {
    @Override
    public String spi() {
        return "SPI实现类TEST2";
    }
}
  • 在resources目录下,建立文件夹META-INF/services,创建一个以接口的全限定名为名称的文件,内容是该实现类的全限定名

在这里插入图片描述
在这里插入图片描述

  • 使用ServiceLoader.load()方法来加载实现类
    public static void main(String[] args) {
        /*JDK SPI*/
        ServiceLoader<SpiInterface> load = ServiceLoader.load(SpiInterface.class);
        Iterator<SpiInterface> iterator = load.iterator();
        while (iterator.hasNext()) {
            SpiInterface spiInterface = iterator.next();
            System.out.println(spiInterface.spi());
        }
    }
测试结果:
SPI实现类test1
SPI实现类TEST2

java中最常见的spi机制应用就是数据库驱动的加载,java其实就是定义了java语言跟数据库交互的接口,但是具体的实现得交给各大数据库厂商来实现,那么java怎么知道你的数据库厂商的实现了?这时就需要spi机制了,java好约了定在 Classpath 路径下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后内容是该数据库厂商的实现的接口的全限定名,这样数据库厂商只要按照这个规则去配置,java就能找到。

缺点:

  • JDK的SPI会把所有的对象都实例化到内存中,不管你用不用,可能会导致资源的浪费
  • 无法区分使用者应该使用哪一个实现类,需要从接口设计上入手决定使用那个实现类,不够灵活

Spring的SPI机制-SpringFactoriesLoader

Spring的SPI机制的约定如下:

  • 配置文件必须在META-INF/目录下,文件名必须为spring.factories
  • 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。

案例实现:

  • 配置spring.factories

在Spring中提供的SPI机制,我们只需要在 META-INF/spring.factories 中配置接口实现类名,即可通过服务发现机制,在运行时加载接口的实现类:

在这里插入图片描述

  • 使用SpringFactoriesLoader.loadFactories()来加载
        List<SpiInterface> spiInterfaces = SpringFactoriesLoader.loadFactories(SpiInterface.class, Demo.class.getClassLoader());
        for (SpiInterface spiInterface : spiInterfaces) {
            System.out.println(spiInterface.spi());
        }
测试结果:
SPI实现类test1
SPI实现类TEST2
  • 自动配置原理

在我们日常定义starter的时候,一般在spring.factories里会定义成org.springframework.boot.autoconfigure.EnableAutoConfiguration作为key,这样是为了SpringBoot启动的时候,根据@EnableAutoConfiguration注解来自动注入配置

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

在引入EnableAutoConfiguration的时候,会执行AutoConfigurationImportSelector里的逻辑,而真正去扫描各类starter的是其中的一个继承类ImportAutoConfigurationImportSelector

	protected Collection<String> loadFactoryNames(Class<?> source) {
		List<String> factoryNames = new ArrayList<>(
				SpringFactoriesLoader.loadFactoryNames(source, getBeanClassLoader()));
		ImportCandidates.load(source, getBeanClassLoader()).forEach(factoryNames::add);
		return factoryNames;
	}

SpringFactoriesLoader.loadFactoryNames:

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);

这里就用到了SpringFactoriesLoader.loadFactoryNames(source, getBeanClassLoader())去扫描各starter下的META-INF/spring.factories配置文件,然后通过反射等操作把文件里的value值变成Spring里的Bean对象,完成自动配置;

但是SpringBoot3.0之后不再使用SpringFactoriesLoader,而是Spring重新从META-INF/spring/目录下的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中读取了。

优点:

  • 不需要像JDK的SPI,一个接口就要定义一个文件,而是有统一的spring.factories配置文件,文件内容由k-v组成
  • Spring的SPI机制提供了获取类限定名的方法loadFactoryNames,而Java的SPI机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到Spring容器中,用Spring容器加载这些Bean,而不仅仅是通过反射

缺点:

  • 没有实现获取指定某个指定实现类的功能,所以要想能够找到具体的某个实现类,还得依靠具体接口的设计

Dubbo的SPI机制-ExtensionLoader

Dubbo的SPI机制也做了以下几点约定:

  • 接口必须要加@SPI注解
  • 配置文件可以放在META-INF/services/META-INF/dubbo/internal/ META-INF/dubbo/META-INF/dubbo/external/这四个目录底下,文件名也是接口的全限定名,一般使用META-INF/dubbo/
  • 内容为键值对,键为短名称(可以理解为spring中Bean的名称),值为实现类的全限定名

案例实现:

在这里插入图片描述

测试:

        ExtensionLoader<SpiInterface> extensionLoader =
                ApplicationModel.defaultModel()
                        .getExtensionDirector()
                        .getExtensionLoader(SpiInterface.class);
        SpiInterface test1 = extensionLoader.getExtension("test1");
        System.out.println(test1.spi());

Dubbo的SPI是可以根据指定的key来选择使用哪个实现类的,这样比较灵活

dubbo核心机制

  • 自适应机制

自适应机制说的是在代码运行过程中,可以基于参数动态的选择到具体的实现类来执行

每个接口有且只有一个自适应类,可以通过extensionLoader.getAdaptiveExtension();得到这个自适应类

自适应类有两种方式产生,第一种就是自己指定,就是上面测试的那种写法,执行要执行的key

另外一种是在接口的实现类上加@Adaptive注解,那么这个这个实现类就是自适应实现类。

@Adaptive
public class SpiInterfaceTest1 implements SpiInterface {
    @Override
    public String spi() {
        return "SPI实现类test1";
    }
}
        ExtensionLoader<SpiInterface> extensionLoader =
                ApplicationModel.defaultModel()
                        .getExtensionDirector()
                        .getExtensionLoader(SpiInterface.class);
        SpiInterface adaptiveExtension = extensionLoader.getAdaptiveExtension();
        System.out.println(adaptiveExtension.spi());

// SPI实现类test1
  • 自动激活

自动激活,就是根据你的参数,动态的选择一批实现类返回给你,最常见的使用方式就是dubbo里Filter,可以动态的选择是提供者还是消费者

自动激活的实现类上需要加上Activate注解:

@Activate(group = {CommonConstants.PROVIDER})
public class DubboRequestFilter implements Filter {

这就说明该DubboRequestFilter在dubbo的提供方会被激活,提供自定义拦截器作用
在这里插入图片描述

posted @ 2023-05-12 16:03  孙半仙人  阅读(55)  评论(0编辑  收藏  举报