聊聊 SPI 机制

354 阅读6分钟

前言

其实第一次接触 SPI 的概念是在两年前阅读 dubbo 源码的时候。

可能是年少无知吧,当时看了一下感觉这特性平平无奇。

但是随着在后来的工作中慢慢发现其重要性,尤其最近在写基础组件的时候,更加深有体会。

什么是 SPI

SPI(Service Provider Interface),是 JDK 引入了一个用于发现和加载与给定接口匹配的实现的特性。

简单来说,可以理解为它提供了一种动态的可插拔式的实现。

在系统设计的过程中,我们通常都会抽象各个功能模块。在代码的体现,则是基于接口的抽象,不同的业务场景有各自的实现类。

比如日志模块中有多种日志实现类,数据库驱动加载接口有不同类型厂商的数据库的驱动实现类。

那么对于开发者不同的需求实现,那岂不是需要修改代码,替换所需的实现类?

SPI 正是为了解决这个问题。

举个简单的例子:

下面是 springboot-jdbc 的配置,可以看到参数 driverClassName 配置的数据源 mysql:

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123456

但是如果开发者的数据库是 SqlServer 呢?

没错,仅需要修改一下 driverClassName 的属性就可以实现切换到 SqlServer 的数据源,无需改动任何代码!

spring:
  datasource:
    driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
    url: jdbc:sqlserver:/localhost:3306;databaseName=springboot
    username: root
    password: 123456

这时候应该能体验到 SPI 的实用之处了吧。

将服务具体的实现,交由以配置的方式来控制,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

应用场景

Java SPI

我们先来看一下 JDK 内置提供的 SPI 机制

这里定义了一个数据库执行器(Executor)的接口,不同厂商的实现可能有多种方式,比如 MySql、Oracle。

  • 定义执行器(Executor)接口
public interface Executor {
    public String invoker();
}
  • 执行器(Executor)的 MySql 实现
public class MysqlExecutor implements Executor {
    @Override
    public String invoker() {
        return "mysql";
    }
}
  • 执行器(Executor)的 Oracle 实现
public class OracleExecutor implements Executor{
    @Override
    public String invoker() {
        return "oracle";
    }
}
  • 在 resources 下新建 META-INF/services/ 目录,然后新建接口全限定名的文件:com.demo.Executor,里面加上我们需要用到的实现类
com.demo.MysqlExecutor
  • 测试方法
public class SPIMain {
    public static void main(String[] argus){
        ServiceLoader<Executor> serviceLoader = ServiceLoader.load(Executor.class);
        Iterator<Executor> iterator = serviceLoader.iterator();
        while (iterator.hasNext()) {
            Executor executor =  iterator.next();
            System.out.println(executor.invoker());
        }
    }
}

输出结果:mysql

我们这里可以看到, ServiceLoader.load(Executor.class) 通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把实现类加载到 JVM。

当然,ServiceLoader 同时也是一个迭代器,在com.demo.Executor文件里写上多个实现类,也会全部加载进去。

将接口的实现类抽象到上层,由配置文件来控制其选择,可以说有点 IOC 的内味了。

JDBC DriverManager

JDK 中定义了 java.sql.Driver 接口,但并没有具体的实现,具体的实现都是由不同厂商来提供的。

要接入不同的厂商的数据源,就需要引入相应的客户端 Jar 包,比如

MySql 需要引入: mysql-connector-java-x.x.x.jar

Postgresql 需要引入:postgresql-x.x.x.jar

其实在 Jar 包里的 META-INF/services 目录下,都会有一个 java.sql.Driver 文件,而文件的内容则是各自各自厂商具体的实现类。

上面提到 spring-boot 数据库驱动配置的栗子,其实底层的核心实现也是通过此原理实现。

Spring

如果大家有了解过一些开发框架或者中间件集成 Spring-boot 的 starter 组件,比如 dubbo-spring-boot-starterrocketmq-spring-boot-starter 等,相信对 spring.factories 一定不会陌生。

与 Java SPI 机制区别的是,Springboot 的 SPI 配置文件是一个固定的文件 : META-INF/spring.factories,而且实现类的加载和实例化由 SpringFactoriesLoader 实现。

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

当然,我们通常见到的开源框架集成的 starter 组件一般都只扩展了 EnableAutoConfiguration 这个接口,当项目启动的时候能够自动装配需要的 Bean。

dubbo-spring-boot-starter 中的 spring.factories 配置:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBinding2AutoConfiguration

Dubbo

Dubbo SPI 是整个框架的精髓之一,小年觉得非常值得去学习一下的。

碍于篇幅,这里就简单讲讲几个核心概念不做过多的展开,后续的话会独立一篇源码分析吧。

Dubbo 是自己实现了一套 SPI 机制,相比于JDK 内置提供的 SPI,会更加的灵活和扩展性也会更高。

话不多说,直接举个简单的栗子

先问一个常见面试题:Dubbo 集群容错有哪几种方式?

  • Failover
  • Failfast
  • Failsafe
  • Failback
  • ......

@SPI 定义 Cluster 为可扩展接口,而具体的集群实现类都是通过实现此接口。

Dubbo SPI 配置目录分为三种:

  • META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI
  • META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件
  • META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件

与 JAVA SPI 的配置方式不同的是,文件设计成 KV 键值对的形式。

看到上图 中的 @SPI(Cluster.DEFAULT) 其实就等同于 @SPI("failover"), @SPI 中的属性值则对应配置中的 Key,意思是指 FailoverCluster 实现类作为默认适配扩展点。

对于为什么要设计成 KV 的这种形式,我们可以有个更直观的感受就是,在配置服务 Provider 或者 Consumer 的集群类型的时候,只需要填写对应 key 就可以 <dubbo:reference cluster="failfast">

Dubbo 自主实现的 ExtensionLoader ,类似于 Java SPI 中的 ServiceLoader

正因为借助于 SPI 的方式,Dubbo 所有的内部组件都能够提供可插拔的形式,开发者也能够随意扩展,这种方式可谓是YYDS!

总结

简而言之,SPI 的核心思想是提供一个可插拔可扩展的机制,并且把具体实现类的配置抽象到配置文件中,更加方便开发者的使用。

SPI 在众多的开源框架中是非常常见的,而各种框架的 SPI 机制又各有不同,或多或少都有一些演变。

比如 Spring SPI 将所有的扩展点集成在一个配置文件;Dubbo SPI 以注解的方式提供扩展性更强的机制。

但是不管如何演变,其实背后的原理都是大同小异。

通过深入学习开源框架的原理特性,有助于我们对系统设计的理解,在日常的开发也能够提供实用的参考,不至于书到用书方恨少。

普通的改变,将改变普通

我是宅小年,一个在互联网低调前行的小青年

关注公众号「宅小年」,个人博客 📖 edisonz.cn,阅读更多分享文章