从实现到原理,聊聊Java中的SPI动态扩展
原创:微信公众号
码农参上,欢迎分享,转载请保留出处。
八股文背多了,相信大家都听说过一个词,SPI扩展。
有的面试官就很喜欢问这个问题,SpringBoot的自动装配是如何实现的?
基本上,你一说是基于spring的SPI扩展机制,再把spring.factories文件和EnableAutoConfiguration提一下,那么这个问题就答的八九不离十了。
就像四五年前,我去面试的时候被问到这个问题,SPI动态扩展机制这几个词从嘴里一说出来,就把面试官唬的一愣一愣的。可能他们也没见过这么能装逼的,一句话能简简单单说明白,非要拽一个听上去很高大上的词。
话说回来,被唬住的可不止是面试官,其实还有我自己。至于SPI扩展究竟是个啥,是怎么实现的,我当时也根本不明白。
不过现在的面试就是这样,对线八股文,要想唬住面试官,就得先唬住自己。
那么我们今天暂且不提spring的SPI扩展,先来看看java本身自带的SPI扩展机制是怎么一回事。
1、简介
SPI的全称是Service Provider Interface,翻译过来就是服务提供者的接口,它所实现的其实是一种服务的发现机制。
这么说起来可能还是有点不好理解,我举个例子来类比一下。
在spring项目中,写service层代码前,会约定俗成的会添加一个接口层。然后通过spring中的依赖注入,可以借助@Autowired等方式注入这个接口的实现类的实例对象,之后对于service的调用一般也基于接口操作。
简单形容就是这样的:

如图所示,接口、实现类都是由服务提供方提供,我们可以把controller看作服务调用者,调用方只管调用接口就可以了。
虽然也有声音认为,大部分情况下service只有一个实现类,接口层显得有些多余。但是在《Head First Design Patterns》这本书中,大佬们还是建议过:
Program to an interface, not an implementation.
没错,就是常说的要面向接口编程。至于好处,也不外乎是降低耦合度、方便日后扩展、提高了代码的灵活性和可维护性等等。
在上面这个例子里,这个接口层和其中的方法我们可以称之为API,而我们要讨论的SPI和它相比,有类似也有差异,还是先看图:

简单来说,就是服务的调用方定义一个接口规范,可以由不同的服务提供者实现。并且,调用方能够通过某种机制来发现服务提供方,并通过接口调用它的能力。
通过对比,我们可以看出它们虽然都有着接口这一层面,但还是有很大的不同:
API中的接口是服务提供者给服务调用者的一个功能列表,而SPI中更多强调的是,服务调用者对服务实现的一种约束,服务提供者根据这种约束实现的服务,可以被服务调用者发现。
说白了,Java中的SPI实现的就是,你按我的接口规范实现服务,我就能通过某种机制为这个接口寻找到这个服务。
这么说起来可能还有些抽象,下面我们举一个例子,类比具体描述一下这个过程。
2、定义接口
说起智能家居系统,大家现在都比较熟悉了,只要是相同品牌下的产品,连上wifi就能够通过手机app控制了,非常方便。
虽然产品不断更新换代,型号更新层出不穷,但是同种家电在app上操作起来,功能一般都是一样的。就拿空调来说,我们在app上操作起来一般也就三个主要功能:开关,选模式,调节温度。
假设我现在在客厅、卧室、书房安装了3款不同型号的空调,并把它们都接入到了我app中,那么之后的操作都是相同的几个按键,简单粗暴。

思考一下,无论是开关还是调温,都是通过app去调用设备的接口罢了,那么如果不同型号的空调各写各的接口,后端app在开发的时候光对接接口都麻烦的要死。
解决方法也很简单,我先定义一套接口规范,不管你以后什么型号的空调,都按我的规范来实现接口。以后只要我能发现你的设备,那么都可以按相同的方法来调用接口。
那么下面就先来定义这么一套接口规范,如果你以后想要接入智能家居系统,那么就要遵循这个规范来开发接口。
新建一个项目作为标准,就叫aircondition-standard好了,然后创建一个接口。除了3个操作以外,我们再添加一个获取空调型号的方法。
public interface IAircondition {// 获取型号String getType();// 开关void turnOnOff();// 调节温度void adjustTemperature(int temperature);// 模式变更void changeModel(int modelId);
}
这个接口后面要给服务的实现方来使用,用maven把它打成jar包:
mvn clean install
之后服务提供者在项目中就可以引入这个jar包了,有了这套规范,就保证了产品后期不管怎么更新换代,都能接入到系统来。
3、服务实现
制定并发布完规则后,挂式空调作为第一个服务提供者就来了,新建一个项目aircondition-hanging-type,并引入刚才打好的jar包:
<dependency><groupId>com.cn.hydra</groupId><artifactId>aircondition-standard</artifactId><version>1.0-SNAPSHOT</version>
</dependency>
创建服务类,并实现前面定义的接口:
public class HangingTypeAirconditionimplements IAircondition{public String getType() {return "HangingType";}public void turnOnOff() {System.out.println("挂式空调开关");}public void adjustTemperature(int i) {System.out.println("挂式空调调节温度");}public void changeModel(int i) {System.out.println("挂式空调更换模式");}
}
在项目的resources的目录下,创建META-INF/services目录,然后以前面定义的接口名com.cn.hydra.IAircondition创建文件,并在文件中写入实现类的全限定名。
com.cn.hydra.HangingTypeAircondition
整个项目结构非常简单:

这样,一个服务方的简单实现就搞定了,用maven打成jar包,之后就可以提供给调用方使用了。
同理,我们可以再创建一个立式空调的项目aircondition-vertical-type,也只创建一个服务类:
public class VerticalTypeAirconditionimplements IAircondition{public String getType() {return "VerticalType";}public void turnOnOff() {System.out.println("立式空调开关");}public void adjustTemperature(int i) {System.out.println("立式空调调节温度");}public void changeModel(int i) {System.out.println("立式空调更换模式");}
}
还是按上面的命名规则,创建一个配置文件:
com.cn.hydra.VerticalTypeAircondition
同样,打成jar包就完事了,至于服务调用者如何去发现和调用这两个服务,下面详细再说。
4、服务发现
现在两个服务提供方都实现了接口,下面关键的一步就是服务发现,这一步java中的spi发现机制已经帮我们实现好了。
创建一个新项目aircondition-app,引入上面打好的两个jar包。
<dependencies><dependency><groupId>com.cn.hydra</groupId><artifactId>aircondition-hanging-type</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>com.cn.hydra</groupId><artifactId>aircondition-vertical-type</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
按照上面的说法,虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法。
下面,就是关键的服务发现环节,我们写一个方法,根据型号去调用对应空调的开关方法。
public class AirconditionApp {public static void main(String[] args) {new AirconditionApp().turnOn("VerticalType");}public void turnOn(String type){ServiceLoader<IAircondition> load = ServiceLoader.load(IAircondition.class);for (IAircondition iAircondition : load) {System.out.println("检测到:"+iAircondition.getClass().getSimpleName());if (type.equals(iAircondition.getType())){iAircondition.turnOnOff();}}}
}
测试结果:

可以看到,测试过程中,通过定义的接口IAircondition发现了两个实现类,并通过参数,调用了特定实现类的某个方法。整段代码中没有出现过具体的服务实现类,操作都是通过接口调用。
5、原理
了解了spi的工作流程,我们再来看看它的实现,其实最关键的就是上面代码中出现的ServiceLoader这个类。
上面的示例代码中,对于ServiceLoader的load()方法的结果,我们用for循环进行了遍历,这一点我们看一下源码就能明白,因为ServiceLoader实现了Iterable这一接口,而整个服务发现的核心,就在它的iterator()方法中。

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

注释写的非常明白,providers就是一个缓存,在迭代器中如果先从这里面进行查找,如果里面有就继续往下找,没有了的话就用这个懒加载的lookupIterator查找。
那么就简单了,接着往下看LazyIterator,看看它里面的hasNext()和next()两个方法是怎么实现的。

这个acc是一个安全管理器,在前面通过System.getSecurityManager()判断并赋值,debug看一下这里都是null,所以直接看hasNextService()和nextService()方法就可以了。
在hasNextService()方法中,会取出接口取出实现类的类名放到nextName中:

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

在迭代器的迭代过程中,会完成所有实现类的实例化,其实归根结底,还是基于java反射去实现的。
6、应用
要说spi的实际应用,大家最常见的应该就是日志框架slf4j了,它利用spi实现了插槽式接入其他具体的日志框架。
说白了,slf4j本身就是个日志门面,并不提供具体的实现,需要绑定其他具体实现才能真正的引入日志功能。
例如我们可使用log4j2作为具体的绑定器,只需要在pom中引入slf4j-log4j12,就可以使用具体功能。
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.3</version>
</dependency>
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>2.0.3</version>
</dependency>
引入项目后,点开它的jar包看一下具体结构:

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

大意就是在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提供了一种比较特别的服务发现和调用机制,通过接口灵活的将服务调用与服务提供者分离,用于提供给第三方实现扩展时还是很方便的。但是也有缺点,比方说一旦加载一个接口,就会把所有实现类都加载进来,可能会加载到不需要的冗余服务。不过站在整体角度上,还是给我们提供了一种非常不错的框架扩展、集成的思路。
那么,这次的分享就到这里,我是Hydra,我们下篇再见。
作者简介,
码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术,关注领取大量学习资料。
也欢迎添加我好友,多多交流。
相关文章:
从实现到原理,聊聊Java中的SPI动态扩展
原创:微信公众号 码农参上,欢迎分享,转载请保留出处。 八股文背多了,相信大家都听说过一个词,SPI扩展。 有的面试官就很喜欢问这个问题,SpringBoot的自动装配是如何实现的? 基本上,…...
3、MySQL字符集
1.MySQL字符集和校验规则 字符集:是一套符号和编码的规则校验规则:是对该套符号和编码的校验,定义字符的排序和比较规则,其中是否区分大小写,跟校验规则有关。2.查看字符集方法 netstat -lntup |grep 3306 tcp6 0 0 :::3306 :::* …...
大漠插件最新中文易语言模块7.2302
模块名称:大漠插件中文模块最新通用7.2302模块简介:大漠插件中文模块最新通用7.2302模块特色:原翻译:花老板完善命令备注:易生易世本人花费一个月时间才将命令完善了插件的备注说明.且用且珍惜去掉了大漠插件定制版类.因为没用.模块特色:什么是中文模块?大漠插件模块是由大漠类…...
极客大挑战 2021
题量很大,收获挺多,持续时间也长,据说结束之后会再持续一段时间,然后题目会开源。 WEB Dark 暗网签到,难以置信 Welcome2021 改个请求方法会提示你文件,再进去就好了 babysql 直接把请求包扔sqlmap里&…...
C#开发的OpenRA加载文件的管理
C#开发的OpenRA加载文件的管理 在前面我们分析了mod.yaml文件,发现里面有很多文件列表, 比如下像下面的文件: Packages: ~^SupportDir|Content/cnc ~^SupportDir|Content/cnc/movies ^EngineDir $cnc: cnc ^EngineDir|mods/common: common ~speech.mix ~conquer.mix ~sounds…...
SSM实现文件上传
目录 SSM实现文件上传 1、修改from表单请求方式改为post,添加属性 2、修改springmvc配置文件,添加一下配置 3、后端方法 SSM实现文件上传 1、修改from表单请求方式改为post,添加属性: enctype"multipart/form-data"…...
OPENCV计算机视觉开发实践-图像的基本概念
1.图像与图形: 图像->客观世界的反映,图与像之结合 图->物体透射光与反射光的分布 像->人的视觉得对图的认识 图像->通过照相,摄像,扫描产生. 图形->通过数学规则产生,或者具有一定规则的图案.用一组符号或线条表示性质. 2.数字图像: 数字图像->称数码图像或…...
Android 9.0 ResolverActivity.java多个app选择界面去掉始终保留仅有一次
1.前言 在9.0的系统rom定制化开发过程中,在系统中安装同类型多个app的时候,在系统启动的过程中,会在启动launcher或播放器的过程中,在启动的过程中都是弹出选择框的,然后在选择启动哪个app,这些选择都是在ResolverActivity.java中完成的,所以需要在ResolverActivity.java…...
【算法 | 例题简答】相关例题讲解
目录 简答题 计算题 时间复杂度的计算 递归算法计算 背包问题(0-1背包问题) 回溯法 动态规划法 编程题 用回溯法解方程 动态规划法解决蜘蛛吃蚊子 用分治法解决抛硬币问题 用二分法分两边求最大值 简答题 1、什么是算法?算法有哪…...
浅谈AQS
1.前言 AQS是AbstractQueuedSynchronizer(抽象同步队列)的简写,它是实现同步器的基础组件,并发包下的锁就是通过AQS实现的。作为开发者可能并不会直接用到AQS,但是知道其原理对于架构设计还是很有帮助的。 那为什么说…...
关于服务连接器(Servlet)你了解多少?
Servlet 1 简介 Servlet是JavaWeb最为核心的内容,它是Java提供的一门动态web资源开发技术。 使用Servlet就可以实现,根据不同的登录用户在页面上动态显示不同内容。 Servlet是JavaEE规范之一,其实就是一个接口,将来我们需要定义…...
面对学员的投诉,中创教育是如何处理的?
客户满意度的检测指标是客户的期望值和服务感知之间的差距。当顾客购买商品时,对商品本身和企业的服务都抱有良好的愿望和期盼值,如果这些愿望和要求得不到满足,就会失去心理平衡,由此产生的抱怨和想"讨个说法"的行为&a…...
算法问题——排序算法问题
摘要 查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。一般在面试中最常…...
ArcGIS网络分析之构建网络分析数据集(一)
说明: 1. 本文主要用于演示网络分析服务的搭建过程。所以在此不会深入讨论网络分析服务的每一个细节,本文的目的就是让初学者学会使用网络分析服务进行基本的分析(主要针对后续的WEB开发):路径分析,最近设施点分析,以及服务区分析。 2.关于OD成本矩阵分析,多路径配送,…...
微电影的行业痛点有哪些?
微电影全称微型电影,又称微影。是指能够通过互联网新媒体平台传播(几分钟到60分钟不等)的影片,适合在移动状态、短时休闲状态下观看,具有完整故事情节的“微(超短)时”(几分钟-60分钟)放映、“微(超短)周期制作(7-15天…...
spark3.0源码分析-driver-executor心跳机制
前言 driver和executor心跳机制分为两种机制: 1、executor发送心跳机制 2、driver接受心跳机制 至于为何要分为两种,原因是在分布式场景中,服务的稳定性是无法保障的,例如executor宕机后无法发送心跳,故driver端需要…...
数据分析就要选择这款免费报表工具
对于一家企业来说,在日常运营的过程中本身就会产出很多的数据,那么这些数据本身就应该形成报表。可是如果只是选择手工的一种操作,确实需要浪费大量的人力物力。伴随着科技进入到快速发展的阶段,市面上更是出现了很多报表工具可以…...
node学习-3:服务器渲染和客户端渲染
1. 概念 一.服务端渲染,后端嵌套模板,后端渲染模板,SSR(后端把页面组装好) 做好静态页面,动态效果 把前端代码提供给后端,后端则把静态html以及里面的假数据给删除掉 通过模板进行动态生成h…...
LeetCode刷题笔记和周赛题解总目录
之前一段时间一直在刷LeetCode,在上面积累了很多笔记,这些笔记是做题过程中的一些重要积累和心得,现在将它们汇总和总结至此,此博客将不断更新。 刷题笔记(提供md和pdf两种格式可供下载,不断更新) LeetCode刷题笔记 …...
用类比方式学习编程中函数递归(个人理解仅供参考)(内含汉诺塔问题的求解)
目录 1.前言 2.递归的数学模型 3.相关的c语法 4.将递归的数学模型写成编程语言 5.利用类比方法将实际问题的代码写成函数递归的形式 例1: 例2: 6.汉诺塔问题的求解 1.前言 本人在学习函数递归编程方法的过程中,发现用类比的方式学习递归法可帮助我们在各种编…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...
脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)
一、数据处理与分析实战 (一)实时滤波与参数调整 基础滤波操作 60Hz 工频滤波:勾选界面右侧 “60Hz” 复选框,可有效抑制电网干扰(适用于北美地区,欧洲用户可调整为 50Hz)。 平滑处理&…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...
【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序
一、开发准备 环境搭建: 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 项目创建: File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...
