当前位置: 首页 > 后端技术 > Java

JavaSPI机制从原理到实战

时间:2023-04-02 01:31:09 Java

1.什么是SPI1。背景在面向对象的设计原则中,一般推荐基于接口的模块间编程。通常,调用模块不会感知被调用模块内部的实现。一旦代码中涉及到具体的实现类,就违反了开闭原则。如果需要更换一个实现,就需要修改代码。为了实现模块组装时无需在程序中动态指定,需要服务发现机制。JavaSPI提供了这样一种机制:一种为接口寻找服务实现的机制。这有点类似于IOC的思想,将汇编的控制权转移到程序外部。SPI英文是ServiceProviderInterface,字面意思是“服务提供者接口”。我的理解是:专门为扩展框架功能的服务商或开发者提供的接口。SPI将服务接口与具体的服务实现分离,将服务调用者与服务实现者解耦,可以提高程序的扩展性和可维护性。修改或替换服务实现不需要修改调用者。2、使用场景很多框架都使用了Java的SPI机制,比如:数据库加载驱动、日志接口、dubbo的扩展实现等。3、SPI和API有什么区别?说到SPI,就不得不说到API。从广义上讲,它们都属于接口,容易混淆。我们用一张图来说明:一般模块之间通过接口进行通信,所以我们在服务调用者和服务实现者(也叫服务提供者)之间引入一个“接口”。当实现者提供接口和实现时,我们可以通过调用实现者的接口来拥有实现者提供的能力。这就是API,这个接口和实现是放在实现者身上的。当调用方存在接口时,就是SPI。接口调用者确定接口规则,然后不同厂商实现接口提供服务。举个通俗易懂的例子:H公司是一家科技公司,新设计了一款芯片,现在需要量产。市场上有几家芯片制造公司。),那么这些合作的芯片公司(服务商)就会按照标准交付自己的特色芯片(提供不同方案的实现,但结果是一样的)。2、实际演示Spring框架提供的日志服务SLF4J其实只是一个日志门面(接口),但是SLF4J有几种具体的实现,如:Logback、Log4j、Log4j2等,并且还可以切换,在具体实现切换日志的时候我们不需要改动项目代码,只需要修改Maven依赖中的一些pom依赖即可。这是依靠SPI机制实现的,所以接下来我们来实现一个简单版的日志框架。1.ServiceProviderInterface新建Java项目service-provider-interface目录结构如下:├─.idea└─src├─META-INF└─org└─spi└─service├─Logger.java├─LoggerService。java├─Main.java└─MyServicesLoader.java新建Logger接口,即SPI,服务提供者接口,后续服务提供者必须实现该接口。包org.spi.service;公共接口记录器{voidinfo(Stringmsg);voiddebug(Stringmsg);}接下来是LoggerService类,主要为服务使用者(调用者)提供具体的功能。有疑惑的可以先继续看后面。包org.spi.service;导入java.util.ArrayList;导入java.util.List;导入java.util.ServiceLoader;公共类LoggerService{privatestaticfinalLoggerServiceSERVICE=newLoggerService();私人最终记录器记录器;私人最终列表loggerList;privateLoggerService(){ServiceLoaderloader=ServiceLoader.load(Logger.class);Listlist=newArrayList<>();for(Loggerlog:loader){list.add(log);}//LoggerList是所有ServiceProviderloggerList=list;if(!list.isEmpty()){//Logger只取一个logger=list.get(0);}else{记录器=null;}}publicstaticLoggerServicegetService(){返回服务;}publicvoidinfo(Stringmsg){if(logger==null){System.out.println("info中没有发现Logger服务提供者");}else{logger.info(味精);}}publicvoiddebug(Stringmsg){if(loggerList.isEmpty()){System.out.println("在调试中找不到Logger服务提供者");}loggerList.forEach(日志->log.debug(msg));}}新建Main类(服务用户,调用者),启动程序查看结果包org.spi.service;publicclassMain{publicstaticvoidmain(String[]args){LoggerServiceservice=LoggerService.getService();service.info("你好SPI");service.debug("你好SPI");}}程序结果:info中找不到Logger服务提供者,debug中找不到Logger服务提供者,整个程序直接打包成jar包,项目直接通过IDEA打包成jar包即可。2.ServiceProvider接下来新建工程实现Logger接口。新项目service-provider目录结构如下:├─.idea├─lib│└─service-provider-interface.jar└─src├─META-INF│└─services│└─org.spi.service。Logger└─org└─spi└─provider└─Logback.java新的Logback类包org.spi.provider;importorg.spi.service.Logger;publicclassLogbackimplementsLogger{@Overridepublicvoidinfo(Stringmsg){System.out.println("Logback信息的输出:"+msg);}@Overridepublicvoiddebug(Stringmsg){System.out.println("Logbackdebug的输出:"+msg);}}将service-provider-interface的jar导入到项目中。新建一个lib目录,然后复制jar包加入到项目中。再次单击“确定”。接下来可以在项目中导入jar包中的一些类和方法,就像JDK工具类导入包一样。实现Logger接口,在src目录下新建META-INF/services文件夹,然后新建文件org.spi.service.Logger(SPI的全类名),文件内容为:org.spi.provider.Logback(LogbackSPI的全类名,即SPI实现类的包名+类名)。这是JDKSPI机制ServiceLoader约定的标准。接下来将service-provider项目也打包成jar包。这个jar包就是服务提供者的实现。通常我们导入到maven中的pom依赖和这个有些类似,但是我们并没有将这个jar包发布到maven公共仓库中,所以只能手动添加到需要使用的项目中。3.效果展示接下来,回到service-provider-interface项目。导入service-providerjar包,重新运行Main方法。运行结果如下:logbackinfooutput:HelloSPIlogbackdebugoutput:HelloSPI说明jar包中导入的实现类已经生效。通过使用SPI机制,可以看出服务(LoggerService)与服务提供者之间的耦合度很低。如果需要更换一个实现(将Logback换成另一个实现),只需要换一个jar包即可。能。这不就是SLF4J的原理吗?如果哪天需求有变化,需要把日志输出到消息队列或者做一些其他的操作。这个时候你根本不需要去改动Logback的实现。你只需要通过这个在项目中添加一个新的服务实现(service-provider)Newimplementations也可以从外部引入新的服务实现jar包。我们可以在服务(LoggerService)中选择一个具体的服务实现(service-provider)来完成我们需要的操作。loggerList.forEach(日志->log.debug(味精));或loggerList.get(1).debug(msg);loggerList.get(2).debug(味精);这里需要明白一件事:ServiceLoader在加载特定服务时,会扫描包下所有src目录下META-INF/services的内容,然后通过反射生成对应的对象保存在一个list列表,这样你就可以通过迭代或者遍历得到你需要的那个服务实现了。3、如果ServiceLoader要使用Java的SPI机制,需要依赖ServiceLoader来实现。那么我们来看看ServiceLoader是如何实现的:ServiceLoader是JDK提供的一个工具类,位于包java.util下;包裹。加载服务实现的工具。这是JDK官方给出的注释:一个加载服务实现的工具。再往下看,我们发现这个类是一个final类型,所以不能被继承修改,它实现了Iterable接口。之所以实现迭代器,是为了方便后续通过迭代实现相应的服务。publicfinalclassServiceLoaderimplementsIterable{xxx...}可以看到一个熟悉的常量定义:privatestaticfinalStringPREFIX="META-INF/services/";下面是load方法:可以发现load方法支持两个重载入参;publicstaticServiceLoaderload(Classservice){ClassLoadercl=Thread.currentThread().getContextClassLoader();returnServiceLoader.load(service,cl);}publicstaticServiceLoaderload(Classservice,ClassLoaderloader){returnnewServiceLoader<>(service,loader);}privateServiceLoader(Classsvc,ClassLoadercl){service=Objects.requireNonNull(svc,"服务接口不能为空");装载机=(cl==null)?ClassLoader.getSystemClassLoader():cl;acc=(System.getSecurityManager()!=null)?AccessController.getContext():空;reload();}publicvoidreload(){providers.clear();lookupIterator=newLazyIterator(service,loader);}根据代码的调用顺序,在reload()方法中,是通过一个内部类LazyIterator来实现的。继续往下看。ServiceLoader实现了Iterable接口的方法后,就具备了迭代的能力。当iterator方法被调用时,会先在ServiceLoader的Provider缓存中查找。如果缓存中没有命中,它将在LazyIterator中搜索。publicIteratoriterator(){returnnewIterator(){Iterator>knownProviders=providers.entrySet().iterator();publicbooleanhasNext(){if(knownProviders.hasNext())返回真;返回lookupIterator.hasNext();//调用LazyIterator}publicSnext(){if(knownProviders.hasNext())returnknownProviders.next().getValue();返回lookupIterator.next();//调用LazyIterator}publicvoidremove(){thrownewUnsupportedOperationException();}};}在调用LazyIterator时,具体实现如下:publicbooleanhasNext(){if(acc==null){returnhasNextService();}else{PrivilegedActionaction=newPrivilegedAction(){publicBooleanrun(){returnhasNextService();}};返回AccessController.doPrivileged(action,acc);}}privatebooleanhasNextService(){if(nextName!=null){returntrue;}if(configs==null){try{//通过PREFIX(META-INF/services/)和类名得到对应的配置文件,得到具体的实现类StringfullName=PREFIX+service.getName();if(loader==null)configs=ClassLoader.getSystemResources(fullName);否则configs=loader.getResources(fullName);}catch(IOExceptionx){fail(service,"错误定位配置文件",x);}}while((pending==null)||!pending.hasNext()){if(!configs.hasMoreElements()){returnfalse;}pending=parse(service,configs.nextElement());}nextName=pending.next();returntrue;}publicSnext(){if(acc==null){returnnextService();}else{PrivilegedActionaction=newPrivilegedAction(){publicSrun(){returnnextService();}};返回AccessController.doPrivileged(action,acc);}}privateSnextService(){if(!hasNextService())thrownewNoSuchElementException();Stringcn=nextName;下一个名字=空;类c=null;try{c=Class.forName(cn,false,loader);}catch(ClassNotFoundExceptionx){fail(service,"Provider"+cn+"notfound");}if(!service.isAssignableFrom(c)){fail(service,"Provider"+cn+"不是子类型");}try{Sp=service.cast(c.newInstance());providers.put(cn,p);返回p;}catch(Throwablex){fail(service,"Provider"+cn+"无法实例化",x);抛出新的错误();//这不可能发生}4.总结其实不难发现,SPI机制的具体实现本质上是通过反射完成的即:我们会按照规定在META-INF/services/文件下声明要对外暴露的具体实现类。事实上,SPI机制在很多框架中都有应用:Spring框架的基本原理类似于反射。还有一个dubbo框架,提供了同样的SPI扩展机制。通过SPI机制可以大大提高接口设计的灵活性,但是SPI机制也有一些缺点,比如:遍历加载所有的实现类,所以效率比较低;当同时加载多个ServiceLoader时,会出现并发问题。