详解 SPI 机制

SPI(Service Provider Interface) 是 JDK 内置的一种服务提供发现机制:可以用来启用框架扩展和替换组件,主要用于框架中开发。例如:Dubbo、Spring、Common-Logging,JDBC 等都是采用 SPI 机制,针对同一接口采用不同的实现提供给不同的用户,从而提高了框架的扩展性

1. Java SPI 实现

Java 内置的 SPI 通过 java.util.ServiceLoader 类解析 classPath 和 jar 包的 META-INF/services/ 目录下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用

1.1 案例

对于智能家居系统,只要是相同品牌下的产品,连上 wifi 就能够通过手机 app 控制了,非常方便。虽然产品不断更新换代,型号更新层出不穷,但是同种家电在 app 上操作起来,功能一般都是一样的。就拿空调来说,我们在 app 上操作起来一般也就三个主要功能:开关,选模式,调节温度

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

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

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

①:定义接口

新建一个 maven 项目 aircondition-standard,定义一个接口:

public interface AirConditionService {// 获取型号String getType();// 开关void turnOnOff();// 调节温度void adjustTemperature(int temperature);// 模式变更void changeModel(int modelId);}

用 maven 把它打成 jar 包,供后续的服务实现使用(服务提供者在项目中就可以引入这个 jar 包)

mvn clean install

有了这套规范,就保证了产品后期不管怎么更新换代,都能接入到系统来

②:服务实现

现有两个类型的空调:挂式空调(HangingType)、立式空调(VerticalType)

挂式空调: 新建一个项目 aircondition-hanging-type,并引入上述 jar:

<dependency><groupId>com.zzc</groupId><artifactId>aircondition-standard</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>

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

public class HangingTypeAirConditionService implements AirConditionService {@Overridepublic String getType() {return "HangingType";}@Overridepublic void turnOnOff() {// TODOSystem.out.println("挂式空调开关");}@Overridepublic void adjustTemperature(int temperature) {// TODOSystem.out.println("挂式空调调节温度");}@Overridepublic void changeModel(int modelId) {// TODOSystem.out.println("挂式空调更换模式");}}

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

com.zzc.airconditionhangingtype.service.HangingTypeAirConditionService

如下图:

在这里插入图片描述
这样,一个服务方的简单实现就搞定了,用 maven 打成 jar 包,之后就可以提供给调用方使用了

同理,我们可以再创建一个立式空调的项目 aircondition-vertical-type

public class VerticalTypeAirConditionService implements AirConditionService {@Overridepublic String getType() {// TODOreturn "VerticalType";}@Overridepublic void turnOnOff() {// TODOSystem.out.println("立式空调开关");}@Overridepublic void adjustTemperature(int i) {// TODOSystem.out.println("立式空调调节温度");}@Overridepublic void changeModel(int i) {// TODOSystem.out.println("立式空调更换模式");}}

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

com.zzc.service.VerticalTypeAirConditionService
③:服务发现

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

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

<dependency><groupId>com.zzc</groupId><artifactId>aircondition-hanging-type</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency><groupId>com.zzc</groupId><artifactId>aircondition-vertical-type</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>

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

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

public class AirConditionApp {public static void main(String[] args) {new AirConditionApp().turnOn("VerticalType");}public void turnOn(String type){ServiceLoader<AirConditionService> load = ServiceLoader.load(AirConditionService.class);for (AirConditionService iAircondition : load) {System.out.println("检测到:"+iAircondition.getClass().getSimpleName());if (type.equals(iAircondition.getType())){iAircondition.turnOnOff();}}}
}

测试结果:

在这里插入图片描述

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

1.3 JDBC 的 SPI 机制

深入理解 Java 中的 SPI 机制

2. Spring 的 SPI 机制

2.1 简介

Spring SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单:

//获取所有factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

Spring 也是支持 ClassPath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 ArrayList 中。由于没有别名,所以也没有去重的概念,有多少就添加多少

但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个 spring.factories 文件,那么你项目中的文件会被第一个加载,得到的 Factories中,项目中 spring.factories 里配置的那个实现类也会排在第一个

如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个 META-INF/spring.factories 文件,只添加你要的那个配置,不要完整的复制一遍 Spring Boot 的 spring.factories 文件然后修改

2.2 案例

①:定义接口:HelloService

public interface HelloService {void sayHello();
}

②:其实现类:HelloServiceImpl1

public class HelloServiceImpl1 implements HelloService {@Overridepublic void sayHello() {System.out.println("Hello World from HelloServiceImpl1!");}
}

HelloServiceImpl2

public class HelloServiceImpl2 implements HelloService {@Overridepublic void sayHello() {System.out.println("Hello World from HelloServiceImpl2!");}
}

③:spring.factories 文件:resources/META-INF 路径下

com.zzc.service.HelloService=\
com.zzc.service.impl.HelloServiceImpl1,\
com.zzc.service.impl.HelloServiceImpl2

idea当中如何创建*.factories文件

④:HelloServiceLoader

public class HelloServiceLoader {public static void main(String[] args) {// 使用Spring SPI机制动态加载HelloService实现类List<HelloService> loader = SpringFactoriesLoader.loadFactories(HelloService.class, Thread.currentThread().getContextClassLoader());// 遍历所有实现类并调用sayHello方法for (HelloService helloService : loader) {helloService.sayHello();}}
}

2.3 实现原理

查看 SpringFactoriesLoader 类源码:

public final class SpringFactoriesLoader {public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap();private SpringFactoriesLoader() {}// 加载并实例化给定类型的工厂实现public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);List<T> result = new ArrayList(factoryImplementationNames.size());Iterator var5 = factoryImplementationNames.iterator();while(var5.hasNext()) {String factoryImplementationName = (String)var5.next();result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));}return result;}public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {String factoryTypeName = factoryType.getName();return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());}// 获取所有 jar 包中 META-INF/spring.factories 文件路径,整合 factoryClass 类型的实现类名称,获取到实现类的全类名称后进行类的实例化操作private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {Map<String, List<String>> result = (Map)cache.get(classLoader);if (result != null) {return result;} else {HashMap result = new HashMap();try {Enumeration urls = classLoader.getResources("META-INF/spring.factories");while(urls.hasMoreElements()) {URL url = (URL)urls.nextElement();UrlResource resource = new UrlResource(url);Properties properties = PropertiesLoaderUtils.loadProperties(resource);Iterator var6 = properties.entrySet().iterator();while(var6.hasNext()) {Entry<?, ?> entry = (Entry)var6.next();String factoryTypeName = ((String)entry.getKey()).trim();String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());String[] var10 = factoryImplementationNames;int var11 = factoryImplementationNames.length;for(int var12 = 0; var12 < var11; ++var12) {String factoryImplementationName = var10[var12];((List)result.computeIfAbsent(factoryTypeName, (key) -> {return new ArrayList();})).add(factoryImplementationName.trim());}}}result.replaceAll((factoryType, implementations) -> {return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));});cache.put(classLoader, result);return result;} catch (IOException var14) {throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);}}}// 实例化 Bean:通过反射来实现对应的初始化private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);return ReflectionUtils.accessibleConstructor(factoryImplementationClass, new Class[0]).newInstance();}
}

3. Common-Logging 的 SPI

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/441133.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot博物馆游客预约系统【附源码】

基于SpringBoot博物馆游客预约系统 效果如下&#xff1a; 主页面 注册界面 展品信息界面 论坛交流界面 后台登陆界面 后台主界面 参观预约界面 留言板界面 研究背景 随着现代社会的快速发展和人们生活水平的提高&#xff0c;文化生活需求也在日益增加。博物馆作为传承文化、…

k8s 中的 PV 的动态供给

目录 1 存储类 Storageclass 介绍 1.1 StorageClass 说明 1.2 StorageClass 的属性 2 存储分配器 NFS Client Provisioner 2.1 官网存储分配器的部署介绍 2.2 实现动态创建 PV 模版清单文件的介绍 2.2.1 Storageclass 存储类的模版 2.2.2 创建 Provisioner 制备器的模版 2.2.3…

【Linux】文件IO系统[ 库函数 ]封装了[ 系统调用 ] +【区分文件结构体FILE和file与files_srtuct表】(读写接口盘点与介绍)

前言 大家好吖&#xff0c;欢迎来到 YY 滴Linux系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

世邦通信股份有限公司IP网络对讲广播系统RCE

漏洞描述 SPON世邦IP网络广播系统采用的IPAudio™技术, 将音频信号以数据包形式在局域网和广域网上进行传送&#xff0c;是一套纯数字传输的双向音频扩声系统。传统广播系统存在的音质不佳&#xff0c;传输距离有限&#xff0c;缺乏互动等问题。该系统设备使用简便&#xff0c…

AAA Mysql与redis的主从复制原理

一 &#xff1a;Mysql主从复制 重要的两个日志文件&#xff1a;bin log 和 relay log bin log&#xff1a;二进制日志&#xff08;binnary log&#xff09;以事件形式记录了对MySQL数据库执行更改的所有操作。 relay log&#xff1a;用来保存从节点I/O线程接受的bin log日志…

界面控件DevExpress中文教程 - 如何拓展具有AI功能的文本编辑器(一)

本文重点介绍了DevExpress在近年来最热门领域——人工智能(AI)和自然语言处理(NLP)的改进&#xff01; NLP是人工智能的一个分支&#xff0c;它允许计算机与人类语言进行交互&#xff0c;这包括以有意义/有用的方式理解、解释、生成和回应文本(和语音)的能力。基于NLP的功能允…

仿RabbitMQ实现消息队列客户端

文章目录 客⼾端模块实现订阅者模块信道管理模块异步⼯作线程实现连接管理模块生产者客户端消费者客户端 客⼾端模块实现 在RabbitMQ中&#xff0c;提供服务的是信道&#xff0c;因此在客⼾端的实现中&#xff0c;弱化了Client客⼾端的概念&#xff0c;也就是说在RabbitMQ中并…

认知战认知作战:激发认知战战术分享热情的秘诀

认知战认知作战&#xff1a;激发认知战战术分享热情的秘诀 认知战认知作战&#xff1a;激发认知战战术分享热情的秘诀 关键词&#xff1a;认知战, 认知作战, 创造独特体验, 融入社交元素, 情感共鸣策略, 分享激励机制, 战略形象塑造, 个性化内容推荐,认知作战,新质生产力,人类…

E. Tree Pruning Codeforces Round 975 (Div. 2)

原题 E. Tree Pruning 解析 本题题意很简单, 思路也很好想到, 假设我们保留第 x 层的树叶, 那么对于深度大于 x 的所有节点都要被剪掉, 而深度小于 x 的节点, 如果没有子节点深度大于等于 x, 那么也要被删掉 在做这道题的时候, 有关于如何找到一个节点它的子节点能通到哪里,…

用Arduino单片机制作一个简单的音乐播放器

Arduino单片机上有多个数字IO针脚&#xff0c;可以输出数字信号&#xff0c;用于驱动发声器件&#xff0c;从而让它发出想要的声音。蜂鸣器是一种常见的发声器件&#xff0c;通电后可以发出声音。因此&#xff0c;单片机可以通过数字输出控制蜂鸣器发出指定的声音。另外&#x…

马丁代尔药物大典数据库

马丁代尔药物大典是一本由Pharmaceutical Press出版的参考书&#xff0c;拥有全球使用的近 6000 种药物和药品&#xff0c;包括超过 125,000 种专有制剂的详细信息。其中还包括近 700 篇疾病治疗评论。 它于 1883 年首次出版&#xff0c;马丁代尔包含全球临床用药信息&#xff…

【qt】QQ仿真项目2

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 一览全局: QQ仿真项目 一.主窗口的创建二.主窗口的ui设计三.初始化状态,等级,app…

<Rust>iced库(0.13.1)学习之部件(三十一):picklist部件的使用及可变style设置

前言 本专栏是学习Rust的GUI库iced的合集,将介绍iced涉及的各个小部件分别介绍,最后会汇总为一个总的程序。 iced是RustGUI中比较强大的一个,目前处于发展中(即版本可能会改变),本专栏基于版本0.12.1. 注:新版本已更新为0.13 概述 这是本专栏的第三十一篇,主要说明下…

俗人,精气神,歌曲《错的人》

精气神&#xff0c;在人体中&#xff0c;精指构成人体生命活动的各层次的有形元素&#xff0c;常呈固体或液体状态。 哲学前提&#xff1a;世界上的一切&#xff0c;从微观上讲&#xff0c;都是由精微物质构成的&#xff0c;比如基本粒子。 关于有形与无形、与主观关注点相关…

DHCP安装

步骤 1&#xff1a;安装DHCP服务器 在系统上安装DHCP服务。以下是安装命令&#xff1a; # 安装DHCP软件包 yum install dhcp步骤 2&#xff1a;配置DHCP服务器 安装完成后&#xff0c;需要配置DHCP服务器来绑定MAC地址和IP地址。 # 备份原始的DHCP配置文件 cp /etc/dhcp/dh…

迁移学习案例-python代码

大白话 迁移学习就是用不太相同但又有一些联系的A和B数据&#xff0c;训练同一个网络。比如&#xff0c;先用A数据训练一下网络&#xff0c;然后再用B数据训练一下网络&#xff0c;那么就说最后的模型是从A迁移到B的。 迁移学习的具体形式是多种多样的&#xff0c;比如先用A训练…

HCIA综合实验

实验步骤 1.划分网段 内网部分---三个大块 2.先配交换机 左边&#xff1a;3个vlan &#xff0c;3个access&#xff0c;1个trunk 右边&#xff1a;2个vlan &#xff0c;2个access&#xff0c;1个trunk 3.再配路由 3.1 r5先配接口ipg/0/0/0 口配子接口 g0/0/0.1-0.3 g0/0/1 …

【YOLOv8实时产品缺陷检测】

YOLOv8应用于产品缺陷检测实例 项目概况项目实现YOLOv8安装及模型训练关键代码展示动态效果展示 项目概况 本项目是应用YOLOv8框架实现训练自定义模型实现单一零件的缺陷检测&#xff0c;软件界面由PyQt5实现。 功能已正式使用&#xff0c;识别效果达到预期。 项目实现 项目…

手机误删照片?试试这5款免费数据恢复神器!

大家好&#xff01;今天咱们来聊聊一个大家都关心的话题——免费数据恢复工具。不论是误删照片、视频&#xff0c;还是丢失重要文件&#xff0c;数据恢复都是个让人头疼的问题。但好消息是&#xff0c;现在有众多免费的数据恢复工具能帮助我们找回失去的数据。今天我就来为大家…