SPI

SPI是什么

SPI 全称 Service Provider Interface ,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

Java SPI实际上是基于接口+策略模式+配置文件组合实现的动态加载机制

Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。

将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

所以 SPI 的核心思想就是解耦

SPI与API的区别

SPI四要素

  • SPI接口:为服务提供者实现类约定的接口或抽象类

  • SPI实现类:实际提供服务的实现类

  • SPI配置:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称

  • ServiceLoader:Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。

实践

SPI接口

首先,需要定义一个 SPI 接口,和普通接口并没有什么差别。

1
2
3
public interface city {
String cityName();
}

SPI实现类

1
2
3
4
5
6
public class ChenzhouCity implements city{
@Override
public String cityName() {
return "郴州";
}
}
1
2
3
4
5
6
public class ChangSha implements city{
@Override
public String cityName() {
return "长沙";
}
}

service 传入的是期望加载的 SPI 接口类型 到目前为止,定义接口,并实现接口和普通的 Java 接口实现没有任何不同。

SPI配置

如果想通过 Java SPI 机制来发现服务,就需要在 SPI 配置中约定好发现服务的逻辑。配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。

1
2
spi.ChangSha
spi.ChenzhouCity

ServiceLoader

完成了上面的步骤,就可以通过 ServiceLoader 来加载服务。示例如下:

1
2
3
4
5
6
7
public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<city> serviceLoader = ServiceLoader.load(city.class);
System.out.println("============ Java SPI 测试============");
serviceLoader.forEach(loader -> System.out.println(loader.cityName()));
}
}

输出:

SPI作用

举例:日志

SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接口),其具体实现有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。

这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。

生产场景

相信大家在生产上都使用过 JDBC,没错,我们的 JDBC 实际上也使用了 SPI

我们看 DriverManager 的静态方法 loadInitialDrivers

1
2
3
4
5
 static {
// 初始化加载
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

我们查看下 loadInitialDrivers 方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static void loadInitialDrivers() {
String drivers;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// SPI的加载机制
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 迭代
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
}
return null;
}
});
}

当然,这里我们需要引入下面的 MAVEN 依赖,不然 Driver.class 的实现类为空

1
2
3
4
5
6
 <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.study.spring.spi;

import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;

public class JDBCTest {
public static void main(String[] args) {
ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);

for (Iterator<Driver> iterator = serviceLoader.iterator(); iterator.hasNext(); ) {

Driver driver = iterator.next();

System.out.println(driver.getClass().getPackage() + " ------> " + driver.getClass().getName());
}
}
}

结果:

1
package com.mysql.cj.jdbc, JDBC, version 4.2 ------> com.mysql.cj.jdbc.Driver

从这里的输出我们可以得出一个假设:我们引入的 JDBC 包里面,存在上述 SPI 机制的 txt 文件名称为: java.sql.Driver 且内容为:com.mysql.cj.jdbc.Driver

我们直接去引入的包里面搜索一下:

JDK标准

JDK SPI 机制 ServiceLoader 约定好的标准:这里先大概解释一下:Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的 META-INF 文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。

所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。

接下来同样将 service-provider 项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。


SPI
http://example.com/2023/10/11/SPI/
Author
Posted on
October 11, 2023
Licensed under