JavaSPI的原理和实践
在Java中,我们经常会提到面向接口编程,这样减少了模块之间的耦合,更加灵活。
在一个项目中我们也通常将接口和实现类放在一起,但是如果哪天我们要替换其它的实现类,或者是修改实现类,涉及到实现类的代码也要相应地修改。
能不能这样:在调用服务的时候,我们只调用接口,不用关心实现类呢?无论我们怎么切换实现类,调用接口的部分代码都能正常运行?
当然是可以的,Java SPI ( Service Provider Interface )就提供了这样的机制。
Java SPI机制中,我们 不再是手动指定接口和实现类的关系,而是让接口去寻找可用的实现类 。
事实上,我们经常使用的Spring框架、日志接口等等,都是使用了SPI机制实现了扩展。 1,SPI和API
在说起 SPI 之前,我们还是先看一下API ,API 我们已经很熟悉了,和SPI 都可以被称作接口。
只不过 API 的功能的实现,以及接口的定义全部是接口的实现者提供的,调用者只需要调用接口即可:
不过 SPI 就不一样了,在SPI 机制中,调用者仍然是调用接口,但是这个接口是独立存在的,并且可以由不同的实现者实现:
也就是说,这里接口只是一个标准,并且提供接口的那一方并不一定回去实现接口,而是根据接口的定义,由更多的第三方实现。
这个接口可以 由一个甚至是多个实现者去实现 。也因此, 调用者在调用接口时,可能还需要指定一下使用哪个实现者的实现类 。
实现者也叫做 服务提供者 。
事实上,我们日常生活中经常使用的U盘也很类似SPI 机制,U盘使用的是USB接口,USB接口仅仅是一个规范(接口),但是发明USB接口的公司并没有去生产U盘,而是由不同的U盘厂商例如金士顿、闪迪(实现者)等等去根据这个规范生产U盘,然后我们就可以去选择自己喜欢的牌子(选择实现者)购买U盘,不过平时无论使用什么牌子的U盘,我们只需要插入到电脑的USB接口(调用接口)即可使用,而不用关心不同的厂商是怎么实现USB接口的功能的。
可见,SPI 机制将实现者和接口再次解耦合了,使得接口更加易于扩展。
事实上,我们常常用的SLF4J 就是一个Java的日志接口,但是它也仅仅是一个接口,所以被称作门面。而它的实现有Logback 、Log4j 等等,并且在切换实现的时候,我们只需要修改一下依赖配置即可,代码并不需要任何变动,因为代码中也仅仅是调用了接口。2,自己完成一个SPI
那么现在,我们也来以一个最简单的日志接口为例,实现自己的SPI 。(1) 定义SPI接口
先新建一个空的Maven项目log-interface ,然后在里面创建一个日志接口,声明日志接口具备的方法(功能):package com.gitee.swsk33.loginterface.spi; /** * 定义日志接口 */ public interface Logger { /** * INFO级别日志方法 * * @param message 日志打印消息 */ void info(String message); /** * DEBUG级别日志方法 * * @param message 日志打印消息 */ void debug(String message); } 复制代码
这样,我们便定义了这么一个日志接口,并声明日志接口需要有info 和debug 这两个日志功能。
然后就是编写服务类,这个服务类是这里最为重要的地方,它的作用是扫描所有实现了Logger 接口的实现类并加载进来,然后供调用者去调用。
先看代码:package com.gitee.swsk33.loginterface.service; import com.gitee.swsk33.loginterface.spi.Logger; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; /** * 服务,用于加载所有服务使用者的实现类,以及供外部调用 * 该类为一个单例 */ public class LoggerService { /** * 该类唯一单例 */ private static final LoggerService LOGGER = new LoggerService(); /** * 默认的Logger实现类 */ private final Logger defaultLogger; /** * 所有的Logger实现类列表 */ private final List allLoggers = new ArrayList<>(); /** * 私有化构造器 */ private LoggerService() { // 加载全部Logger接口的实现类 ServiceLoader loader = ServiceLoader.load(Logger.class); // 将实现类放入我们的Logger实现类列表 for (Logger logger : loader) { allLoggers.add(logger); } // 这里取出第一个作为默认实现类 if (!allLoggers.isEmpty()) { defaultLogger = allLoggers.get(0); } else { defaultLogger = null; } System.out.println("加载到" + allLoggers.size() + "个服务实现!"); } /** * 获取该服务类的唯一单例 * * @return 该服务类的唯一单例 */ public static LoggerService getInstance() { return LOGGER; } /** * 调用默认的实现类的info日志打印方法 * * @param message 消息 */ public void info(String message) { if (defaultLogger == null) { System.err.println("没有找到实现了Logger接口的类!"); return; } defaultLogger.info(message); } /** * 调用默认的实现类的debug日志打印方法 * * @param message 消息 */ public void debug(String message) { if (defaultLogger == null) { System.err.println("没有找到实现了Logger接口的类!"); return; } defaultLogger.debug(message); } } 复制代码
首先这个类是一个单例的类,在构造器中,我们使用ServiceLoader 这个类来将实现了Logger 接口的所有类都扫描进来,并存入我们的实现类列表,然后我们取出列表中的第一个作为默认实现。
在下面我们定义了info 和debug 来完成对接口的默认实现类的调用。
最后,在项目目录下执行mvn install 命令将其安装至本地Maven仓库,以便后续服务提供者引入并实现。(2) 完成一个接口的实现
现在再新建一个空的Maven项目logservice-one ,并引入上面接口项目为依赖:
然后编写实现类:package com.gitee.swsk33.logserviceone.service; import com.gitee.swsk33.loginterface.spi.Logger; /** * Logger SPI的实现类 */ public class LogOne implements Logger { @Override public void info(String s) { System.out.println("[LogOne INFO] " + s); } @Override public void debug(String s) { System.out.println("[LogOne DEBUG] " + s); } } 复制代码
然后在resources 目录下创建目录META-INF/services ,这个目录中是用于声明该服务实现中有哪些实现类实现了什么接口。
在这个目录下我们新建一个文件名为com.gitee.swsk33.loginterface.spi.Logger ,文件中的内容为:com.gitee.swsk33.logserviceone.service.LogOne 复制代码
可见,该目录下文件名是要实现的接口的全限定类名(包名 + 类名),而文件中内容是实现了该接口的实现类的全限定类名。
大家参考这里的文件名及其中的内容,与我们上述的接口全限定类名、实现类全限定类名对比一下就知道了!
如果说这个项目中有多个类实现了Logger 接口,那么我们都需要在文件中声明,一行一个实现类的全限定类名。
最终整个项目结构如下:
同样地,最后记得在项目目录下执行mvn install 命令将其安装至本地Maven仓库,以便调用者调用。(3) 测试接口
这里再新建一个Maven空项目log-test ,作为接口的调用者,在依赖中引入实现者:
然后创建一个主类调用一下接口试试:package com.gitee.swsk33.logtest; import com.gitee.swsk33.loginterface.service.LoggerService; public class Main { private static final LoggerService LOGGER = LoggerService.getInstance(); public static void main(String[] args) { LOGGER.info("测试info消息"); LOGGER.debug("测试debug消息"); } } 复制代码
结果:
可见,我们成功地调用了Logger 接口中的方法。
通常调用者的依赖中可能会同时引入 SPI 接口依赖和服务提供者(实现)的依赖,这样也没问题,不过通常服务提供者本身就依赖于SPI 接口,因此只引入服务提供者依赖,也会间接地引入SPI 接口依赖,不影响我们调用SPI 接口。
我们这里只有一个服务提供者logservice-one ,如果说还有logservice-two 等等多个服务提供者,我们只需要在依赖中更换一下即可,代码完全不需要改变。
也可见调用者在调用接口的时候,只需要关注接口就行了,不需要关心实现类。3,再看ServiceLoader
可见在SPI 接口中,我们使用ServiceLoader 完成了对所有实现了Logger 接口的类的扫描和加载,那么具体的过程是什么样的呢?
如果大家去查看这个类的源码,可以发现它实现了Iterable 接口,这也说明我们可以通过迭代的方式去完成多个实现类的切换。
然后在其源码中,有这么一个常量定义:static final String PREFIX = "META-INF/services/"; 复制代码
这就说明,ServiceLoader 会去扫描服务提供者的classpath 路径下的META-INF/services 目录,来扫描哪些类实现了指定接口,而其静态方法load 的参数,正是指定了被实现的接口。也因此我们要在服务提供者的项目的resources 目录下创建这个目录并申明接口和对应实现类的全限定类名。
在Maven项目中, resources 目录就对应的是classpath 的根目录。
简而言之,ServiceLoader 加载实现类的过程如下:先是调用load 方法并指定要扫描的接口然后扫描项目中META-INF/services 目录,这包括调用者项目以及它所引入的所有依赖包中的META-INF/services 目录下的声明扫描到所有实现类后,根据其类名,先判断是否跟SPI 接口为同一类型,如果是则利用反射的方式将所有实现类实例化,加载进内存,并返回所有实现类的实例列表
可见,这就是JDK中SPI 机制加载服务的大致过程,事实上,现在很多框架也利用SPI 机制实现了灵活地扩展。
美国新一代登月火箭再次检测到液氢泄漏美国新一代登月火箭再次检测到液氢泄漏财联社9月22日电,当地时间9月21日,美国国家航空航天局(NASA)表示,他们再次检测到新一代登月火箭太空发射系统发生液氢泄漏,泄漏与此前影响
毅力号在火星上发现大量有机物据科技日报援引英国新科学家杂志网站近日报道,美国国家航空航天局(NASA)的毅力号探测器在火星上发现了大量有机分子被认为是生命组成部分的含碳分子,这些物质的发现或使火星干涸的河流三
重磅更新!PS202223。5。1ACR14。5全新神经滤镜!全新界面领取提示关注私信关键词PS哈喽!小伙伴们!Adobe系列软件是摄影师和设计师的必备软件,它的更新十分快,每一次的升级功能体验均获得再次提升。今天小森跟大家分享的是最新版PS2022
高端智能锁大PK,鹿客德施曼凯迪仕谁能更胜一筹面对市面琳琅满目的智能门锁,消费者应当如何选择?虽然不同类型不同品牌功能各异的智能门锁纷纷面世,但核心还是要看两样指标一是安全性能,门锁能不能将危险因子杜绝门外二是人性化设计,用户
联想YOGAPro14si9版9月26日预售,配32GB大内存IT之家9月21日消息,据联想官方消息,联想YOGAPro14si9版将在9月26日预售,售价暂未公布。据介绍,联想YOGAPro14si9版将搭载i912900H处理器,配备32
盘子女人坊ampampamp从一汉服双品牌明星体验师赵樱子十一年破浪之旅伴随着浪姐3的高热度,出道11年代表作众多,却总是让人隐隐约约有听说过的赵樱子再一次强势进入了大众的视野。提到赵樱子这个名字,大家或许没有很深的印象,但是提到她说塑造过的因为爱情有
那些你不能错过的小众情侣鞋头条创作挑战赛adidas有哪些好看的鞋帅鞋哪些运动鞋最适合作为情侣鞋好看的鞋子千千万,好看的情侣鞋没几款相信这是大多数人的感受。运动鞋中,男款和女款都有不错的鞋,然而当你想搭配成
科学整理手表戴左手还是右手?俗话说得好,萝卜酸菜个人所爱。每个人都有自己的个人习惯,个体之间都存在着差异,有人习惯使用右手,也有人习惯使用左手。所以手表戴左手还是右手?是根据个人生活习惯怎么方便怎么戴,没有男
00后美妆博主零食清单公开,好吃又养人,网友难怪底子好随着时代进步,越来越多的年轻人开始接触自媒体行业,21岁的婷婷就是其中一个。虽然年龄小,但精致生活的她有着美妆方面的天赋,很早就开始经营短视频账号,经常会录制一些自己的美妆小技巧上
减龄不扮嫩435岁选择这些图案,让你青春又有气质点击上方蓝字关注我相对于纯色的服装,图案使得整体增加了变化和动感,给人带来一种活力青春感。当然,我们要始终牢记美的原则,即信息的和谐平衡。图案有大小,形状,抽象具象等不同属性,携带
做自己的医生生活随笔不带怒气出门,不带怨气处事,不带烦恼睡觉,不带急性情绪办事,清醒的时候做事,心烦时静心,糊涂时沉淀,大怒时控制,调整好情绪,做自己的心理医生。我们不得不承认,自己的病外面的
离婚后,从孩子成长角度来说,应该多来看孩子,还是少来看孩子?我觉得离婚了,为了孩子健康成长,应该多去看望孩子。我离婚后,孩子归我抚养照顾,前夫付抚养费。我跟前夫是和平离婚的,我们谁也不怨恨对方,也不会在孩子面前说对方的坏话,总是叫孩子要学习
孩子上一年级,汉字写得一塌糊涂,该怎么办?一年级的孩子,才刚刚练习写字,字写的不端正一般都是姿势不端正导致的。1。写字姿势不端正。有的孩子写字的时候头低得很低,有的身体歪着,还有头枕在桌子上写字身不正,肩不平,写出来的字自
航天发射基地为什么会选择海南文昌?文昌,历史上的名字叫紫贝,西汉时期就有了这个建置,历史悠久,是海南岛的古邑之一,选择在海南文昌,是因为这里远离人口密集的大城市,避免干扰经济建设。文昌卫星发射中心,处于文昌的龙楼镇
AI人工智能将来的市场大吗?人工智能是算是这两年互联网的一大热门关键词了,中国乃至全球各大互联网企业都在布局AI人工智能,从埃隆马斯克的特斯拉自动驾驶汽车谷歌的阿尔法狗IBM的沃森人工智能以及到国内阿里巴巴的
如何看待刚认识就要微信的现象?这最多是对微信走火入魔而已,至于能否遇上知音,从此网上诉说衷情,还是认识了骗子,不幸落入圈套,咎由自取,休怪他人。若是微信交友别有用心,也可能利用微信招遥撞骗,拉人网购三无产品。这
湖南下一所双一流大学会是谁呢?谁会是湖南下一所双一流高校呢?下面介绍的这些资料,可能会给你答案马论学科可能A(上轮评估B,2019年JY部牵头,马论学科评估得分最高的北大清华人大东北师大武大五校联合援建湘大马克
湖南师范大学为什么不与湖南大学合并?这个问题比较复杂,三言两语说不清,各种版本都有,我也是道听途说,当不得真。湖大和湖师大历史上颇有渊源,当年湖南师范学院就座落在岳麓山下,其校区包括了大名鼎鼎的岳麓书院,后来湖南省要
吃什么水果降血脂最快?苹果富含纤维物质,纤维可减少胆固醇在体内生成,从而更加有利于冠心病的防治。山楂能扩张血管,降高血压,降低胆固醇,有良好的降压健胃消积的作用。蓝莓有超级水果的美誉,含有可帮助血管扩张
未来5年,最值得轻资产创业方向在哪里?轻资产是所有创业和投资者,未来必须重点选择的一个核心!那么,什么样的项目具有轻资产的特征?又是未来发展的潜力领域呢?我们先说一个现象和事实好吧!当经济好的时候,我们要努力做高端群体
百年纪念币市场情况跌破12大关,会出现反弹吗?关注老孟收藏不迷路,不定期更新钱币收藏价格表!前言JD纪念币到今天算是第一批兑换基本差不多了,虽然有地方延迟了,但是按照目前的这个行情来看,就算是那些纪念币在换出来,可能上涨的幅度
目前上海的月工资多少才算正常?月工资多少才能够自己生活?这个要看你是什么职业了,工资多少才算正常,这话没办法回答,月工资多少才够自己生活,这个我觉得我可以说说,咱们可以先算一下。第一,如果你是一个单生狗,还是一个宅男的话。那么真的不需要