概述
SPI
(Service Provider Interface)是一种比较流行的服务发现机制,其核心原理是通过在扫描classpath:META-INF/services
文件夹下定义文件,来实现自动加载某个接口的实现类。
这种机制常为很多框架的扩展提供了便捷,比较常见的,如 Dubbo、JDBC 等。
小案例
下面先通过一个小案例,来看看我们是如何利用这种机制的:
// 定义一个接口,该接口是项目的启动器
1
2
3
4
5
6
|
package com.wuwenze.spi;
public interface Launcher {
void run();
}
|
// 实现定义的 Launcher,该实现类用模拟使用 Jetty 容器来启动项目
1
2
3
4
5
6
7
8
9
10
|
package com.wuwenze.spi.impl;
import com.wuwenze.spi.Launcher;
public class JettyLauncher implements Launcher {
@Override
public void run() {
System.out.println("JettyLauncher is running...");
}
}
|
// 再提供一个 Tomcat 的实现
1
2
3
4
5
6
7
8
9
10
|
package com.wuwenze.spi.impl;
import com.wuwenze.spi.Launcher;
public class TomcatLauncher implements Launcher {
@Override
public void run() {
System.out.println("TomcatLauncher is running...");
}
}
|
// 最后一步,在classpath:META-INF/services
文件夹下定义文件,文件名为接口的全名
com.wuwenze.spi.Launcher
1
2
|
com.wuwenze.spi.impl.JettyLauncher
com.wuwenze.spi.impl.TomcatLauncher
|

// 然后我们就可以利用ServiceLoader.load()
加载该接口的实现,进行测试了

ServiceLoader是如何实现的?
带着疑问,跟一下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
|
public final class ServiceLoader<S>
implements Iterable<S>
{
// 配置实现类文件的路径,刚才已经用过了,按照官方的规范,这个是不能改路径的
private static final String PREFIX = "META-INF/services/";
// 表示正在加载的服务的类或接口
private final Class<S> service;
// 用于查找,加载和实例化提供程序的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采取的访问控制上下文
private final AccessControlContext acc;
// 已经缓存的实现类(按实例顺序)
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 内部类,用于查找实现类
private LazyIterator lookupIterator;
//...
}
|
类的成员,基本都看了,想必大概也猜到是如何实现的了,查找实现类和创建实现类的过程,都在LazyIterator完成。ServiceLoader本质上其实是个迭代器,当我们进行迭代操作时,实际上调用的都是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
|
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();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
|
这里,我们重点关注lookupIterator.hasNext()
方法,然后会调用到hasNextService():
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
|
// 该方法用于获取实现类的类名
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// META-INF/services/com.wuwenze.spi.Launcher
String fullName = PREFIX + service.getName();
// 将文件路径转成URL对象
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;
}
|
当进行迭代器的.next()
操作时,这时就进行真正的对象实例化操作了,这时走到了nextService():
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
|
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
}
|
上面的代码就很明显了,通过反射Class.forName()
轻松获得实例。
总结
SPI底层实现实际上就是反射,Java 的反射特性真是太方便了,吹爆~
评论