SPI机制

SPI机制用于接口和实现的分离,提高框架可扩展性。Java的SPI通过ServiceLoader加载实现类,Spring使用SpringFactoriesLoader加载配置在spring.factories中的实现。Dubbo的SPI更灵活,允许通过指定key选择实现,并具备自适应机制和自动激活功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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的提供方会被激活,提供自定义拦截器作用
在这里插入图片描述

### Java SPI机制的原理及实现 #### 1. Java SPI的核心概念 Java SPI(Service Provider Interface)是一种面向服务的设计模式,主要用于支持可插拔的服务架构。它的基本思想是将接口的定义与其具体实现分离,使得应用程序可以在运行时动态加载和替换这些实现[^1]。 #### 2. 工作流程概述 SPI的工作流程基于`META-INF/services/`目录下的配置文件。当开发者希望使用某种服务时,只需要提供该服务对应的接口类名即可。而实际的实现则由第三方或开发团队自行完成,并注册到指定路径下。以下是工作过程的关键点: - **接口声明**:定义一个公共接口作为服务的标准。 - **实现编写**:为上述接口创建多个不同功能的具体实现类。 - **配置文件维护**:在JAR包中的`META-INF/services/`目录下放置以接口全限定名为名称的文件,其中每行记录对应一种实现类的全限定名[^2]。 #### 3. 使用 `ServiceLoader` 加载服务 为了简化对SPI的支持,Java提供了`java.util.ServiceLoader`工具类来自动发现并加载所有的可用实现。下面是一个简单的例子展示如何利用`ServiceLoader`加载特定接口的所有已知实现: ```java import java.util.ServiceLoader; public class SpiExample { public static void main(String[] args) { ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class); for (MyService service : loader) { System.out.println(service.perform()); } } } interface MyService { String perform(); } ``` 在这个案例中,假设存在两个实现了`MyService`接口的不同类,则它们会被依次打印输出各自的结果[^3]。 #### 4. 实现细节分析 从技术角度来看,SPI的主要优势在于增强了系统的灵活性与扩展能力。通过这种方式,不仅能够轻松切换不同的算法或者逻辑处理单元,而且还可以引入外部贡献者的定制版本而不改变原有代码结构[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值