什么是SPI1。背景 在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。 为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。JavaSPI就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移交到了程序之外。 SPI英文为ServiceProviderInterface字面意思就是:服务提供者的接口,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 SPI将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。2。使用场景 很多框架都使用了Java的SPI机制,比如:数据库加载驱动,日志接口,以及dubbo的扩展实现等等。3。SPI和API有啥区别 说到SPI就不得不说一下API了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下: 一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个接口。 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是API,这种接口和实现都是放在实现方的。 当接口存在于调用方这边时,就是SPI,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务,举个通俗易懂的例子:公司H是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要H公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。 实战演示 Spring框架提供的日志服务SLF4J其实只是一个日志门面(接口),但是SLF4J的具体实现可以有几种,比如:Logback、Log4j、Log4j2等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在Maven依赖里面修改一些pom依赖就好了。 这就是依赖SPI机制实现的,那我们接下来就实现一个简易版本的日志框架。1。ServiceProviderInterface 新建一个Java项目serviceproviderinterface目录结构如下:。ideasrcMETAINForgspiserviceLogger。javaLoggerService。javaMain。javaMyServicesLoader。java 新建Logger接口,这个就是SPI,服务提供者接口,后面的服务提供者就要针对这个接口进行实现。packageorg。spi。service;publicinterfaceLogger{voidinfo(Stringmsg);voiddebug(Stringmsg);} 接下来就是LoggerService类,这个主要是为服务使用者(调用方)提供特定功能的。如果存在疑惑的话可以先往后面继续看。packageorg。spi。service;importjava。util。ArrayList;importjava。util。List;importjava。util。ServiceLoader;publicclassLoggerService{privatestaticfinalLoggerServiceSERVICEnewLoggerService();privatefinalLoggerlogger;privatefinalListLoggerloggerList;privateLoggerService(){ServiceLoaderLoggerloaderServiceLoader。load(Logger。class);ListLoggerlistnewArrayList();for(Loggerlog:loader){list。add(log);}LoggerList是所有ServiceProviderloggerListlist;if(!list。isEmpty()){Logger只取一个loggerlist。get(0);}else{loggernull;}}publicstaticLoggerServicegetService(){returnSERVICE;}publicvoidinfo(Stringmsg){if(loggernull){System。out。println(info中没有发现Logger服务提供者);}else{logger。info(msg);}}publicvoiddebug(Stringmsg){if(loggerList。isEmpty()){System。out。println(debug中没有发现Logger服务提供者);}loggerList。forEach(loglog。debug(msg));}} 新建Main类(服务使用者,调用方),启动程序查看结果。packageorg。spi。service;publicclassMain{publicstaticvoidmain(String〔〕args){LoggerServiceserviceLoggerService。getService();service。info(HelloSPI);service。debug(HelloSPI);}} 程序结果: info中没有发现Logger服务提供者 debug中没有发现Logger服务提供者 将整个程序直接打包成jar包,可以直接通过IDEA将项目打包成一个jar包。 2。ServiceProvider 接下来新建一个项目用来实现Logger接口 新建项目serviceprovider目录结构如下:。idealibserviceproviderinterface。jarsrcMETAINFservicesorg。spi。service。LoggerorgspiproviderLogback。java 新建Logback类packageorg。spi。provider;importorg。spi。service。Logger;publicclassLogbackimplementsLogger{Overridepublicvoidinfo(Stringmsg){System。out。println(Logbackinfo的输出:msg);}Overridepublicvoiddebug(Stringmsg){System。out。println(Logbackdebug的输出:msg);}} 将serviceproviderinterface的jar导入项目中。新建lib目录,然后将jar包拷贝过来,再添加到项目中。 再点击OK。 接下来就可以在项目中导入jar包里面的一些类和方法了,就像JDK工具类导包一样的。 实现Logger接口,在src目录下新建METAINFservices文件夹,然后新建文件org。spi。service。Logger(SPI的全类名),文件里面的内容是:org。spi。provider。Logback(Logback的全类名,即SPI的实现类的包名类名)。 这是JDKSPI机制ServiceLoader约定好的标准 接下来同样将serviceprovider项目打包成jar包,这个jar包就是服务提供方的实现。通常我们导入maven的pom依赖就有点类似这种,只不过我们现在没有将这个jar包发布到maven公共仓库中,所以在需要使用的地方只能手动的添加到项目中。3。效果展示 接下来再回到serviceproviderinterface项目。 导入serviceproviderjar包,重新运行Main方法。运行结果如下: Logbackinfo的输出:HelloSPI Logbackdebug的输出:HelloSPI 说明导入jar包中的实现类生效了。 通过使用SPI机制,可以看出服务(LoggerService)和服务提供者两者之间的耦合度非常低,如果需要替换一种实现(将Logback换成另外一种实现),只需要换一个jar包即可。这不就是SLF4J原理吗? 如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改Logback的实现,只需要新增一个服务实现(serviceprovider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现jar包。我们可以在服务(LoggerService)中选择一个具体的服务实现(serviceprovider)来完成我们需要的操作。 loggerList。forEach(loglog。debug(msg)); 或者 loggerList。get(1)。debug(msg); loggerList。get(2)。debug(msg); 这里需要先理解一点:ServiceLoader在加载具体的服务实现的时候会去扫描所有包下src目录的METAINFservices的内容,然后通过反射去生成对应的对象,保存在一个list列表里面,所以可以通过迭代或者遍历的方式得到你需要的那个服务实现。 3。ServiceLoader 想要使用Java的SPI机制是需要依赖ServiceLoader来实现的,那么我们接下来看看ServiceLoader具体是怎么做的: ServiceLoader是JDK提供的一个工具类,位于packagejava。util;包下。Afacilitytoloadimplementationsofaservice。 这是JDK官方给的注释:一种加载服务实现的工具。 再往下看,我们发现这个类是一个final类型的,所以是不可被继承修改,同时它实现了Iterable接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。publicfinalclassServiceLoaderSimplementsIterableS{xxx。。。} 可以看到一个熟悉的常量定义: privatestaticfinalStringPREFIXMETAINFservices; 下面是load方法:可以发现load方法支持两种重载后的入参;publicstaticSServiceLoaderSload(ClassSservice){ClassLoaderclThread。currentThread()。getContextClassLoader();returnServiceLoader。load(service,cl);}publicstaticSServiceLoaderSload(ClassSservice,ClassLoaderloader){returnnewServiceLoader(service,loader);}privateServiceLoader(ClassSsvc,ClassLoadercl){serviceObjects。requireNonNull(svc,Serviceinterfacecannotbenull);loader(clnull)?ClassLoader。getSystemClassLoader():cl;acc(System。getSecurityManager()!null)?AccessController。getContext():null;reload();}publicvoidreload(){providers。clear();lookupIteratornewLazyIterator(service,loader);} 根据代码的调用顺序,在reload()方法中是通过一个内部类LazyIterator实现的。先继续往下面看。 ServiceLoader实现了Iterable接口的方法后,具有了迭代的能力,在这个iterator方法被调用时,首先会在ServiceLoader的Provider缓存中进行查找,如果缓存中没有命中那么则在LazyIterator中进行查找。publicIteratorSiterator(){returnnewIteratorS(){IteratorMap。EntryString,SknownProvidersproviders。entrySet()。iterator();publicbooleanhasNext(){if(knownProviders。hasNext())returntrue;returnlookupIterator。hasNext();调用LazyIterator}publicSnext(){if(knownProviders。hasNext())returnknownProviders。next()。getValue();returnlookupIterator。next();调用LazyIterator}publicvoidremove(){thrownewUnsupportedOperationException();}};} 在调用LazyIterator时,具体实现如下:publicbooleanhasNext(){if(accnull){returnhasNextService();}else{PrivilegedActionBooleanactionnewPrivilegedActionBoolean(){publicBooleanrun(){returnhasNextService();}};returnAccessController。doPrivileged(action,acc);}}privatebooleanhasNextService(){if(nextName!null){returntrue;}if(configsnull){try{通过PREFIX(METAINFservices)和类名获取对应的配置文件,得到具体的实现类StringfullNamePREFIXservice。getName();if(loadernull)configsClassLoader。getSystemResources(fullName);elseconfigsloader。getResources(fullName);}catch(IOExceptionx){fail(service,Errorlocatingconfigurationfiles,x);}}while((pendingnull)!pending。hasNext()){if(!configs。hasMoreElements()){returnfalse;}pendingparse(service,configs。nextElement());}nextNamepending。next();returntrue;}publicSnext(){if(accnull){returnnextService();}else{PrivilegedActionSactionnewPrivilegedActionS(){publicSrun(){returnnextService();}};returnAccessController。doPrivileged(action,acc);}}privateSnextService(){if(!hasNextService())thrownewNoSuchElementException();StringcnnextName;nextNamenull;Classlt;?cnull;try{cClass。forName(cn,false,loader);}catch(ClassNotFoundExceptionx){fail(service,Providercnnotfound);}if(!service。isAssignableFrom(c)){fail(service,Providercnnotasubtype);}try{Spservice。cast(c。newInstance());providers。put(cn,p);returnp;}catch(Throwablex){fail(service,Providercncouldnotbeinstantiated,x);}thrownewError();Thiscannothappen} 4。总结 其实不难发现,SPI机制的具体实现本质上还是通过反射完成的。即:我们按照规定将要暴露对外使用的具体实现类在METAINFservices文件下声明。 其实SPI机制在很多框架中都有应用:Spring框架的基本原理也是类似的反射。还有dubbo框架提供同样的SPI扩展机制。 通过SPI机制能够大大地提高接口设计的灵活性,但是SPI机制也存在一些缺点,比如:遍历加载所有的实现类,这样效率还是相对较低的;当多个ServiceLoader同时load时,会有并发问题。 写在最后 FreemenApp是一款专注于IT程序员求职招聘的一个求职平台,旨在帮助IT技术工作者能更好更快入职及努力协调IT技术者工作和生活的关系,让工作更自由! 本文转载自江璇Up