从实现到原理,聊聊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包: com.cn.hydra aircondition-standard 1.0-SNAPSHOT
创建服务类,并实现前面定义的接口: public class HangingTypeAircondition implements 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 VerticalTypeAircondition implements 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包。 com.cn.hydra aircondition-hanging-type 1.0-SNAPSHOT com.cn.hydra aircondition-vertical-type 1.0-SNAPSHOT
按照上面的说法,虽然每个服务提供者对于接口都有不同的实现,但是作为调用者来说,它并不需要关心具体的实现类,我们要做的是通过接口来调用服务提供者实现的方法。
下面,就是关键的服务发现环节,我们写一个方法,根据型号去调用对应空调的开关方法。 public class AirconditionApp { public static void main(String[] args) { new AirconditionApp().turnOn("VerticalType"); } public void turnOn(String type){ ServiceLoader 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 ,就可以使用具体功能。 org.slf4j slf4j-api 2.0.3 org.slf4j slf4j-log4j12 2.0.3
引入项目后,点开它的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提供了一种比较特别的服务发现和调用机制,通过接口灵活的将服务调用与服务提供者分离,用于提供给第三方实现扩展时还是很方便的。但是也有缺点,比方说一旦加载一个接口,就会把所有实现类都加载进来,可能会加载到不需要的冗余服务。不过站在整体角度上,还是给我们提供了一种非常不错的框架扩展、集成的思路。
初夏五月大荔行文王宗合初夏五月,风吹麦浪起。2022年5月24日,来自陕西省各市(区)县,巴斯夫施乐健忠实客户种植业专业合作社种粮大户代表及应邀嘉宾陕西省植保站副站长正高级农艺师苏小记省植保站药
我们的脊梁断了吗?不知是跪得久了习惯了,感觉站着不舒服,还是什么其他原因,当一个事情都还没有开始,别人口嗨了一句,我们的好多人居然就腿都软了!就习惯性地想跪下,这让人太过于不耻了!当然,也许是他们有
红极一时的组合离婚,互泼脏水,利益不均,宁愿他们没遇见单飞不可怕,谁糊谁尴尬这句话似乎成为娱乐圈组合解散后的定律。今天小编就来盘点一些那些红极一时却走向解散的组合。1下一个凤凰传奇玖月奇迹娱乐圈就是这样,总喜欢给各种明星安头衔。但是能
美校园枪击案嫌犯曾晒枪18岁生日第一件事就是买枪据美国有线电视新闻网(CNN)报道,美国得克萨斯州一所小学24日发生枪击案,已致21人死亡,包括19名学生。嫌犯作案三天前,曾在网上晒枪。美国得州小学枪击案嫌犯照片嫌犯是一名18岁
荷媒拜仁1900万欧550万奖金报价格拉文贝赫,附二次转会分成直播吧5月25日讯荷兰媒体荷兰电讯报的消息,拜仁方面对阿贾克斯球员格拉文贝赫再一次提出报价。据悉拜仁愿意支付1900万欧元550万欧的比较容易达成的奖金,以签下球员。同时,若球员之
毛主席说此战若败,先斩许世友,打王建安40军棍,我也官降3级解放战争时期,我军攻克的第一个大城市是河北的石家庄。1947年11月6日至12日,历经6天6夜的石家庄战役胜利结束,开创了人民解放军夺取敌占大城市的先例。但从防守力量上看,石家庄当
播放量破亿最快日漫!间谍过家家四集破亿,其实并不意外日漫霸权新番间谍过家家只靠着四集就播放量破亿,其实严格来说它是三集破亿,因为第四集的播放量是锦上添花,它只靠前三集播放量就破亿了。间谍过家家也是播放量破亿最快的日漫,耗时22天,破
轮到你敬酒,傻坐着不知道说啥?教你3句万能的话,让人刮目相看感情深一口闷!敬酒是体现礼貌,而拥有一口流利的话是礼貌的升华,这也就是说为什么口才好的人,能够在很多场合上吃得开的原因。在职场或者和客户谈判的时候最能体现出来,无论是应酬还是,公司
电车和油车成本对比即便不算残值,BEV也输九条街每一次油价调整,都有一大堆车友排队加油,可见大家对于油价的变化还是相当敏感的。因为油价太高而选择电动车?现在的油价太贵了,属实加不起了。看着不停上涨的油价,我家隔壁的小陈按捺不住了
有个如狼似虎的女友是什么体验不瞒你说,最近双腿都有点发软累的女生能色到什么程度?又让社死上升了一个新高度不愧是阎王贷杀人诛心哥们很久没见女朋友了,憋不住了,就晚上去学校找她(不在同一个学校),饥渴难耐,就找个小树林战斗,两人激战正酣,被学校
匿名出轨短信叮咚叮咚咚咚咚!咚咚咚咚咚!客房的门铃声变成了急促的敲门声,小曹放下手头的动作,看着床上的人叹了口气,拢了拢自己的头发,想要扎起来,却发现手腕上没有头绳,只能作罢。客房面积不大,她