Java中的SPI原理浅谈

news/2024/5/5 15:36:52/文章来源:https://blog.csdn.net/AS011x/article/details/126947113

在面向对象的程序设计中,模块之间交互采用接口编程,通常情况下调用方不需要知道被调用方的内部实现细节,因为一旦涉及到了具体实现,如果需要换一种实现就需要修改代码,这违反了程序设计的"开闭原则"。 所以我们一般有两种选择:一种是使用API(Application Programming Interface),另一种是SPI(Service Provider Interface),API通常被应用程序开发人员使用,而SPI通常被框架扩展人员使用。

在进入下面学习之前,我们先来再加深一下API和SPI这两个的印象:

API:由实现方制定接口标准并完成对接口的不同实现,这种模式服务接口从概念上更接近于实现方;

SPI:由调用方制定接口标准,实现方来针对接口提供不同的实现;从前半句话我们来看 ,SPI其实就是" 为接口查找实现 " 的一种服务发现机制;这种模式,服务接口组织上位于调用方所在的包中,实现位于独立的包中。

API和SPI简略图示:

看完上面的简单图示,相信大家对API和SPI的区别有了一个大致的了解,现在我们使用SPI机制来实现我们一个简单的日志框架:

第一步,创建一个maven项目命名为spi-interface,定义一个SPI对外服务接口,用来后续提供给调用者使用;

package cn.com.wwh;
/*** * @FileName Logger.java* @version:1.0* @Description: 服务提供者接口* @author: wwh* @date: 2022年9月19日 上午10:31:53*/
public interface Logger {/*** * @Description:(功能描述)* @param msg*/public void info(String msg);/*** * @Description:(功能描述)* @param msg*/public void debug(String msg);
}
package cn.com.wwh;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;/*** * @FileName LoggerService.java* @version:1.0* @Description: 为服务的调用者提供特定的功能,是SPI的核心功能* @author: wwh* @date: 2022年9月19日 上午10:33:30*/
public class LoggerService {private static final LoggerService INSTANCE = new LoggerService();private final Logger logger;                          private final List<Logger> loggers = new ArrayList<>();private LoggerService() {//ServiceLoader是实现SPI的核心类ServiceLoader<Logger> sl = ServiceLoader.load(Logger.class);Iterator<Logger> it = sl.iterator();while (it.hasNext()) {loggers.add(it.next());}if (!loggers.isEmpty()) {logger = loggers.get(0);} else {logger = null;}}/*** @Description:(功能描述)* @return*/public static LoggerService getLoggerService() {return INSTANCE;}/*** * @Description:(功能描述)* @param msg*/public void info(String msg) {if (logger == null) {System.err.println("在info方法中没有找到Logger的实现类...");} else {logger.info(msg);}}/*** * @Description:(功能描述)* @param msg*/public void debug(String msg) {if (logger == null) {System.err.println("在debug方法中没有找到Logger的实现类...");} else {logger.info(msg);}}
}

将上面这个这个项目打成spi-interface.jar包。

第二步,新建一个maven项目并导入第一步中打出来的spi-interface.jar包,这个项目用来提供服务的实现,定义一个类,实现第一步中定义的cn.com.wwh.Logger接口,示例代码如下:

package cn.com.wwh;import cn.com.pep.Logger;/*** * @FileName Logback.java* @version:1.0* @Description: 服务接口的实现类* @author: wwh* @date: 2022年9月19日 上午10:50:31*/
public class Logback implements Logger {@Overridepublic void debug(String msg) {System.err.println("调用Logback的debug方法,输出的日志为:" + msg);}@Overridepublic void info(String msg) {System.err.println("调用Logback的info方法,输出的日志为:" + msg);}}

同时在当前项目的classpath路径下建立META-INF/services/文件夹(至于为什么这么建立目录,我们一会儿再解释),并且新建一个名称为cn.com.wwh.Logger内容为cn.com.wwh.Logback的文件,这一步是关键(具体作用后面再详细说明),然后将上面第二步这个这个项目打成spi-provider.jar包,供给之后使用,我目前使用的开发工具是Eclipse,目录结构如下图所示:

第三步,编写测试类,新建一个maven项目,命名为spi-test,导入前面两个步骤打的spi-interface.jar和spi-provider.jar这两个jar包,并编写测试代码,示例如下:

package cn.com.wwh;import cn.com.pep.LoggerService;/*** * @FileName SpiTest.java* @version:1.0* @Description: * @author: wwh* @date: 2022年9月19日 上午10:56:31*/
public class SpiTest {public static void main(String[] args) {LoggerService logger = LoggerService.getLoggerService();logger.info("我是中国人");logger.debug("白菜多少钱一斤");}
}

有了SPI我们可以将服务和服务提供者轻松地解耦,假如将来的某一天我们需要将日志保存到数据库,或者通过网络发送,我们直接只需要替换针对服务接口的实现类即可,别的地方都不用修改,这更符合程序设计中的“开闭原则”。

SPI的大致原理是:应用启动的时候,扫描classpath下面的所有jar包,将jar包下的/META-INF/services/目录下的文件加载到内存中,进行一系列的解析(文件的名称是spi接口的全路径名称,文件内容应该是spi接口实现类的全路径名,可以用多个实现类,在文件中换行保存),之后判断当前类和当前接口是否是同一类型?结果为true,则通过反射生成指定类的实例对象,保存到一个map集合中,可以通过遍历或者迭代的方式拿出来使用。

SPI实质就是一个加载服务实现的工具 ,核心类是ServiceLoader,其实了解了SPI的原理,我们再接着探究JDK中的源码就没有那么费力了,下面我们开始源码分析吧。

ServiceLoader类是定义在java.util包下的,使用final定义禁止子类继承和修改,实现了Iterable接口,使得可以通过迭代或者遍历的方式获取SPI接口的不同实现。

从上面的我们所举的例子中,我们知道SPI的入口是ServiceLoader.load(Class<S> service)方法,我们来看看它都干了什么? 

上面的这 4步总的来说,就是使用指定的类型和当前线程绑定的classLoader实例化了一个LazyIterator对象赋值给lookupIterator这个引用,并且清除了原来providers列表中缓存的服务的实现。接下来我们调用了ServiceLoader实例的iterator()方法获取了一个迭代器,代码如下:

 1 public Iterator<S> iterator() {2         //通过匿名内部类方式提供了一个迭代器3         return new Iterator<S>() {4             //获取缓存的服务实现者的迭代器5             Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator();6             7             //判断迭代器中是否还有元素8             public boolean hasNext() {9                 //缓存的服务实现者的迭代器中已经没有元素了
10                 if (knownProviders.hasNext())
11                     return true;
12                 return lookupIterator.hasNext();//判断延迟加载的迭代器中是否还有元素
13             }
14 
15             //获取迭代其中的下一个元素
16             public S next() {
17                 if (knownProviders.hasNext())
18                     return knownProviders.next().getValue();
19                 return lookupIterator.next();//获取延迟加载的迭代器中的下一个元素
20             }
21 
22             public void remove() {
23                 throw new UnsupportedOperationException();
24             }
25         };
26    }

我们接着调用上步获取的迭代器it的hasNext()方法,因为我们在ServiceLoader.load()过程中其实是清除了providers列表中的缓存服务实现的,所以其实调用的是lookupIterator.hasNext()方法,如下:

 1 public boolean hasNext() {2         if (nextName != null) {//存在下一个元素3             return true;4         }5         if (configs == null) {//配置文件为空6             try {7                 String fullName = PREFIX + service.getName();//获取配置文件路径8                 if (loader == null)9                     configs = ClassLoader.getSystemResources(fullName);
10                 else
11                     configs = loader.getResources(fullName);//加载配置文件
12             } catch (IOException x) {
13                 fail(service, "Error locating configuration files", x);
14             }
15         }
16         //遍历配置文件内容
17         while ((pending == null) || !pending.hasNext()) {
18             if (!configs.hasMoreElements()) {
19                 return false;
20             }
21             pending = parse(service, configs.nextElement());//配置文件内容解析
22         }
23         nextName = pending.next();//获取服务实现类的全路径名
24         return true;
25     }
26 

假如上部判断为true,紧接着我们又调用了迭代器it的next()方式,同理也调用的是lookupIterator.next()方法,源码如下:

 1 public S next() {2         if (!hasNext()) {3             throw new NoSuchElementException();4         }5         String cn = nextName;//文件中保存的服务接口实现类的全路径名6         nextName = null;7         Class<?> c = null;8         try {9             //获取全限定名的Class对象
10             c = Class.forName(cn, false, loader);
11         } catch (ClassNotFoundException x) {
12             fail(service, "Provider " + cn + " not found");
13         }
14             //判断实现类和服务接口是否是同一类型
15         if (!service.isAssignableFrom(c)) {
16             fail(service, "Provider " + cn + " not a subtype");
17         }
18         try {
19             //通过反射生成服务接口的实现类,并判断这个实例是否是接口的实现
20             S p = service.cast(c.newInstance());
21             //将服务接口的实现缓存起来,并返回
22             providers.put(cn, p);
23             return p;
24         } catch (Throwable x) {
25             fail(service, "Provider " + cn + " could not be instantiated", x);
26         }
27         throw new Error(); // This cannot happen
28     }

其实spi实现的主要流程是: 扫描classpath路径下的所有jar包下的/META-INF/services/目录( 即我们需要将服务接口的具体实现类暴露在这个目录下,之前我们提到需要在实现类的classpath下面建立一个/META-INF/services/文件夹就是这个原因。 ),找到对应的文件,读取这个文件名找到对应的SPI接口,然后通过InputStream流将文件内容读出来,获取到实现类的全路径名,并得到这个全路径名所表示的Class对象,判断其与服务接口是否是同一类型,然后通过反射生成服务接口的实现,并保存在providers列表中,供给后续的使用。

SPI这种设计方式为我们的应用扩展提供了极大的便利,但是它的短板也是显而易见的,Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。所以说 Java SPI 无法按需加载实现类。

另外,SPI 机制在很多框架中都有应用:slf4j日志框架、Spring 框架的基本原理也是类似的反射。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别( Dubbo可以实现按需加载实现类 ),不过整体的原理都是一致的,我们今天先对SPI有个简单的了解,相信有了今天的基础理解剩下的那几个也不是什么难事。

好了,今天就到这儿了,文章中有说的不对的地方还请各位大佬批评指正,一起学习,共同进步,谢谢。

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

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

相关文章

常见的敏捷开发框架

读书笔记《敏捷测试&#xff1a;以持续测试促进持续交付》 极限编程 极限编程&#xff08;eXtreme Programming&#xff0c;XP&#xff09;是一种软件工程方法学&#xff0c; 是敏捷软件开发中最富有成效的几种方法学之一&#xff0c;基本思想是“沟通、 简单、反馈、勇气”。…

vue 中 父子组件值交互怎么使用 this.$emit

前提条件 父页面&#xff1a;代号爸爸子界面一号&#xff1a;代号 儿子一号子界面二号&#xff1a;儿子二号 父子配合 首先儿子一号有一个点击方法 点击后出发 爸爸的 changeType 方法 爸爸中怎么接收 爸爸接收儿子一号的值 改变 this.sortType 符合条件会触发儿子二号 …

MYSQL 自动补全工具

MYSQL自动补全工具 1、yum install -y python3 #安装Python 更换PIP源 1、mkdir -p ~/.pip 2、vim pip.conf [global] index-url https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host https://pypi.tuna.tsinghua.edu.cn 1、pip3 install --upgrade pip #更新 …

Hash,位图,布隆过滤器

文章目录哈希概念哈希冲突哈希函数除留余数法--(非常常用)直接定址法--(常用)平方取中法--(了解)折叠法--(了解)随机数法--(了解)数学分析法--(了解 &#xff09;哈希冲突解决闭散列闭散列代码关键码函数扩容插入删除find全部代码开散列代码InsertFindErase迭代器全部代码闭散列…

`Promise`全面解析

Promise入门 1.Promise介绍 1.1 理解 1.抽象表达 Promise是一门新的技术&#xff08;ES6规范&#xff09; Promise是JS中进行异常编程的新解决方案 备注&#xff1a;旧方案是单纯使用回调函数【】 2.具体表达 从语法上来说&#xff1a;Promise是一个构造函数从功能上来说…

计算机毕业设计 SSM+Vue钢材销售管理系统 建材物资销售平台 钢材建材管理系统 Java

计算机毕业设计 SSM+Vue钢材销售管理系统 建材物资销售平台 钢材建材管理系统 Java💖🔥作者主页:计算机毕设老哥🔥 💖精彩专栏推荐订阅:在 下方专栏👇🏻👇🏻👇🏻👇🏻Java实战项目专栏 Python实战项目专栏 安卓实战项目专栏 微信小程序实战项目专栏目…

核心交换机、汇聚交换机、接入交换机的概念

先从百度上扒几个图下来看看。 我是外行。看了很多的网络拓扑图&#xff0c;这些拓扑图里面包含很多的设备&#xff0c;共有的设备包括服务器&#xff0c;防火墙&#xff0c;交换机&#xff0c;路由器。先从交换机入手&#xff0c;解释下基本概念&#xff0c;学习下。 核心交换…

常用设计模式学习总结

设计模式是人们经过长期编程经验总结出来的一种编程思想。随着软件工程的不断演进&#xff0c;针对 不同的需求&#xff0c;新的设计模式不断被提出&#xff08;比如大数据领域中这些年不断被大家认可的数据分片思 想&#xff09;&#xff0c;但设计模式的原则不会变。基于设计…

ArcGIS 底图服务前端加载某些级别不显示问题

一、只前面几个级别 在js中加载&#xff0c;只能显示前面几级切片&#xff0c;放大到4&#xff0c;5级之后就无法放大。 分析原因 前一次发布切片&#xff0c;切片了5级之后的切片。 第二次发布切片时&#xff0c;出现了大比例尺图层组级别在ArcMap中没有勾选显示图层&#x…

【js】获取未来七天日期判断星期几

作为一个前端你要自给自足&#xff0c;自己造数据&#xff08;内心&#xff1a;有一句mmp不知道当讲不当讲&#xff09; 要求&#xff1a; .获取未来七天的日期和星期几&#xff0c;遍历数组进行渲染&#xff0c;要求从明天开始&#xff0c;不算今天 效果图如下&#xff1a;&a…

中国20强游戏公司2022上半年年报分析:复合因素下业绩增长承压,海外新兴市场蕴含增长新趋势

易观分析&#xff1a;2022上半年&#xff0c;国内游戏版号恢复发放、海外新兴市场迅速崛起&#xff0c;游戏行业迎来新转折点&#xff0c;但新游匮乏、买量效果差、投融资事件减少等因素仍持续影响行业发展。关注头部上市游戏企业上半年财务表现&#xff0c;可深入了解行业当下…

ubuntu18.04安装pcl1.9.1

ubuntu18.04安装pcl1.9.1所需的cmake3.14.3和vtk8.2.0 先安装Qt5&#xff0c;X11&#xff0c;OpenGL 根据VTK的要求要先安装Qt5,X11,OpenGL 根据 官方文档 &#xff0c;先更新qt5的依赖&#xff0c;python、Perl、Ruby 再进入 官网 下载Qt5&#xff1a;https://download.qt.…

诡异的定时任务-quartz

引出问题 现在是2022年9月19日14:38:19 定时任务上一次执行的时间是2022-09-14 15:03:12.620 将近5天的时间没执行。 造成的结果是&#xff0c;数据没入库。 上次重启是2个月之前。2022-7-21 上午9:52 肯定是有问题的。需要排查下原因。 解决步骤 使用的是quartz 看…

Flutter快学快用03 Hello Flutter:三步法掌握 Flutter,开始你的第一个应用

本课时将进入 Flutter 开发实践应用。在进入实践应用之前&#xff0c;我先讲解最基础的环境搭建&#xff0c;然后会应用 Dart 语言开发第一个 App — Hello Flutter&#xff0c;最后再讲解一些开发过程中常用的调试方法和工具。 本课时需要一定的实践动手能力&#xff0c;因此…

关于java中的反射,我只能努力到这一步了

文章目录反射是什么反射的用途反射的缺点反射的基本运用获取Class 类对象类相关的反射获取包名获取supperClass获取Public成员类获取声明的类获取所有Public构造方法获取泛型参数获取实现的接口获取所有Public方法获取所有Public字段获取所有注释获取权限修饰符字段相关反射获取…

基于注解实现缓存的框架 -- SpringCache

目录 1、介绍 2、注解 3、 入门案例 3.1 环境准备 3.2 CachePut注解 3.3 CacheEvict注解 3.4 Cacheable注解 3.4.1 测试 3.4.2 缓存非null值 4 、集成Redis 1、介绍 Spring Cache是一个框架&#xff0c;实现了基于注解的缓存功能&#xff0c;只需要简单地加一个注解…

Java开发学习---Maven私服(二)本地仓库访问私服配置与私服资源上传下载

一、本地仓库访问私服配置 我们通过IDEA将开发的模块上传到私服&#xff0c;中间是要经过本地Maven的 本地Maven需要知道私服的访问地址以及私服访问的用户名和密码 私服中的仓库很多&#xff0c;Maven最终要把资源上传到哪个仓库? Maven下载的时候&#xff0c;又需要携带用…

花了 3000 美元,我在 SaaStr 大会学到了什么?——码农驱动的 SaaS 增长之路

Michael Yuan&#xff0c;WasmEdge Runtime 创始人SaaStr 是 SaaS 领域最具影响力的大会之一。 历经疫情阴霾&#xff0c;SaaStr 盛会2022年再次归来。尽管 SaaS 估值如过山车一般疯涨又跌落&#xff0c;但即使在当下所谓的萧条中&#xff0c;SaaS 公司和产品的收入也在以前所未…

点成分享 | 带你了解移液器的原理及其分类

移液器&#xff0c;全称叫微量移液器&#xff0c;也叫移液枪、取样枪&#xff0c;是实验室定量移取微量液体体积的精密仪器&#xff0c;一次可量取0.1μL-10mL的液体&#xff0c;可实现精准的液体配比转移&#xff0c;多用于环境检测、医学实验室、生物技术实验室、食品检测实验…

一次明白 JDBC,ORM,JPA,SpringDataJPA 之间的关系

java持久层框架访问数据库一般有两种方式&#xff1a; 以SQL为核心&#xff0c;封装JDBC操作&#xff0c;如&#xff1a;MyBatis以java实体类为核心&#xff0c;将实体类和数据库表之间映射的ORM框架&#xff0c;比如&#xff1a;Spring Data JPA和Hibernate 接下来就是详细的…