浅谈 SPI 机制

SPI的概念

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。

SPI 全称为 Service Provider Interface ,是 Java 提供的一套用来被第三方实现或者扩展的API,可以用来启用框架扩展和替换组件。SPI 是一种服务发现机制,本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,使得运行时可以动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

SPI对比API

API:

  1. 概念上更接近实现方
  2. 组织上位于实现方所在的包中
  3. 实现和接口在一个包中

SPI:

  1. 概念上更依赖调用方
  2. 组织上位于调用方所在的包中
  3. 实现位于独立的包中(也可认为在提供方中)

Java SPI

在 JDK6 里面引进的一个新的特性 ServiceLoader,它主要是用来装载一系列的 service provider 。而且 ServiceLoader 可以通过 service provider 的配置文件来装载指定的 service provider 。当服务的提供者,提供了服务接口的一种实现之后,只需要在jar包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

Java SPI 示例

前面简单介绍了 SPI 机制的原理,本节通过一个示例演示 Java SPI 的使用方法。首先,定义一个接口,名称为 Fruit。

1
2
3
public interface Fruit {
void color();
}

接下来定义两个实现类,分别为 Apple 和 Banana。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Apple implements Fruit {

@Override
public void color() {
System.out.println("Red");
}
}

public class Banana implements Fruit {

@Override
public void color() {
System.out.println("Yellow");
}
}

接下来 META-INF/services 文件夹下创建一个文件,名称为 Fruit 的全限定名 org.apache.spi.Fruit 。文件内容为实现类的全限定的类名,如下:

1
2
org.apache.spi.Apple
org.apache.spi.Banana

做好所需的准备工作,接下来编写代码进行测试。

1
2
3
4
5
6
7
8
9
public class JavaSPITest {

@Test
public void color() throws Exception {
ServiceLoader<Fruit> serviceLoader = ServiceLoader.load(Fruit.class);
System.out.println("Java SPI");
serviceLoader.forEach(Fruit::color);
}
}

最后来看一下测试结果,从测试结果可以看出,我们的两个实现类被成功的加载,并输出了相应的内容。

1
2
3
Java SPI
Red
Yellow

DriverManager SPI

DriverManager 是 JDBC 管理和注册不同数据库驱动的工具类。针对一个数据库可能会存在着不同的数据库驱动实现,在使用特定的驱动实现时不希望修改现有的代码才能达到目的,而希望通过一个简单的配置就可以达到效果。比如,我们现在有一个数据库的驱动 A,我们希望在程序里使用它而不修改代码。一种理想的选择就是我们将驱动A的信息加入到一个配置文件中,程序通过读取配置文件信息将 A 加载进来。而以后如果我们希望改用另外一个驱动 B 的时候,我们只需要将配置文件里的信息修改成驱动 B 即可。

我们在运用 Class.forName("com.mysql.jdbc.Driver") 加载 mysql 驱动后,就会执行其中的静态代码把driver注册到 DriverManager 中,以便后续的使用。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}

static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}

这里可以看到,不同的驱动实现了相同的接口 java.sql.Driver ,然后通过 registerDriver 把当前 driver 加载到 DriverManager
这就体现了使用方提供规则,提供方根据规则把自己加载到使用方中的SPI思想。查看 DriverManager 的源码,可以看到其内部的静态代码块中有一个 loadInitialDrivers 方法,在注释中我们看到用到了上文提到的 SPI 工具类 ServiceLoader

1
2
3
4
5
6
7
8
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

点进方法,看到方法里有如下代码:

1
2
3
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> drivers = loadedDrivers.iterator();
println("DriverManager.initialize: jdbc.drivers = " + loadedDrivers);

可见,DriverManager 初始化时也运用了 SPI 机制,使用 ServiceLoader 把写到配置文件里的 Driver 都加载了进来。打开 mysql-connector-java 的 jar 包,果然在 META-INF/services 下发现了上文中提到的接口路径,打开里面的内容,可以看到是 com.mysql.jdbc.Driver

Dubbo SPI

SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader ,可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo路径下,配置内容如下。

1
2
apple = org.apache.spi.Apple
banana = org.apache.spi.Banana

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Fruit 接口上标注@SPI注解。下面来演示 Dubbo SPI 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
public class DubboSPITest {

@Test
public void color() throws Exception {
ExtensionLoader<Fruit> extensionLoader =
ExtensionLoader.getExtensionLoader(Fruit.class);
Fruit apple = extensionLoader.getExtension("apple");
apple.color();
Fruit banana = extensionLoader.getExtension("banana");
banana.color();
}
}

测试结果是:

1
2
3
Dubbo SPI
Red
Yellow

总结

本篇文章简单分别介绍了SPI机制的概念,以及Java SPI ,DriverManager SPI 与 Dubbo SPI 用法。

参考

  1. Java SPI思想梳理
  2. Dubbo SPI
  3. 设计原则:小议 SPI 和 API