前言
其实第一次接触 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-starter
、 rocketmq-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,阅读更多分享文章