您好,欢迎访问一九零五行业门户网

Java Spring Dubbo三种SPI机制的区别是什么

spi 有什么用?举个栗子,现在我们设计了一款全新的日志框架:「super-logger」。默认以xml文件作为我们这款日志的配置文件,并设计了一个配置文件解析的接口:
package com.github.kongwu.spisamples;public interface superloggerconfiguration {void configure(string configfile);}
然后来一个默认的xml实现:
package com.github.kongwu.spisamples;public class xmlconfiguration implements superloggerconfiguration{public void configure(string configfile){......}}
那么我们在初始化,解析配置时,只需要调用这个xmlconfiguration来解析xml配置文件即可
package com.github.kongwu.spisamples;public class loggerfactory {static {superloggerconfiguration configuration = new xmlconfiguration();configuration.configure(configfile);}public static getlogger(class clazz){......}}
这样就完成了一个基础的模型,看起来也没什么问题。不过扩展性不太好,因为如果想定制/扩展/重写解析功能的话,我还得重新定义入口的代码,loggerfactory 也得重写,不够灵活,侵入性太强了。
比如现在用户/使用方想增加一个 yml 文件的方式,作为日志配置文件,那么只需要新建一个yamlconfiguration,实现 superloggerconfiguration 就可以。但是……怎么注入呢,怎么让 loggerfactory中使用新建的这个 yamlconfiguration ?难不成连 loggerfactory 也重写了?
如果借助spi机制的话,这个事情就很简单了,可以很方便的完成这个入口的扩展功能。
下面就先来看看,利用jdk 的 spi 机制怎么解决上面的扩展性问题。
jdk spijdk 中 提供了一个 spi 的功能,核心类是 java.util.serviceloader。其作用就是,可以通过类名获取在"meta-inf/services/"下的多个配置实现文件。
为了解决上面的扩展问题,现在我们在meta-inf/services/下创建一个com.github.kongwu.spisamples.superloggerconfiguration文件(没有后缀)。文件中只有一行代码,那就是我们默认的com.github.kongwu.spisamples.xmlconfiguration(注意,一个文件里也可以写多个实现,回车分隔)
meta-inf/services/com.github.kongwu.spisamples.superloggerconfiguration:com.github.kongwu.spisamples.xmlconfiguration
然后通过 serviceloader 获取我们的 spi 机制配置的实现类:
serviceloader<superloggerconfiguration> serviceloader = serviceloader.load(superloggerconfiguration.class);iterator<superloggerconfiguration> iterator = serviceloader.iterator();superloggerconfiguration configuration;while(iterator.hasnext()) {//加载并初始化实现类configuration = iterator.next();}//对最后一个configuration类调用configure方法configuration.configure(configfile);
最后在调整loggerfactory中初始化配置的方式为现在的spi方式:
package com.github.kongwu.spisamples;public class loggerfactory {static {serviceloader<superloggerconfiguration> serviceloader = serviceloader.load(superloggerconfiguration.class);iterator<superloggerconfiguration> iterator = serviceloader.iterator();superloggerconfiguration configuration;while(iterator.hasnext()) {configuration = iterator.next();//加载并初始化实现类}configuration.configure(configfile);}public static getlogger(class clazz){......}}
「等等,这里为什么是用 iterator ? 而不是get之类的只获取一个实例的方法?」
试想一下,如果是一个固定的get方法,那么get到的是一个固定的实例,spi 还有什么意义呢?
spi 的目的,就是增强扩展性。将固定的配置提取出来,通过 spi 机制来配置。那既然如此,一般都会有一个默认的配置,然后通过 spi 的文件配置不同的实现,这样就会存在一个接口多个实现的问题。要是找到多个实现的话,用哪个实现作为最后的实例呢?
所以这里使用iterator来获取所有的实现类配置。刚才已经在我们这个 「super-logger」 包里增加了默认的superloggerconfiguration 实现。
为了支持 yaml 配置,现在在使用方/用户的代码里,增加一个yamlconfiguration的 spi 配置:
meta-inf/services/com.github.kongwu.spisamples.superloggerconfiguration:com.github.kongwu.spisamples.ext.yamlconfiguration
此时通过iterator方法,就会获取到默认的xmlconfiguration和我们扩展的这个yamlconfiguration两个配置实现类了。
在上面那段加载的代码里,我们遍历iterator,遍历到最后,我们**使用最后一个实现配置作为最终的实例。
「再等等?最后一个?怎么算最后一个?」
使用方/用户自定义的的这个 yamlconfiguration 一定是最后一个吗?
这个真的不一定,取决于我们运行时的 classpath 配置,在前面加载的jar自然在前,最后的jar里的自然当然也在后面。所以「如果用户的包在classpath中的顺序比super-logger的包更靠后,才会处于最后一个位置;如果用户的包位置在前,那么所谓的最后一个仍然是默认的xmlconfiguration。」
举个栗子,如果我们程序的启动脚本为:
java -cp super-logger.jar:a.jar:b.jar:main.jar example.main
默认的xmlconfiguration spi配置在super-logger.jar,扩展的yamlconfiguration spi配置文件在main.jar,那么iterator获取的最后一个元素一定为yamlconfiguration。
但这个classpath顺序如果反了呢?main.jar 在前,super-logger.jar 在后
java -cp main.jar:super-logger.jar:a.jar:b.jar example.main
这样一来,iterator 获取的最后一个元素又变成了默认的xmlconfiguration,我们使用 jdk spi 没啥意义了,获取的又是第一个,还是默认的xmlconfiguration。
由于这个加载顺序(classpath)是由用户指定的,所以无论我们加载第一个还是最后一个,都有可能会导致加载不到用户自定义的那个配置。
「所以这也是jdk spi机制的一个劣势,无法确认具体加载哪一个实现,也无法加载某个指定的实现,仅靠classpath的顺序是一个非常不严谨的方式」
dubbo spidubbo 就是通过 spi 机制加载所有的组件。不过,dubbo 并未使用 java 原生的 spi 机制,而是对其进行了增强,使其能够更好的满足需求。在 dubbo 中,spi 是一个非常重要的模块。基于 spi,我们可以很容易的对 dubbo 进行拓展。如果大家想要学习 dubbo 的源码,spi 机制务必弄懂。接下来,我们先来了解一下 java spi 与 dubbo spi 的用法,然后再来分析 dubbo spi 的源码。
dubbo 中实现了一套新的 spi 机制,功能更强大,也更复杂一些。相关逻辑被封装在了 extensionloader 类中,通过 extensionloader,我们可以加载指定的实现类。dubbo spi 所需的配置文件需放置在 meta-inf/dubbo 路径下,配置内容如下(以下demo来自dubbo官方文档)。
optimusprime = org.apache.spi.optimusprimebumblebee = org.apache.spi.bumblebee
与 java spi 实现类配置不同,dubbo spi 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @spi 注解。
下面来演示 dubbo spi 的用法:
@spipublic interface robot {void sayhello();}public class optimusprime implements robot {@overridepublic void sayhello(){system.out.println("hello, i am optimus prime.");}}public class bumblebee implements robot {@overridepublic void sayhello(){system.out.println("hello, i am bumblebee.");}}public class dubbospitest {@testpublic void sayhello() throws exception {extensionloader<robot> extensionloader =extensionloader.getextensionloader(robot.class);robot optimusprime = extensionloader.getextension("optimusprime");optimusprime.sayhello();robot bumblebee = extensionloader.getextension("bumblebee");bumblebee.sayhello();}}
「dubbo spi 和 jdk spi 最大的区别就在于支持“别名”」,可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取 robot 多个 spi 实现中别名为“optimusprime”的实现,也可以获取别名为“bumblebee”的实现,这个功能非常有用!
通过 @spi 注解的 value 属性,还可以默认一个“别名”的实现。比如在dubbo 中,默认的是dubbo 私有协议:「dubbo protocol - dubbo://」**
来看看dubbo中协议的接口:
@spi("dubbo")public interface protocol {......}
在 protocol 接口上,增加了一个 @spi 注解,而注解的 value 值为 dubbo ,通过 spi 获取实现时就会获取 protocol spi 配置中别名为dubbo的那个实现,com.alibaba.dubbo.rpc.protocol文件如下:
filter=com.alibaba.dubbo.rpc.protocol.protocolfilterwrapperlistener=com.alibaba.dubbo.rpc.protocol.protocollistenerwrappermock=com.alibaba.dubbo.rpc.support.mockprotocoldubbo=com.alibaba.dubbo.rpc.protocol.dubbo.dubboprotocolinjvm=com.alibaba.dubbo.rpc.protocol.injvm.injvmprotocolrmi=com.alibaba.dubbo.rpc.protocol.rmi.rmiprotocolhessian=com.alibaba.dubbo.rpc.protocol.hessian.hessianprotocolcom.alibaba.dubbo.rpc.protocol.http.httpprotocolcom.alibaba.dubbo.rpc.protocol.webservice.webserviceprotocolthrift=com.alibaba.dubbo.rpc.protocol.thrift.thriftprotocolmemcached=com.alibaba.dubbo.rpc.protocol.memcached.memcachedprotocolredis=com.alibaba.dubbo.rpc.protocol.redis.redisprotocolrest=com.alibaba.dubbo.rpc.protocol.rest.restprotocolregistry=com.alibaba.dubbo.registry.integration.registryprotocolqos=com.alibaba.dubbo.qos.protocol.qosprotocolwrapper
然后只需要通过getdefaultextension,就可以获取到 @spi 注解上value对应的那个扩展实现了
protocol protocol = extensionloader.getextensionloader(protocol.class).getdefaultextension();//protocol: dubboprotocol
还有一个 adaptive 的机制,虽然非常灵活,但&hellip;&hellip;用法并不是很“优雅”,这里就不介绍了
dubbo 的 spi 中还有一个“加载优先级”,优先加载内置(internal)的,然后加载外部的(external),按优先级顺序加载,「如果遇到重复就跳过不会加载」了。
所以如果想靠classpath加载顺序去覆盖内置的扩展,也是个不太理智的做法,原因同上 - 加载顺序不严谨
spring spispring 的 spi 配置文件是一个固定的文件 - meta-inf/spring.factories,功能上和 jdk 的类似,每个接口可以有多个扩展实现,使用起来非常简单:
//获取所有factories文件中配置的loggingsystemfactorylist<loggingsystemfactory>> factories =springfactoriesloader.loadfactories(loggingsystemfactory.class, classloader);
下面是一段 spring boot 中 spring.factories 的配置
# logging systemsorg.springframework.boot.logging.loggingsystemfactory=\org.springframework.boot.logging.logback.logbackloggingsystem.factory,\org.springframework.boot.logging.log4j2.log4j2loggingsystem.factory,\org.springframework.boot.logging.java.javaloggingsystem.factory# propertysource loadersorg.springframework.boot.env.propertysourceloader=\org.springframework.boot.env.propertiespropertysourceloader,\org.springframework.boot.env.yamlpropertysourceloader# configdata location resolversorg.springframework.boot.context.config.configdatalocationresolver=\org.springframework.boot.context.config.configtreeconfigdatalocationresolver,\org.springframework.boot.context.config.standardconfigdatalocationresolver......
spring spi 中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦。至于多个接口的扩展配置,是用一个文件好,还是每个单独一个文件好这个,这个问题就见仁见智了(个人喜欢 spring 这种,干净利落)。
spring的spi 虽然属于spring-framework(core),但是目前主要用在spring boot中&hellip;&hellip;
和前面两种 spi 机制一样,spring 也是支持 classpath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 arraylist 中。由于没有别名,所以也没有去重的概念,有多少就添加多少。
但由于 spring 的 spi 主要用在 spring boot 中,而 spring boot 中的 classloader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个spring.factories文件,那么你项目中的文件会被第一个加载,得到的factories中,项目中spring.factories里配置的那个实现类也会排在第一个
如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个meta-inf/spring.factories文件,「只添加你要的那个配置,不要完整的复制一遍 spring boot 的 spring.factories 文件然后修改」**
比如我只想添加一个新的 loggingsystemfactory 实现,那么我只需要新建一个meta-inf/spring.factories文件,而不是完整的复制+修改:
org.springframework.boot.logging.loggingsystemfactory=\com.example.log4j2demo.log4j2loggingsystem.factory
对比jdk spi
dubbo spi
spring spi
文件方式
每个扩展点单独一个文件
每个扩展点单独一个文件
所有的扩展点在一个文件
获取某个固定的实现
不支持,只能按顺序获取所有实现
有“别名”的概念,可以通过名称获取扩展点的某个固定实现,配合dubbo spi的注解很方便
不支持,只能按顺序获取所有实现。但由于spring boot classloader会优先加载用户代码中的文件,所以可以保证用户自定义的spring.factoires文件在第一个,通过获取第一个factory的方式就可以固定获取自定义的扩展
其他

支持dubbo内部的依赖注入,通过目录来区分dubbo 内置spi和外部spi,优先加载内部,保证内部的优先级最高

文档完整度
文章 & 三方资料足够丰富
文档 & 三方资料足够丰富
文档不够丰富,但由于功能少,使用非常简单
ide支持


idea 完美支持,有语法提示
三种 spi 机制对比之下,jdk 内置的机制是最弱鸡的,但是由于是 jdk 内置,所以还是有一定应用场景,毕竟不用额外的依赖;dubbo 的功能最丰富,但机制有点复杂了,而且只能配合 dubbo 使用,不能完全算是一个独立的模块;spring 的功能和jdk的相差无几,最大的区别是所有扩展点写在一个 spring.factories 文件中,也算是一个改进,并且 idea 完美支持语法提示。
以上就是java spring dubbo三种spi机制的区别是什么的详细内容。
其它类似信息

推荐信息