0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

基于spring的SPI扩展机制是如何实现的?

jf_ro2CN3Fa 来源:码农参上 2023-03-07 09:17 次阅读

八股文背多了,相信大家都听说过一个词,SPI 扩展

有的面试官就很喜欢问这个问题,SpringBoot 的自动装配是如何实现的?

基本上,你一说是基于 spring 的 SPI 扩展机制,再把spring.factories文件和EnableAutoConfiguration提一下,那么这个问题就答的八九不离十了。

就像四五年前,我去面试的时候被问到这个问题,SPI 动态扩展机制 这几个词从嘴里一说出来,就把面试官唬的一愣一愣的。可能他们也没见过这么能装逼的,一句话能简简单单说明白,非要拽一个听上去很高大上的词。

话说回来,被唬住的可不止是面试官,其实还有我自己。至于 SPI 扩展究竟是个啥,是怎么实现的,我当时也根本不明白。

不过现在的面试就是这样,对线八股文,要想唬住面试官,就得先唬住自己。

那么我们今天暂且不提 spring 的 SPI 扩展,先来看看 java 本身自带的 SPI 扩展机制是怎么一回事。

1、简介

SPI 的全称是Service Provider Interface,翻译过来就是服务提供者的接口 ,它所实现的其实是一种服务的发现机制。

这么说起来可能还是有点不好理解,我举个例子来类比一下。

在 spring 项目中,写 service 层代码前,会约定俗成的会添加一个接口层。然后通过 spring 中的依赖注入,可以借助@Autowired等方式注入这个接口的实现类的实例对象,之后对于 service 的调用一般也基于接口操作。

简单形容就是这样的:

d8448642-bc7d-11ed-bfe3-dac502259ad0.jpg

如图所示,接口、实现类都是由服务提供方提供,我们可以把 controller 看作服务调用者,调用方只管调用接口就可以了。

虽然也有声音认为,大部分情况下 service 只有一个实现类,接口层显得有些多余。但是在《Head First Design Patterns》这本书中,大佬们还是建议过:

Program to an interface, not an implementation.

没错,就是常说的要面向接口编程 。至于好处,也不外乎是降低耦合度、方便日后扩展、提高了代码的灵活性和可维护性等等。

在上面这个例子里,这个接口层和其中的方法我们可以称之为API ,而我们要讨论的SPI 和它相比,有类似也有差异,还是先看图:

d8603fd6-bc7d-11ed-bfe3-dac502259ad0.jpg

简单来说,就是服务的调用方定义一个接口规范,可以由不同的服务提供者实现。并且,调用方能够通过某种机制来发现服务提供方,并通过接口调用它的能力。

通过对比,我们可以看出它们虽然都有着接口 这一层面,但还是有很大的不同:

API 中的接口是服务提供者给服务调用者的一个功能列表,而 SPI 中更多强调的是,服务调用者对服务实现的一种约束,服务提供者根据这种约束实现的服务,可以被服务调用者发现。

说白了,Java 中的 SPI 实现的就是,你按我的接口规范实现服务,我就能通过某种机制为这个接口寻找到这个服务。

这么说起来可能还有些抽象,下面我们举一个例子,类比具体描述一下这个过程。

2、定义接口

说起智能家居系统,大家现在都比较熟悉了,只要是相同品牌下的产品,连上 wifi 就能够通过手机 app 控制了,非常方便。

虽然产品不断更新换代,型号更新层出不穷,但是同种家电在 app 上操作起来,功能一般都是一样的。就拿空调来说,我们在 app 上操作起来一般也就三个主要功能:开关选模式调节温度

假设我现在在客厅、卧室、书房安装了 3 款不同型号的空调,并把它们都接入到了我 app 中,那么之后的操作都是相同的几个按键,简单粗暴。

d872a176-bc7d-11ed-bfe3-dac502259ad0.jpg

思考一下,无论是开关还是调温,都是通过 app 去调用设备的接口罢了,那么如果不同型号的空调各写各的接口,后端 app 在开发的时候光对接接口都麻烦的要死。

解决方法也很简单,我先定义一套接口规范,不管你以后什么型号的空调,都按我的规范来实现接口。以后只要我能发现你的设备,那么都可以按相同的方法来调用接口。

那么下面就先来定义这么一套接口规范,如果你以后想要接入智能家居系统,那么就要遵循这个规范来开发接口。

新建一个项目作为标准,就叫aircondition-standard好了,然后创建一个接口。除了 3 个操作以外,我们再添加一个获取空调型号的方法。

publicinterfaceIAircondition{
//获取型号
StringgetType();

//开关
voidturnOnOff();

//调节温度
voidadjustTemperature(inttemperature);

//模式变更
voidchangeModel(intmodelId);
}

这个接口后面要给服务的实现方来使用,用 maven 把它打成 jar 包:

mvncleaninstall

之后服务提供者在项目中就可以引入这个 jar 包了,有了这套规范,就保证了产品后期不管怎么更新换代,都能接入到系统来。

3、服务实现

制定并发布完规则后,挂式空调 作为第一个服务提供者就来了,新建一个项目aircondition-hanging-type,并引入刚才打好的 jar 包:


com.cn.hydra
aircondition-standard
1.0-SNAPSHOT

创建服务类,并实现前面定义的接口:

publicclassHangingTypeAircondition
implementsIAircondition{
publicStringgetType(){
return"HangingType";
}

publicvoidturnOnOff(){
System.out.println("挂式空调开关");
}

publicvoidadjustTemperature(inti){
System.out.println("挂式空调调节温度");
}

publicvoidchangeModel(inti){
System.out.println("挂式空调更换模式");
}
}

在项目的resources的目录下,创建META-INF/services目录,然后以前面定义的接口名com.cn.hydra.IAircondition创建文件,并在文件中写入实现类的全限定名。

com.cn.hydra.HangingTypeAircondition

整个项目结构非常简单:

d8864118-bc7d-11ed-bfe3-dac502259ad0.png

这样,一个服务方的简单实现就搞定了,用 maven 打成 jar 包,之后就可以提供给调用方使用了。

同理,我们可以再创建一个立式空调 的项目aircondition-vertical-type,也只创建一个服务类:

publicclassVerticalTypeAircondition
implementsIAircondition{
publicStringgetType(){
return"VerticalType";
}

publicvoidturnOnOff(){
System.out.println("立式空调开关");
}

publicvoidadjustTemperature(inti){
System.out.println("立式空调调节温度");
}

publicvoidchangeModel(inti){
System.out.println("立式空调更换模式");
}
}

还是按上面的命名规则,创建一个配置文件:

com.cn.hydra.VerticalTypeAircondition

同样,打成 jar 包就完事了,至于服务调用者如何去发现和调用这两个服务,下面详细再说。

4、服务发现

现在两个服务提供方都实现了接口,下面关键的一步就是服务发现,这一步 java 中的 spi 发现机制已经帮我们实现好了。

创建一个新项目aircondition-app,引入上面打好的两个 jar 包。



com.cn.hydra
aircondition-hanging-type
1.0-SNAPSHOT



com.cn.hydra
aircondition-vertical-type
1.0-SNAPSHOT


按照上面的说法,虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法。

下面,就是关键的服务发现环节,我们写一个方法,根据型号去调用对应空调的开关方法。

publicclassAirconditionApp{
publicstaticvoidmain(String[]args){
newAirconditionApp().turnOn("VerticalType");
}

publicvoidturnOn(Stringtype){
ServiceLoaderload=ServiceLoader
.load(IAircondition.class);

for(IAirconditioniAircondition:load){
System.out.println("检测到:"+iAircondition.getClass().getSimpleName());
if(type.equals(iAircondition.getType())){
iAircondition.turnOnOff();
}
}
}
}

测试结果:

d8a8028a-bc7d-11ed-bfe3-dac502259ad0.png

可以看到,测试过程中,通过定义的接口IAircondition发现了两个实现类,并通过参数,调用了特定实现类的某个方法。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用。

5、原理

了解了 spi 的工作流程,我们再来看看它的实现,其实最关键的就是上面代码中出现的ServiceLoader这个类。

上面的示例代码中,对于ServiceLoader的load()方法的结果,我们用for循环进行了遍历,这一点我们看一下源码就能明白,因为ServiceLoader实现了Iterable这一接口,而整个服务发现的核心,就在它的iterator()方法中。

d8ba70fa-bc7d-11ed-bfe3-dac502259ad0.png

注意这里面有两个关键的东西,找一下在源码中定义的地方:

d8de2a72-bc7d-11ed-bfe3-dac502259ad0.png

注释写的非常明白,providers就是一个缓存,在迭代器中如果先从这里面进行查找,如果里面有就继续往下找,没有了的话就用这个懒加载的lookupIterator查找。

那么就简单了,接着往下看LazyIterator,看看它里面的hasNext()和next()两个方法是怎么实现的。

d8ed3f58-bc7d-11ed-bfe3-dac502259ad0.png

这个acc是一个安全管理器,在前面通过System.getSecurityManager()判断并赋值,debug 看一下这里都是null,所以直接看hasNextService()和nextService()方法就可以了。

在hasNextService()方法中,会取出接口取出实现类的类名放到nextName中:

d90653b2-bc7d-11ed-bfe3-dac502259ad0.png

接下来,在nextService()方法中,则会先加载这个实现类,然后实例化对象,最终放入缓存中去。

d91865b6-bc7d-11ed-bfe3-dac502259ad0.png

在迭代器的迭代过程中,会完成所有实现类的实例化,其实归根结底,还是基于 java 反射去实现的。

6、应用

要说 spi 的实际应用,大家最常见的应该就是日志框架slf4j了,它利用 spi 实现了插槽式接入其他具体的日志框架。

说白了,slf4j本身就是个日志门面,并不提供具体的实现,需要绑定其他具体实现才能真正的引入日志功能。

例如我们可使用log4j2作为具体的绑定器,只需要在 pom 中引入slf4j-log4j12,就可以使用具体功能。


org.slf4j
slf4j-api
2.0.3


org.slf4j
slf4j-log4j12
2.0.3

引入项目后,点开它的 jar 包看一下具体结构:

d92accc4-bc7d-11ed-bfe3-dac502259ad0.png

有没有发现一个彩蛋,先说为什么我们 pom 中引入的明明是slf4j-log4j12,实际上引入的是slf4j-reload4j?翻一下官网的文档:

d960a876-bc7d-11ed-bfe3-dac502259ad0.png

大意就是在 2015 年和 2022 年,log4j1.x就已经宣布end of life终止了,原因也不难猜,估计是因为频繁爆出的漏洞。在那之后,slf4j-log4j在构建阶段就会自动重定向到slf4j-reload4j了,并且官方也强烈建议使用slf4j-reload4j作为替代。

再回头看一下 jar 包的META-INF.services里面,通过 spi 注入了Reload4jServiceProvider这个实现类,它实现了SLF4JServiceProvider这一接口,在它的初始化方法initialize()中,会完成初始化等工作,后续可以继续获取到LoggerFactory和Logger等具体日志对象。

7、总结

Java 中的 SPI 提供了一种比较特别的服务发现和调用机制,通过接口灵活的将服务调用与服务提供者分离,用于提供给第三方实现扩展时还是很方便的。但是也有缺点,比方说一旦加载一个接口,就会把所有实现类都加载进来,可能会加载到不需要的冗余服务。不过站在整体角度上,还是给我们提供了一种非常不错的框架扩展、集成的思路。






审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2964

    浏览量

    104684
  • wifi信号
    +关注

    关注

    0

    文章

    17

    浏览量

    8314
  • SPI串口
    +关注

    关注

    0

    文章

    4

    浏览量

    922

原文标题:美团:SPI 的原理是什么?

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Sentinel扩展性设计机制分析

    Sentinel 提供多样的 SPI 接口用于提供扩展的能力。用户可以在用同一个 sentinel-core 的基础上自行扩展接口实现,从而可以方便地给 Sentinel 添加自定义的
    的头像 发表于 10-26 10:03 4149次阅读

    java spring教程

    java spring教程理解Spring 实现原理掌握Spring IOC,AOP掌握Spring的基础配置和用法熟练使用SSH开发项目
    发表于 09-11 11:09

    聊聊Dubbo - Dubbo可扩展机制实战

    OSGI容器Dubbo作为一个框架,不希望强依赖其他的IoC容器,比如Spring,Guice。OSGI也是一个很重的实现,不适合Dubbo。最终Dubbo的实现参考了Java原生的SPI
    发表于 06-04 17:33

    聊聊Dubbo - Dubbo可扩展机制源码解析

    机制Spring容器中去寻找。由于ExtensionFactory本身也是一个扩展点,我们可以实现自己的ExtensionFactory,让Dubbo的自动装配支持我们自定义的组件。
    发表于 06-05 18:43

    SPI总线扩展PLC输出点数的设计与实现_李磊

    SPI总线扩展PLC输出点数的设计与实现_李磊
    发表于 03-19 11:28 2次下载

    基于SPI协议的SD卡读写机制实现方法

    基于SPI协议的SD卡读写机制实现方法。
    发表于 03-25 11:21 27次下载
    基于<b class='flag-5'>SPI</b>协议的SD卡读写<b class='flag-5'>机制</b>与<b class='flag-5'>实现</b>方法

    Spring和Springboot的扩展接口总结

    Spring的核心思想就是容器,当容器refresh的时候,外部看上去风平浪静,其实内部则是一片惊涛骇浪,汪洋一片。Springboot更是封装了Spring,遵循约定大于配置,加上自动装配的机制。很多时候我们只要引用了一个依赖
    的头像 发表于 10-13 10:17 921次阅读

    JDK内置的一种服务SPI机制

    SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要用于框架中开发,例如Dubbo、Spring
    的头像 发表于 02-15 09:15 789次阅读

    Java中的SPI动态扩展(上)

    基本上,你一说是基于springSPI扩展机制,再把`spring.factories`文件和`EnableAutoConfigurati
    的头像 发表于 03-24 14:27 461次阅读
    Java中的<b class='flag-5'>SPI</b>动态<b class='flag-5'>扩展</b>(上)

    Java中的SPI动态扩展(下)

    基本上,你一说是基于springSPI扩展机制,再把`spring.factories`文件和`EnableAutoConfigurati
    的头像 发表于 03-24 14:27 553次阅读
    Java中的<b class='flag-5'>SPI</b>动态<b class='flag-5'>扩展</b>(下)

    Spring中11个最常用的扩展点分享1

    在使用spring的过程中,我们有没有发现它的扩展能力很强呢?由于这个优势的存在,使得spring具有很强的包容性,所以很多第三方应用或者框架可以很容易的投入到spring的怀抱中。今
    的头像 发表于 05-11 10:48 554次阅读

    Spring中11个最常用的扩展点分享2

    在使用spring的过程中,我们有没有发现它的扩展能力很强呢?由于这个优势的存在,使得spring具有很强的包容性,所以很多第三方应用或者框架可以很容易的投入到spring的怀抱中。今
    的头像 发表于 05-11 10:48 389次阅读

    Java、Spring、Dubbo三者SPI机制的原理和区别

    其实我之前写过一篇类似的文章,但是这篇文章主要是剖析dubbo的SPI机制的源码,中间只是简单地介绍了一下Java、SpringSPI机制
    的头像 发表于 06-05 15:21 1009次阅读
    Java、<b class='flag-5'>Spring</b>、Dubbo三者<b class='flag-5'>SPI</b><b class='flag-5'>机制</b>的原理和区别

    什么是SPI机制

    1、前言 在之前的 JVM 分析系列之类加载 提到过 Java SPI 机制,主要是类加载器反双亲委派的实现(第三方包不在指定jdk路径,一般类加载器无法加载,需要特殊
    的头像 发表于 10-08 15:03 1129次阅读
    什么是<b class='flag-5'>SPI</b><b class='flag-5'>机制</b>

    通过使用多路复用器实现基于SPI的闪存扩展

    电子发烧友网站提供《通过使用多路复用器实现基于SPI的闪存扩展.pdf》资料免费下载
    发表于 09-21 10:50 0次下载
    通过使用多路复用器<b class='flag-5'>实现</b>基于<b class='flag-5'>SPI</b>的闪存<b class='flag-5'>扩展</b>