聊聊SPI

文章详细介绍了Java、Spring和Dubbo的SPI(ServiceProviderInterface)机制,包括它们各自的特点、实现原理和应用场景。Java的SPI通过ServiceLoader加载接口实现,Spring的SPI利用SpringFactoriesLoader加载配置,而Dubbo的SPI不仅包含加载机制,还支持自适应、IOC和AOP等特性。Dubbo的ExtensionLoader允许精确获取指定实现,并提供了自适应类和自动激活功能。
部署运行你感兴趣的模型镜像

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:三友的java日记

7b3ce5c24dc06457a2b642f89caadfe5.jpeg


今天来跟大家聊一聊Java、Spring、Dubbo三者SPI机制的原理和区别。

其实我之前写过一篇类似的文章,但是这篇文章主要是剖析dubbo的SPI机制的源码,中间只是简单地介绍了一下Java、Spring的SPI机制,并没有进行深入,所以本篇就来深入聊一聊这三者的原理和区别。

什么是SPI

SPI全称为Service Provider Interface,是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。

简单来说SPI是一种非常优秀的设计思想,它的核心就是解耦、方便扩展。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

Java SPI机制--ServiceLoader

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

  • 文件必须放在META-INF/services/目录底下

  • 文件名必须为接口的全限定名,内容为接口实现的全限定名

这样就能够通过ServiceLoader加载到文件中接口的实现。

来个demo

第一步,需要一个接口以及他的实现类

public interface LoadBalance {
}

public class RandomLoadBalance implements LoadBalance{
}

第二步,在META-INF/services/目录创建一个文件名LoadBalance全限定名的文件,文件内容为RandomLoadBalance的全限定名

dabc86a93f094304410746dcbe107428.png 9f83a917da8580955a4cb7922fbf0724.png

测试类:

public class ServiceLoaderDemo {

  public static void main(String[] args) {
    ServiceLoader<LoadBalance> loadBalanceServiceLoader = ServiceLoader.load(LoadBalance.class);
    Iterator<LoadBalance> iterator = loadBalanceServiceLoader.iterator();
    while (iterator.hasNext()) {
      LoadBalance loadBalance = iterator.next();
      System.out.println("获取到负载均衡策略:" + loadBalance);
    }
  }

}

测试结果:

1fd6ebe7b66c5d1e1f099724727178c4.png

此时就成功获取到了实现。

在实际的框架设计中,上面这段测试代码其实是框架作者写到框架内部的,而对于框架的使用者来说,要想自定义LoadBalance实现,嵌入到框架,仅仅只需要写接口的实现和spi文件即可。

实现原理

如下是ServiceLoader中一段核心代码

1113101c8d93d5990610b5c09b27f12c.png

首先获取一个fullName,其实就是META-INF/services/接口的全限定名

2a79049410b8e9f5bbf4f40c7d194460.png

然后通过ClassLoader获取到资源,其实就是接口的全限定名文件对应的资源,然后交给parse方法解析资源

3dd0d64bae5e6de358644558d59f562e.png

parse方法其实就是通过IO流读取文件的内容,这样就可以获取到接口的实现的全限定名

再后面其实就是通过反射实例化对象,这里就不展示了。

所以其实不难发现ServiceLoader实现原理比较简单,总结起来就是通过IO流读取META-INF/services/接口的全限定名文件的内容,然后反射实例化对象。

优缺点

由于Java的SPI机制实现的比较简单,所以他也有一些缺点。

第一点就是浪费资源,虽然例子中只有一个实现类,但是实际情况下可能会有很多实现类,而Java的SPI会一股脑全进行实例化,但是这些实现了不一定都用得着,所以就会白白浪费资源。

第二点就是无法对区分具体的实现,也就是这么多实现类,到底该用哪个实现呢?如果要判断具体使用哪个,只能依靠接口本身的设计,比如接口可以设计为一个策略接口,又或者接口可以设计带有优先级的,但是不论怎样设计,框架作者都得写代码进行判断。

所以总得来说就是ServiceLoader无法做到按需加载或者按需获取某个具体的实现。

使用场景

虽然说ServiceLoader可能有些缺点,但是还是有使用场景的,比如说:

  • 不需要选择具体的实现,每个被加载的实现都需要被用到

  • 虽然需要选择具体的实现,但是可以通过对接口的设计来解决

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

Spring SPI机制--SpringFactoriesLoader

Spring我们都不陌生,他也提供了一种SPI的实现SpringFactoriesLoader。

Spring的SPI机制的约定如下:

  • 配置文件必须在META-INF/目录下,文件名必须为spring.factories

  • 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。

所以也可以看出,Spring的SPI机制跟Java的不论是文件名还是内容约定都不一样。

来个demo

META-INF/目录下创建spring.factories文件,LoadBalance为键,RandomLoadBalance为值

a87af0e7fa588c8bf6f48c4225578033.png 880b5f85719a03f456ac31d98535d55b.png

测试:

public class SpringFactoriesLoaderDemo {

  public static void main(String[] args) {
    List<LoadBalance> loadBalances = SpringFactoriesLoader.loadFactories(LoadBalance.class, MyEnableAutoConfiguration.class.getClassLoader());
    for (LoadBalance loadBalance : loadBalances) {
      System.out.println("获取到LoadBalance对象:" + loadBalance);
    }
  }

}

运行结果:

ce5c483f0be2c3250bd084fb072edd32.png

成功获取到了实现对象。

核心原理

如下是SpringFactoriesLoader中一段核心代码

3f04a754b34c3ec31aaacdddb22d4f2b.png

其实从这可以看出,跟Java实现的差不多,只不过读的是META-INF/目录下spring.factories文件内容,然后解析出来键值对。

使用场景

Spring的SPI机制在内部使用的非常多,尤其在SpringBoot中大量使用,SpringBoot启动过程中很多扩展点都是通过SPI机制来实现的,这里我举两个例子

1、自动装配

在SpringBoot3.0之前的版本,自动装配是通过SpringFactoriesLoader来加载的。

675af1b2e5bf7364b1b3d80f0ab8254b.png

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

cf45f62c1bc9b9c46c283a005a82f74a.png

至于如何读取的,其实猜也能猜到就跟上面SPI机制读取的方式大概差不多,就是文件路径和名称不一样。

2、PropertySourceLoader的加载

PropertySourceLoader是用来解析application配置文件的,它是一个接口

2ce9ba9dfb5e51d935f6a2ca9dd4114c.png

SpringBoot默认提供了 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader两个实现,就是对应properties和yaml文件格式的解析。

SpringBoot在加载PropertySourceLoader时就用了SPI机制

1a8386ba61d45788fef1b26223b0d641.png
与Java SPI机制对比

首先Spring的SPI机制对Java的SPI机制对进行了一些简化,Java的SPI每个接口都需要对应的文件,而Spring的SPI机制只需要一个spring.factories文件。

其次是内容,Java的SPI机制文件内容必须为接口的实现类,而Spring的SPI并不要求键值对必须有什么关系,更加灵活。

第三点就是Spring的SPI机制提供了获取类限定名的方法loadFactoryNames,而Java的SPI机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到Spring容器中,用Spring容器加载这些Bean,而不仅仅是通过反射。

但是Spring的SPI也同样没有实现获取指定某个指定实现类的功能,所以要想能够找到具体的某个实现类,还得依靠具体接口的设计。

所以不知道你有没有发现,PropertySourceLoader它其实就是一个策略接口,注释也有说,所以当你的配置文件是properties格式的时候,他可以找到解析properties格式的PropertiesPropertySourceLoader对象来解析配置文件。

Dubbo SPI机制--ExtensionLoader

ExtensionLoader是dubbo的SPI机制的实现类。每一个接口都会有一个自己的ExtensionLoader实例对象,这点跟Java的SPI机制是一样的。

同样地,Dubbo的SPI机制也做了以下几点约定:

  • 接口必须要加@SPI注解

  • 配置文件可以放在META-INF/services/META-INF/dubbo/internal/ META-INF/dubbo/META-INF/dubbo/external/这四个目录底下,文件名也是接口的全限定名

  • 内容为键值对,键为短名称(可以理解为spring中Bean的名称),值为实现类的全限定名

先来个demo

首先在LoadBalance接口上@SPI注解

@SPI
public interface LoadBalance {

}

然后,修改一下Java的SPI机制测试时配置文件内容,改为键值对,因为Dubbo的SPI机制也可以从META-INF/services/目录下读取文件,所以这里就没重写文件

random=com.sanyou.spi.demo.RandomLoadBalance

测试类:

public class ExtensionLoaderDemo {

  public static void main(String[] args) {
    ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
    LoadBalance loadBalance = extensionLoader.getExtension("random");
    System.out.println("获取到random键对应的实现类对象:" + loadBalance);
  }

}

通过ExtensionLoader的getExtension方法,传入短名称,这样就可以精确地找到短名称对的实现类。

所以从这可以看出Dubbo的SPI机制解决了前面提到的无法获取指定实现类的问题。

测试结果:

e13f0dee6deba7482a69599f53a7b4d4.png

dubbo的SPI机制除了解决了无法获取指定实现类的问题,还提供了很多额外的功能,这些功能在dubbo内部用的非常多,接下来就来详细讲讲。

dubbo核心机制
1、自适应机制

自适应,自适应扩展类的含义是说,基于参数,在运行时动态选择到具体的目标类,然后执行。

每个接口有且只能有一个自适应类,通过ExtensionLoader的getAdaptiveExtension方法就可以获取到这个类的对象,这个对象可以根据运行时具体的参数找到目标实现类对象,然后调用目标对象的方法。

举个例子,假设上面的LoadBalance有个自适应对象,那么获取到这个自适应对象之后,如果在运行期间传入了random这个key,那么这个自适应对象就会找到random这个key对应的实现类,调用那个实现类的方法,如果动态传入了其它的key,就路由到其它的实现类。

自适应类有两种方式产生,第一种就是自己指定,在接口的实现类上加@Adaptive注解,那么这个这个实现类就是自适应实现类。

@Adaptive
public class RandomLoadBalance implements LoadBalance{
}

除了自己代码指定,还有一种就是dubbo会根据一些条件帮你动态生成一个自适应类,生成过程比较复杂,这里就不展开了。

自适应机制在Dubbo中用的非常多,而且很多都是自动生成的,如果你不知道Dubbo的自适应机制,你在读源码的时候可能都不知道为什么代码可以走到那里。。

2、IOC和AOP

一提到IOC和AOP,立马想到的都是Spring,但是IOC和AOP并不是Spring特有的概念,Dubbo也实现IOC和AOP的功能,但是是一个轻量级的。

2.1、依赖注入

Dubbo依赖注入是通过setter注入的方式,注入的对象默认就是上面提到的自适应的对象,在Spring环境下可以注入Spring Bean。

public class RoundRobinLoadBalance implements LoadBalance {

  private LoadBalance loadBalance;

  public void setLoadBalance(LoadBalance loadBalance) {
    this.loadBalance = loadBalance;
  }

}

如上代码,RoundRobinLoadBalance中有一个setLoadBalance方法,参数LoadBalance,在创建RoundRobinLoadBalance的时候,在非Spring环境底下,Dubbo就会找到LoadBalance自适应对象然后通过反射注入。

这种方式在Dubbo中也很常见,比如如下的一个场景

3feacdc1a106c1a7b5f19b2d74d9ebe0.png

RegistryProtocol中会注入一个Protocol,其实这个注入的Protocol就是一个自适应对象。

2.2、接口回调

Dubbo也提供了一些类似于Spring的一些接口的回调功能,比如说,如果你的类实现了Lifecycle接口,那么创建或者销毁的时候就会回调以下几个方法

8237733b18454f41fafb77756a0567de.png

在dubbo3.x的某个版本之后,dubbo提供了更多接口回调,比如说ExtensionPostProcessor、ExtensionAccessorAware,命名跟Spring的非常相似,作用也差不多。

2.3、自动包装

自动包装其实就是aop的功能实现,对目标对象进行代理,并且这个aop功能在默认情况下就是开启的。

在Dubbo中SPI接口的实现中,有一种特殊的类,被称为Wrapper类,这个类的作用就是来实现AOP的。

判断Wrapper类的唯一标准就是这个类中必须要有这么一个构造参数,这个构造方法的参数只有一个,并且参数类型就是接口的类型,如下代码:

public class RoundRobinLoadBalance implements LoadBalance {

  private final LoadBalance loadBalance;

  public RoundRobinLoadBalance(LoadBalance loadBalance) {
    this.loadBalance = loadBalance;
  }

}

此时RoundRobinLoadBalance就是一个Wrapper类。

当通过random获取RandomLoadBalance目标对象时,那么默认情况下就会对RandomLoadBalance进行包装,真正获取到的其实是RoundRobinLoadBalance对象,RoundRobinLoadBalance内部引用的对象是RandomLoadBalance。

*测试一下 *

在配置文件中加入

roundrobin=com.sanyou.spi.demo.RoundRobinLoadBalance

测试结果

9ea170e79191978ec554db4e6c373657.png

从结果可以看出,虽然指定了random,但是实际获取到的是RoundRobinLoadBalance,而RoundRobinLoadBalance内部引用了RandomLoadBalance。

如果有很多的包装类,那么就会形成一个责任链条,一个套一个。

所以dubbo的aop跟spring的aop实现是不一样的,spring的aop底层是基于动态代理来的,而dubbo的aop其实算是静态代理,dubbo会帮你自动组装这个代理,形成一条责任链。

到这其实我们已经知道,dubbo的spi接口的实现类已经有两种类型了:

  • 自适应类

  • Wrapper类

除了这两种类型,其实还有一种,叫做默认类,就是@SPI注解的值对应的实现类,比如

@SPI("random")
public interface LoadBalance {

}

此时random这个key对应的实现类就是默认实现,通过getDefaultExtension这个方法就可以获取到默认实现对象。

3、自动激活

所谓的自动激活,就是根据你的入参,动态地选择一批实现类返回给你。

自动激活的实现类上需要加上Activate注解,这里就又学习了一种实现类的分类。

@Activate
public interface RandomLoadBalance {

}

此时RandomLoadBalance就属于可以被自动激活的类。

获取自动激活类的方法是getActivateExtension,所以根据这个方法的入参,可以动态选择一批实现类。

自动激活这个机制在Dubbo一个核心的使用场景就是Filter过滤器链中。

Filter是dubbo中的一个扩展点,可以在请求发起前或者是响应获取之后就行拦截,作用有点像Spring MVC中的HandlerInterceptor。

be50dddeef7c13896cd77390ece91695.pngFilter的一些实现类

如上Filter有很多实现,所以为了能够区分Filter的实现是作用于provider的还是consumer端,所以就可以用自动激活的机制来根据入参来动态选择一批Filter实现。

比如说ConsumerContextFilter这个Filter就作用于Consumer端。

207aa6e289c628a62134fc1e62dbc89f.pngConsumerContextFilter

总结

通过以上分析可以看出,实现SPI机制的核心原理就是通过IO流读取指定文件的内容,然后解析,最后加入一些自己的特性。

最后总的来说,Java的SPI实现的比较简单,并没有什么其它功能;Spring得益于自身的ioc和aop的功能,所以也没有实现太复杂的SPI机制,仅仅是对Java做了一点简化和优化;但是dubbo的SPI机制为了满足自身框架的使用要求,实现的功能就比较多,不仅将ioc和aop的功能集成到SPI机制中,还提供注入自适应和自动激活等功能。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

38b846469324c3568235772d2ef88fc6.png

已在知识星球更新源码解析如下:

0be65fce4a5e2db076371bb8e1fd402d.jpeg

c961c4711bede5800158c1382615d5ca.jpeg

578095fb7a2ceffc14e86ad354ba7e3f.jpeg

fd4d487ce8cde119a6e4c441f7ff6c84.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

你怎么看下面这段话:要搞懂 “波特率到底是电平变化次数还是周期数”,核心是先抓严格定义,再看不同协议的实际应用场景—— 既不违背标准,又能明白工程里为什么会有 “看似不一样” 的用法,下面一步步说清楚: 一、先定 “严谨基准”:波特率的官方定义就是 “每秒信号变化次数” 不管是 UART 还是 SPI,国际标准(如 RS-232)里的波特率(Baud Rate),本质都是 “每秒内信号电平发生‘跳变’的次数”,单位是 “波特(Baud)”。 举个直观例子:信号从 “高” 变 “低” 是 1 次变化,从 “低” 变 “高” 也是 1 次变化 ——1 个完整的时钟周期(高→低→高,或低→高→低),刚好包含2 次电平变化。 所以严格来说: 1 个时钟周期 = 2 次电平变化 波特率(变化次数 / 秒)= 2 × 时钟周期数 / 秒 二、分场景看:为什么 UART 和 SPI “用起来不一样”? 关键差异在于通信是否需要 “附加位”,以及 “1 次信号变化能不能直接对应 1 位数据”—— 这就导致了工程中对波特率的 “实际解读” 不同,但都没违背上面的严格定义。 场景 1:UART(异步通信)——“波特率≠实际传位数”,需要折算周期 UART 是 “异步” 的,没有统一时钟,所以必须在 “有效数据位” 前后加附加位(比如 1 个起始位 + 1 个停止位,有时还有 1 个校验位),才能让接收端识别数据开始和结束。 我们用 “9600 波特率” 举个严谨的计算: 按定义:9600 波特 = 每秒 9600 次电平变化; 1 个时钟周期 = 2 次变化 → 每秒时钟周期数 = 9600 ÷ 2 = 4800 个; UART 传输 1 个 “有效字节(8 位)”,需要带 1 起始 + 1 停止 = 共 10 位(含附加位); 每秒能传的 “总位数(含附加)”= 时钟周期数(因为 1 周期传 1 位)= 4800 位; 每秒实际传的 “有效数据位”= 4800 ÷ 10 × 8 = 3840 位(远低于 9600)。 这里的核心:UART 的 “波特率(变化次数)” 和 “实际传位数” 没关系,因为附加位占了很多资源,必须先算周期,再扣附加位 —— 这就是之前说 “9600 波特对应 4800 周期” 的原因,是严格按定义推导的。 场景 2:SPI(同步通信)——“波特率≈实际传位数”,工程上直接用 SPI 是 “同步” 的,发送端和接收端共享同一个时钟,不需要任何附加位(起始 / 停止位都没有),1 个时钟周期就精准对应 1 位有效数据: 还是用 “9600 波特率” 算: 按定义:9600 波特 = 每秒 9600 次电平变化; 1 个时钟周期 = 2 次变化 → 每秒时钟周期数 = 9600 ÷ 2 = 4800 个; 但 SPI1 个周期传 1 位有效数据 → 每秒传有效位数 = 4800 位? 不对!这里要注意:工程上没人按 “严格定义” 算 SPI 波特率—— 因为 SPI 没有附加位,“时钟周期数” 就等于 “有效传位数”,大家为了方便,直接把 “SPI 的波特率” 等同于 “每秒传的有效位数”(也就是 “比特率”)。 所以实际说 “SPI 波特率 9600”,意思是 “每秒传 9600 位有效数据”,反推回去: 每秒需要 9600 个时钟周期(1 周期传 1 位); 每秒电平变化次数 = 9600 × 2 = 19200 次(按严格定义,这才是 “9600 比特率对应的波特率”)。 这里的核心:SPI 的 “波特率” 是 “工程简化说法”,本质是把 “比特率(传位数)” 直接叫成了 “波特率”—— 不是定义错了,而是因为没有附加位,两者的换算太简单(1 位 = 1 周期 = 2 次变化),没必要纠结定义,直接用 “传位数” 当波特率,更符合实际需求。 三、总结:一句话分清 “严谨定义” 和 “实际用法” 维度 严格定义(不随协议变) UART(异步)实际应用 SPI(同步)实际应用 波特率本质 每秒电平变化次数 按定义算(变化次数) 简化成 “每秒有效传位数” 与 “传位数” 的关系 无直接关系 需扣附加位,传位数远低于波特率 无附加位,传位数≈波特率 9600 波特的实际效果 每秒 9600 次变化→4800 周期 每秒传 3840 位有效数据 每秒传 9600 位有效数据 简单记: 聊 UART,必须按 “严格定义”(变化次数)算,不然会算错有效数据; 聊 SPI,直接按 “工程习惯”(传位数)用,没人会纠结 “变化次数”—— 因为怎么方便怎么来,且不会出错。
09-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值