关于java:JDKDubboSpring-三种-SPI-机制谁更好

2次阅读

共计 8963 个字符,预计需要花费 23 分钟才能阅读完成。

先点赞再看,养成好习惯

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的实质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样能够在运行时,动静为接口替换实现类。正因而个性,咱们能够很容易的通过 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 SPI

JDK 中 提供了一个 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 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 的机制,哪个更好呢?欢送评论区留言

参考

  • Introduction to the Service Provider Interfaces – Oracle
  • Dubbo SPI – Apache Dubbo
  • Creating Your Own Auto-configuration – Spring

原创不易,未经受权禁止转载。如果我的文章对您有帮忙,请点赞 / 珍藏 / 关注激励反对一下吧❤❤❤❤❤❤

正文完
 0