JDK/Dubbo/Spring 三种 SPI 机制,谁更好?

先点赞再看,养成好习惯
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样能够在运行时,动态为接口替换实现类。正所以特性,咱们能够很容易的经过 SPI 机制为咱们的程序提供拓展功能。

本文主要是特性 & 用法介绍,不涉及源码解析(源码都很简单,相信你必定一看就懂)html

SPI 有什么用?

举个栗子,如今咱们设计了一款全新的日志框架:super-logger。默认以XML文件做为咱们这款日志的配置文件,并设计了一个配置文件解析的接口:java

package com.github.kongwu.spisamples;

public interface SuperLoggerConfiguration {
    void configure(String configFile);
}

而后来一个默认的XML实现:git

package com.github.kongwu.spisamples;

public class XMLConfiguration implements SuperLoggerConfiguration{
    public void configure(String configFile){
        ......
    }
}

那么咱们在初始化,解析配置时,只须要调用这个XMLConfiguration来解析XML配置文件便可。github

package com.github.kongwu.spisamples;

public class LoggerFactory {
    static {
        SuperLoggerConfiguration configuration = new XMLConfiguration();
        configuration.configure(configFile);
    }
    
    public static getLogger(Class clazz){
        ......
    }
}

这样就完成了一个基础的模型,看起来也没什么问题。不过扩展性不太好,由于若是想定制/扩展/重写解析功能的话,我还得从新定义入口的代码,LoggerFactory 也得重写,不够灵活,侵入性太强了。web

好比如今用户/使用方想增长一个 yml 文件的方式,做为日志配置文件,那么只须要新建一个YAMLConfiguration,实现 SuperLoggerConfiguration 就能够。可是……怎么注入呢,怎么让 LoggerFactory中使用新建的这个 YAMLConfiguration ?难不成连 LoggerFactory 也重写了?redis

若是借助SPI机制的话,这个事情就很简单了,能够很方便的完成这个入口的扩展功能。spring

下面就先来看看,利用JDK 的 SPI 机制怎么解决上面的扩展性问题。apache

JDK SPI

JDK 中 提供了一个 SPI 的功能,核心类是 java.util.ServiceLoader。其做用就是,能够经过类名获取在"META-INF/services/"下的多个配置实现文件。bash

为了解决上面的扩展问题,如今咱们在META-INF/services/下建立一个com.github.kongwu.spisamples.SuperLoggerConfiguration文件(没有后缀)。文件中只有一行代码,那就是咱们默认的com.github.kongwu.spisamples.XMLConfiguration(注意,一个文件里也能够写多个实现,回车分隔)oracle

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 SPI

Dubbo 就是经过 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.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

与 Java SPI 实现类配置不一样,Dubbo SPI 是经过键值对的方式进行配置,这样咱们能够按需加载指定的实现类。另外在使用时还须要在接口上标注 @SPI 注解。下面来演示 Dubbo SPI 的用法:

@SPI
public interface Robot {
    void sayHello();
}

public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}


public class DubboSPITest {

    @Test
    public 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.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol


dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol


injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper

而后只须要经过getDefaultExtension,就能够获取到 @SPI 注解上value对应的那个扩展实现了

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol

还有一个 Adaptive 的机制,虽然很是灵活,但……用法并非很“优雅”,这里就不介绍了

Dubbo 的 SPI 中还有一个“加载优先级”,优先加载内置(internal)的,而后加载外部的(external),按优先级顺序加载,若是遇到重复就跳过不会加载了。

因此若是想靠classpath加载顺序去覆盖内置的扩展,也是个不太理智的作法,缘由同上 - 加载顺序不严谨

Spring SPI

Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的相似,每一个接口能够有多个扩展实现,使用起来很是简单:

//获取全部factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories = 
    SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

下面是一段 Spring Boot 中 spring.factories 的配置

# Logging Systems
org.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 Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# ConfigData Location Resolvers
org.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中……

和前面两种 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 完美支持语法提示。

各位看官们大佬们,大家以为 JDK/Dubbo/Spring 三种 SPI 的机制,哪一个更好呢?欢迎评论区留言

参考

原创不易,未经受权禁止转载。若是个人文章对您有帮助,请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤
相关文章
相关标签/搜索