SPI机制

SPI

SPI(Service Provider Interface)是 Java 中一种用于实现服务发现机制的标准扩展机制。它允许在应用程序中定义服务接口,然后在运行时通过服务提供者动态注册实现这些接口的具体实现类。

image-20240305214709454

一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。

API: 当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。

SPI: 当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。

实战演示

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

image-20240401095623282

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

新建一个 Java 项目 service-provider-interface 目录结构如下:(注意直接新建 Java 项目就好了,不用新建 Maven 项目,Maven 项目会涉及到一些编译配置,如果有私服的话,直接 deploy 会比较方便,但是没有的话,在过程中可能会遇到一些奇怪的问题。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
│  service-provider-interface.iml

├─.idea
│ │ .gitignore
│ │ misc.xml
│ │ modules.xml
│ └─ workspace.xml

└─src
└─edu
└─jiangxuan
└─up
└─spi
Logger.java
LoggerService.java
Main.class

Logger接口

新建 Logger 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。

1
2
3
4
5
6
package edu.jiangxuan.up.spi;

public interface Logger {
void info(String msg);
void debug(String msg);
}

LoggerService

接下来就是 LoggerService 类,这个主要是为服务使用者(调用方)提供特定功能的。这个类也是实现 Java SPI 机制的关键所在,如果存在疑惑的话可以先往后面继续看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package edu.jiangxuan.up.spi;

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

public class LoggerService {
private static final LoggerService SERVICE = new LoggerService();

private final Logger logger;

private final List<Logger> loggerList;

private LoggerService() {
ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
List<Logger> list = new ArrayList<>();
for (Logger log : loader) {
list.add(log);
}
// LoggerList 是所有 ServiceProvider
loggerList = list;
if (!list.isEmpty()) {
// Logger 只取一个
logger = list.get(0);
} else {
logger = null;
}
}

public static LoggerService getService() {
return SERVICE;
}

public void info(String msg) {
if (logger == null) {
System.out.println("info 中没有发现 Logger 服务提供者");
} else {
logger.info(msg);
}
}

public void debug(String msg) {
if (loggerList.isEmpty()) {
System.out.println("debug 中没有发现 Logger 服务提供者");
}
loggerList.forEach(log -> log.debug(msg));
}
}

程序结果:

info 中没有发现 Logger 服务提供者
debug 中没有发现 Logger 服务提供者

此时我们只是空有接口,并没有为 Logger 接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。

Service Provider

接下来新建一个项目用来实现 Logger 接口

新建项目 service-provider 目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
│  service-provider.iml

├─.idea
│ │ .gitignore
│ │ misc.xml
│ │ modules.xml
│ └─ workspace.xml

├─lib
│ service-provider-interface.jar
|
└─src
├─edu
│ └─jiangxuan
│ └─up
│ └─spi
│ └─service
│ Logback.java

└─META-INF
└─services
edu.jiangxuan.up.spi.Logger


service-provider-interface 的 jar 导入项目中。

新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。

image-20240401105401398

实现 Logger 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package edu.jiangxuan.up.spi.service;

import edu.jiangxuan.up.spi.Logger;

public class Logback implements Logger {
@Override
public void info(String s) {
System.out.println("Logback info 打印日志:" + s);
}

@Override
public void debug(String s) {
System.out.println("Logback debug 打印日志:" + s);
}
}


src 目录下新建 META-INF/services 文件夹,然后新建文件 edu.jiangxuan.up.spi.Logger (SPI 的全类名),文件里面的内容是:edu.jiangxuan.up.spi.service.Logback (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。

这是 JDK SPI 机制 ServiceLoader 约定好的标准。

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

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

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

效果展示

为了更直观的展示效果,我这里再新建一个专门用来测试的工程项目:java-spi-test

然后先导入 Logger 的接口 jar 包,再导入具体的实现类的 jar 包。

image-20240401110117977

新建 Main 方法测试:

1
2
3
4
5
6
7
8
9
10
11
package edu.jiangxuan.up.service;

import edu.jiangxuan.up.spi.LoggerService;

public class TestJavaSPI {
public static void main(String[] args) {
LoggerService loggerService = LoggerService.getService();
loggerService.info("你好");
loggerService.debug("测试Java SPI 机制");
}
}

运行结果如下:

Logback info 打印日志:你好
Logback debug 打印日志:测试 Java SPI 机制

说明导入 jar 包中的实现类生效了,

如果我们不导入具体的实现类的 jar 包,那么此时程序运行的结果就会是:

info 中没有发现 Logger 服务提供者
debug 中没有发现 Logger 服务提供者

通过使用 SPI 机制,可以看出服务(LoggerService)和 服务提供者两者之间的耦合度非常低,如果说我们想要换一种实现,那么其实只需要修改 service-provider 项目中针对 Logger 接口的具体实现就可以了,只需要换一个 jar 包即可,也可以有在一个项目里面有多个实现,这不就是 SLF4J 原理吗?

如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现(service-provider) 来完成我们需要的操作。

那么接下来我们具体来说说 —— ServiceLoader

ServiceLoader

是Java SPI 工作的重点原理

ServiceLoader 是 Java 提供的一个服务提供者加载机制,它允许服务提供者被动态地加载到应用程序中。这种机制基于服务提供者接口(SPI),使得服务的实现可以在运行时被动态加载,而不需要在编译时就确定。

主要的流程就是:

  1. 通过 URL 工具类从 jar 包的 /META-INF/services 目录下面找到对应的文件,
  2. 读取这个文件的名称找到对应的 spi 接口,
  3. 通过 InputStream 流将文件里面的具体实现类的全类名读取出来,
  4. 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,
  5. 将构造出来的实例对象添加到 Providers 的列表中。

想要使用 Java 的 SPI 机制是需要依赖 ServiceLoader 来实现的,那么我们接下来看看 ServiceLoader 具体是怎么做的:ServiceLoader 是 JDK 提供的一个工具类, 位于package java.util;包下。

1
A facility to load implementations of a service.

这是 JDK 官方给的注释:一种加载服务实现的工具。

再往下看,我们发现这个类是一个 final 类型的,所以是不可被继承修改,同时它实现了 Iterable 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。

1
public final class ServiceLoader<S> implements Iterable<S>{ xxx...}

可以看到一个熟悉的常量定义:

1
private static final String PREFIX = "META-INF/services/";

下面是 load 方法:可以发现 load 方法支持两种重载后的入参;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

根据代码的调用顺序,在 reload() 方法中是通过一个内部类 LazyIterator 实现的。先继续往下面看。

ServiceLoader 实现了 Iterable 接口的方法后,具有了迭代的能力,在这个 iterator 方法被调用时,首先会在 ServiceLoaderProvider 缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator 中进行查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Iterator<S> iterator() {
return new Iterator<S>() {

Iterator<Map.Entry<String, S>> knownProviders
= providers.entrySet().iterator();

public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext(); // 调用 LazyIterator
}

public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next(); // 调用 LazyIterator
}

public void remove() {
throw new UnsupportedOperationException();
}
};
}

在调用 LazyIterator 时,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() {
return hasNextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//通过PREFIX(META-INF/services/)和类名 获取对应的配置文件,得到具体的实现类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}


public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() {
return nextService();
}
};
return AccessController.doPrivileged(action, acc);
}
}

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}


SPI机制
http://example.com/2023/06/26/Java/Java进阶/SPI机制/
作者
PALE13
发布于
2023年6月26日
许可协议