SPI机制

SPI介绍

SPI,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。Java中SPI机制主要思想是将装配的控制权移到程序之外,其核心思想就是解耦。
这里面有三个核心概念:

  • 服务:即服务的抽象接口。
  • 服务提供者:即接口的具体实现类。
  • 服务加载器:serviceloader,通过服务的全路径名称找到服务提供者所在路径并加载到内存。

SPI使用

使用SPI,可以简单的分为4步:

  • 定义接口/抽象类
  • 实现类
  • 在实现方的META-INF/services下,创建一个以接口的全限定名为名称的文件,内容是提供是该接口的实现类的全限定名
  • 使用ServiceLoader.load()方法来加载实现类

举例说明

定义接口
1
2
3
public interface SPIService {
void execute();
}
实现类
1
2
3
4
5
6
7
8
9
10
11
public class SpiImpl1 implements SPIService{
public void execute() {
System.out.println("SpiImpl1.execute()");
}
}

public class SpiImpl2 implements SPIService{
public void execute() {
System.out.println("SpiImpl2.execute()");
}
}
创建一个以接口的全限定名为名称的文件


内容为实现类的全限定名

1
2
com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2
ServiceLoader.load()加载实现类

ServiceLoader.load方法拿到实现类的实例,ServiceLoader.load包位于java.util.ServiceLoader

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);

Iterator<SPIService> iterator = load.iterator();
while(iterator.hasNext()) {
SPIService ser = iterator.next();
ser.execute();
}
}
}

源码浅析(jdk 11.0.13)

构造ServiceLoader对象

静态方法load(Class clazz)不会立刻加载服务提供者类,它仅仅只是获取当前线程上下文类加载器cl,而Reflection.getCallerClass()可以得到调用者的类,然后用clazz和cl构造一个ServiceLoader实例loader。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
public final class ServiceLoader<S> implements Iterable<S>{
// The class or interface representing the service being loaded
private final Class<S> service;

// The class of the service type
private final String serviceName;

// The module layer used to locate providers; null when locating
// providers using a class loader
private final ModuleLayer layer;

// The class loader used to locate, load, and instantiate providers;
// null when locating provider using a module layer
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// The lazy-lookup iterator for iterator operations
private Iterator<Provider<S>> lookupIterator1;
private final List<S> instantiatedProviders = new ArrayList<>();

// The lazy-lookup iterator for stream operations
private Iterator<Provider<S>> lookupIterator2;
private final List<Provider<S>> loadedProviders = new ArrayList<>();
private boolean loadedAllProviders; // true when all providers loaded

// Incremented when reload is called
private int reloadCount;

//对应的例子中的静态方法
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}

//对应的构造函数
private ServiceLoader(Class<?> caller, Class<S> svc, ClassLoader cl) {
Objects.requireNonNull(svc);

if (VM.isBooted()) {
checkCaller(caller, svc);
if (cl == null) {
cl = ClassLoader.getSystemClassLoader();
}
} else {

// if we get here then it means that ServiceLoader is being used
// before the VM initialization has completed. At this point then
// only code in the java.base should be executing.
Module callerModule = caller.getModule();
Module base = Object.class.getModule();
Module svcModule = svc.getModule();
if (callerModule != base || svcModule != base) {
fail(svc, "not accessible to " + callerModule + " during VM init");
}

// restricted to boot loader during startup
cl = null;
}

this.service = svc;
this.serviceName = svc.getName();
this.layer = null;
this.loader = cl;
this.acc = (System.getSecurityManager() != null)
? AccessController.getContext()
: null;
}

}

构造过程中会先清理掉缓存,初始化相关参数。

  • service : 表示正在加载的服务的类或接口
  • serviceName : 服务类型
  • layer : 类加载器的提供者,初始化时为null
  • loader : 用于查找,加载和实例化提供程序的类加载器。
  • acc : 创建ServiceLoader时采取的访问控制上下文
查找实现类及加载实现类

当在迭代或者stream流时,会构建一个LookupIterator延迟加载迭代器,期间会初始化两个局部变量ModuleServicesLookupIteratorLazyClassPathLookupIterator,然后用lookupIterator读取META-INF/services/下以服务接口全限定名命名的配置文件,从中读取提供者全限定名交给线程上下文类加载器进行加载,最后在lookupIterator中实例化。

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
public Iterator<S> iterator() {

// create lookup iterator if needed
if (lookupIterator1 == null) {
lookupIterator1 = newLookupIterator();
}

return new Iterator<S>() {

// record reload count
final int expectedReloadCount = ServiceLoader.this.reloadCount;

// index into the cached providers list
int index;

/**
* Throws ConcurrentModificationException if the list of cached
* providers has been cleared by reload.
*/
private void checkReloadCount() {
if (ServiceLoader.this.reloadCount != expectedReloadCount)
throw new ConcurrentModificationException();
}

@Override
public boolean hasNext() {
checkReloadCount();
if (index < instantiatedProviders.size())
return true;
return lookupIterator1.hasNext();
}

@Override
public S next() {
checkReloadCount();
S next;
if (index < instantiatedProviders.size()) {
next = instantiatedProviders.get(index);
} else {
next = lookupIterator1.next().get();
instantiatedProviders.add(next);
}
index++;
return next;
}

};
}

我们可以看到,当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是newLookupIterator的相应方法。

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
private Iterator<Provider<S>> newLookupIterator() {
assert layer == null || loader == null;
if (layer != null) {
return new LayerLookupIterator<>();
} else {
Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
return new Iterator<Provider<S>>() {
@Override
public boolean hasNext() {
return (first.hasNext() || second.hasNext());
}
@Override
public Provider<S> next() {
if (first.hasNext()) {
return first.next();
} else if (second.hasNext()) {
return second.next();
} else {
throw new NoSuchElementException();
}
}
};
}
}

而newLookupIterator的next和hasNext调用的是LazyClassPathLookupIterator的方法

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
private final class LazyClassPathLookupIterator<T> implements Iterator<Provider<T>>
{
static final String PREFIX = "META-INF/services/";

Set<String> providerNames = new HashSet<>(); // to avoid duplicates
//存放所有实现类的全限定名
Enumeration<URL> configs;
//当前类对象的迭代器
Iterator<String> pending;
//当前迭代的实现类
Provider<T> nextProvider;
ServiceConfigurationError nextError;

LazyClassPathLookupIterator() { }

/**
* Parse a single line from the given configuration file, adding the
* name on the line to set of names if not already seen.
*/
private int parseLine(URL u, BufferedReader r, int lc, Set<String> names)
throws IOException
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
int start = Character.charCount(cp);
for (int i = start; i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (providerNames.add(ln)) {
names.add(ln);
}
}
return lc + 1;
}

/**
* Parse the content of the given URL as a provider-configuration file.
*/
private Iterator<String> parse(URL u) {
Set<String> names = new LinkedHashSet<>(); // preserve insertion order
try {
URLConnection uc = u.openConnection();
uc.setUseCaches(false);
try (InputStream in = uc.getInputStream();
BufferedReader r
= new BufferedReader(new InputStreamReader(in, UTF_8.INSTANCE)))
{
int lc = 1;
while ((lc = parseLine(u, r, lc, names)) >= 0);
}
} catch (IOException x) {
fail(service, "Error accessing configuration file", x);
}
return names.iterator();
}

/**
* Loads and returns the next provider class.
*/
private Class<?> nextProviderClass() {
if (configs == null) {
try {
//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
//META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
String fullName = PREFIX + service.getName();
if (loader == null) {
//将文件路径转成URL对象
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// The platform classloader doesn't have a class path,
// but the boot loader might.
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
//将文件路径转成URL对象
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
//解析URL文件对象,读取内容,最后返回
pending = parse(configs.nextElement());
}
String cn = pending.next();
try {
//加载类对象
return Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}

@SuppressWarnings("unchecked")
private boolean hasNextService() {
while (nextProvider == null && nextError == null) {
try {
//拿到刚加载的类对象
Class<?> clazz = nextProviderClass();
if (clazz == null)
return false;

if (clazz.getModule().isNamed()) {
// ignore class if in named module
continue;
}

//实例化对象
if (service.isAssignableFrom(clazz)) {
Class<? extends S> type = (Class<? extends S>) clazz;
Constructor<? extends S> ctor
= (Constructor<? extends S>)getConstructor(clazz);
ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
nextProvider = (ProviderImpl<T>) p;
} else {
fail(service, clazz.getName() + " not a subtype");
}
} catch (ServiceConfigurationError e) {
nextError = e;
}
}
return true;
}

private Provider<T> nextService() {
if (!hasNextService())
throw new NoSuchElementException();

Provider<T> provider = nextProvider;
if (provider != null) {
nextProvider = null;
return provider;
} else {
ServiceConfigurationError e = nextError;
assert e != null;
nextError = null;
throw e;
}
}

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

@Override
public Provider<T> next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<Provider<T>> action = new PrivilegedAction<>() {
public Provider<T> run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
}
源码浅析小结


迭代过程会涉及到以下几个变量:

  • Enumeration configs:存放所有实现类的全限定名
  • Iterator pending:当前类对象的迭代器
  • Provider nextProvider:当前迭代的实现类

在hasNextService中会调用nextProviderClass返回当前遍历的实现类Class对象,并实例化赋值给nextProvider;而在nextService()中则直接返回nextProvider对象实例。
nextProviderClass方法的作用是获取配置文件中所有的全限定名称实现类URL到configs,并返回当前遍历实现类Class对象

SPI应用

JDBC

1
2
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc://xxxx", "root", "xxx");

这里为什么放mysql驱动可以获取到mysql的连接,放oracle的驱动就能获取到oracle的连接?
DriverManager 第100行如下:

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

可以看到,DriverManger被初始化的时候,会执行loadInitialDrivers()方法。

DriverManager 第586行:

1
2
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

可以看到使用到了SPI方法,ServiceLoader.load()和上面我们写的demo一样,加载Driver这个接口的实现类。那Driver的实现类肯定是在mysql的驱动包内放着

像JDBC这种内置于java核心库的SPI,则使用了线程上下文类加载器,实际上是java给自己开的后门,能够不遵守类加载器的双亲委派原则。

Tomcat

以前使用SpringMVC开发项目的时候,会将项目打包,然后放到tomcat容器中运行。
那么你打开spring-web项目的源码,会发现spring-web下有spi的配置:

再看下Tomcat源码


可以看到Tomcat的源码里确实是去META-INF/services下找需要加载的类。SpringMVC配置了ServletContainerInitializer,在项目部署到Tomcat后,Tomcat回调该方法,SpringMVC完成父子容器的加载。

Spring Boot

在Spring Boot中也应用SPI这种可插件化思想。当你使用@SpringBootApplication注解时,会开始自动配置,而启动配置则会去扫描META-INF/spring.factories下的配置类。

参考资料