SPI

1
2
3
4
5
6
7
SPI 是一种用于动态加载服务的机制。

它的核心思想就是**解耦**,属于典型的微内核架构模式(内核不变,外部插件式扩展)。

SPI 在 Java 世界应用非常广泛,如:Jdbc、Common Logging、Dubbo、Spring Boot 等框架。

本文从源码入手分析,深入探讨 Java SPI 的特性、原理,以及在一些比较经典领域的应用。

SPI 简介

1
2
3
4
5
6
7
8
9
10
11
12
# SPI => 解耦(方便扩展)
SPI 全称 Service Provider Interface,是 Java 提供的,旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制。

Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是:解耦

# Java SPI 四要素
1. SPI 接口:为服务提供者实现类约定的的接口或抽象类。
2. SPI 实现类:实际提供服务的实现类。
3. SPI 配置:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。
配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。
文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
4. ServiceLoader:Java SPI 的核心类,用于加载 SPI 实现类。ServiceLoader 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。

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
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
83
84
85
86
正所谓,实践出真知,我们不妨通过一个具体的示例来看一下,如何使用 Java SPI。

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

package com.taopanfeng.junit.spi;

/**
* 动物
*
* @author 陶攀峰
* @date 2022-11-04 09:50
*/
public interface Animal {
String name();
}

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

package com.taopanfeng.junit.spi;

/**
* 狗
*
* @author 陶攀峰
* @date 2022-11-04 09:52
*/
public class Dog implements Animal {

@Override
public String name() {
return "狗";
}
}

package com.taopanfeng.junit.spi;

/**
* 猫
*
* @author 陶攀峰
* @date 2022-11-04 09:53
*/
public class Cat implements Animal{
@Override
public String name() {
return "猫";
}
}

# 3. SPI 配置
如果想通过 Java SPI 机制来发现服务,就需要在 SPI 配置中约定好发现服务的逻辑。
配置文件必须置于 META-INF/services 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。
文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。
以本示例代码为例,其文件名应该为 com.taopanfeng.junit.spi.Animal
文件中的内容如下:
com.taopanfeng.junit.spi.Dog
com.taopanfeng.junit.spi.Cat

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

package com.taopanfeng.junit.spi;

import java.util.ServiceLoader;

/**
* 描述
*
* @author 陶攀峰
* @date 2022-11-04 09:54
*/
public class Main {
// 2022-11-04 09:54
public static void main(String[] args) throws Exception {
ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);

serviceLoader.forEach(e -> {
System.out.println(e.name());
});
// 狗
// 猫

}
}

SPI 原理

先放图片,下面再讲。
TODO 待补充


1
2
3
4
5
6
上文中,我们已经了解 Java SPI 的要素以及使用 Java SPI 的方法。
你有没有想过,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。

实际上,Java SPI 机制依赖于 ServiceLoader 类去解析、加载服务。
因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。
ServiceLoader 的代码本身很精练,接下来,让我们通过走读源码的方式,逐一理解 ServiceLoader 的工作流程。

1、ServiceLoader 的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
先看一下 ServiceLoader 类的成员变量,大致有个印象,后面的源码中都会使用到。
public final class ServiceLoader<S> implements Iterable<S> {

// SPI 配置文件目录
private static final String PREFIX = "META-INF/services/";

// 将要被加载的 SPI 服务
private final Class<S> service;

// 用于加载 SPI 服务的类加载器
private final ClassLoader loader;

// ServiceLoader 创建时的访问控制上下文
private final AccessControlContext acc;

// SPI 服务缓存,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 懒查询迭代器
private LazyIterator lookupIterator;

// ...
}

2、ServiceLoader 的工作流程

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# (1)ServiceLoader.load 静态方法

应用程序加载 Java SPI 服务,都是先调用 ServiceLoader.load 静态方法。
ServiceLoader.load 静态方法的作用是:
① 指定类加载 ClassLoader 和访问控制上下文;
② 然后,重新加载 SPI 服务
+ 清空缓存中所有已实例化的 SPI 服务
+ 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器

这里,摘录 ServiceLoader.load 相关源码,如下:
// service 传入的是期望加载的 SPI 接口类型
// loader 是用于加载 SPI 服务的类加载器
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}

public void reload() {
// 清空缓存中所有已实例化的 SPI 服务
providers.clear();
// 根据 ClassLoader 和 SPI 类型,创建懒加载迭代器
lookupIterator = new LazyIterator(service, loader);
}

// 私有构造方法
// 重新加载 SPI 服务
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 指定类加载 ClassLoader 和访问控制上下文
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 然后,重新加载 SPI 服务
reload();
}

# (2)应用程序通过 ServiceLoader 的 iterator 方法遍历 SPI 实例
ServiceLoader 的类定义,明确了 ServiceLoader 类实现了 Iterable<T> 接口,所以,它是可以迭代遍历的。
实际上,ServiceLoader 类维护了一个缓存 providers( LinkedHashMap 对象),缓存 providers 中保存了已经被成功加载的 SPI 实例,
这个 Map 的 key 是 SPI 接口实现类的全限定名,value 是该实现类的一个实例对象。

当应用程序调用 ServiceLoader 的 iterator 方法时,ServiceLoader 会先判断缓存 providers 中是否有数据:
+ 如果有,则直接返回缓存 providers 的迭代器;
+ 如果没有,则返回懒加载迭代器的迭代器。
public Iterator<S> iterator() {
return new Iterator<S>() {

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

// lookupIterator 是 LazyIterator 实例,用于懒加载 SPI 实例
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}

public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}

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

};
}

# (3)懒加载迭代器的工作流程
上面的源码中提到了,lookupIterator 是 LazyIterator 实例,而 LazyIterator 用于懒加载 SPI 实例。

那么, LazyIterator 是如何工作的呢?
这里,摘取 LazyIterator 关键代码

hasNextService 方法:
+ 拼接 META-INF/services/ + SPI 接口全限定名
+ 通过类加载器,尝试加载资源文件
+ 解析资源文件中的内容,获取 SPI 接口的实现类的全限定名 nextName

nextService 方法:
+ hasNextService() 方法解析出了 SPI 实现类的的全限定名 nextName,通过反射,获取 SPI 实现类的类定义 Class。
+ 然后,尝试通过 Class 的 newInstance 方法实例化一个 SPI 服务对象。如果成功,则将这个对象加入到缓存 providers 中并返回该对象。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 1.拼接 META-INF/services/ + SPI 接口全限定名
// 2.通过类加载器,尝试加载资源文件
// 3.解析资源文件中的内容
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;
}

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 s");
}
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
}

3、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
通过上面两个章节中,走读 ServiceLoader 代码,我们已经大致了解 Java SPI 的工作原理。
=> 即通过 ClassLoader 加载 SPI 配置文件,解析 SPI 服务,然后通过反射,实例化 SPI 服务实例。
我们不妨思考一下,为什么加载 SPI 服务时,需要指定类加载器 ClassLoader 呢?

# 双亲委派模型
学习过 JVM 的读者,想必都了解过类加载器的双亲委派模型(Parents Delegation Model)。
双亲委派模型要求除了顶层的 BootstrapClassLoader 外,其余的类加载器都应有自己的父类加载器。
这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。

# 双亲委派机制
双亲委派机制约定了:一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。

# 双亲委派的好处
双亲委派的好处:使得 Java 类伴随着它的类加载器,天然具备一种带有优先级的层次关系,从而使得类加载得到统一,不会出现重复加载的问题:
+ 系统类防止内存中出现多份同样的字节码
+ 保证 Java 程序安全稳定运行

# 举个栗子
例如:java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 classpath 中,程序可以编译通过。
因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 优先级更高。
=> 因为 rt.jar 中的 Object 使用的是启动类加载器,而 classpath 中的 Object 使用的是应用程序类加载器。
正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是 rt.jar 中的 Object。

# 双亲委派的限制
子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。
这就导致了双亲委派模型并不能解决所有的类加载器问题。

Java SPI 就面临着这样的问题:
+ SPI ServiceLoader类 是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的;
+ 而 SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。
BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。
它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。
这也解释了本节开始的问题——为什么加载 SPI 服务时,需要指定类加载器 ClassLoader 呢?
==> 因为如果不指定 ClassLoader,则无法获取 SPI 服务。

如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。
在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。
线程上下文类加载器在很多 SPI 的实现中都会用到。

通常可以通过 Thread.currentThread().getContextClassLoader() 获取线程上下文类加载器。

4、Java SPI 的不足

1
2
3
4
5
Java SPI 存在一些不足:
+ 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。
如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
+ 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
+ 多个并发多线程使用 ServiceLoader 类的实例是不安全的。

SPI 应用场景

TODO 待补充


聊天结束