dubbo-JDK的SPI原理及源码分析

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。java语言的特性,一处编译处处运行,很大程度上是因为使用了使用spi机制。

JDK在rt.jar包中定义了很多的接口,这些接口由于各种原因没有给出实现类,

  • 操作系统不同,实现方式不同,例如nio底层的selector实现类,不同os有自己的实现
  • 服务商不同,实现方式不同,例如jdbc,不同的数据库服务商对jdbc都有自己的实现
  • 统一调用接口,例如slf4j

SPI是门面模式的一种应用场景,在平时的开发过程中,如果发现一个模块需要集成多个平台同一个功能,不妨考虑使用这种机制,比如支付功能、对象存储功能等等。此外dubbo为了集成多协议多平台,对spi的使用非常多

SPI如何使用

定义接口类

1
2
3
public interface SPIService {
void execute();
}

然后,定义两个实现类

1
2
3
4
5
6
7
8
9
10
public class SpiImpl1 implements SPIService{
public void execute() {
System.out.println("SpiImpl1");
}
}
public class SpiImpl2 implements SPIService{
public void execute() {
System.out.println("SpiImpl2");
}
}

最后呢,要在ClassPath路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。
文件路径为:

resources/META_INF/services/com.pd.spi.SPIInterface

文件内容为:

1
2
com.pd.spi.SpiImpl1
com.pd.spi.SpiImpl2

在测试代码中,我们使用ServiceLoader.load或者Service.providers方法拿到实现类的实例

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
ServiceLoader<SPIInterface> spiImpls = ServiceLoader.load(SPIInterface.class);
for(SPIInterface impl : spiImpls){
impl.execute();
}
}
}

输出:

1
2
SpiImpl1
SpiImpl2

SPI源码分析

两种服务获取方式:

Service.providers包位于sun.misc.Service

ServiceLoader.load包位于java.util.ServiceLoader

首先看一下ServiceLoader类的成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 已加载的服务类实现集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 真正加载逻辑所在的对象,内部类
private LazyIterator lookupIterator;
}

静态方法load()

1
2
3
4
public static <S> ServiceLoader<S> load(Class<S> service) {    
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

可以看到,这里获取了线程上下文类加载器来加载实现类,双亲委派模式的破坏者。

调用了重载的load()方法

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

调用了私有的构造函数,由于实现类对象最终还是保存到了ServiceLoader的成员变量providers中,所以这里猜测,在构造方法中完成了实现类的获取和实例化:

1
2
3
4
5
6
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();
}

跟进reload()方法:

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

实例化了内部类LazyIterator

1
2
3
4
private LazyIterator(Class<S> service, ClassLoader loader) {    
this.service = service;
this.loader = loader;
}

到此初始化流程就结束了,构造方法中并没有加载的过程啊,猜测错误。

原来这个类名称是LazyIterator,原来这里使用了懒加载,当ServiceLoader初始化的时候并不会主动去加载实现类,而是在用户代码中使用到实现类的时候再进行加载。

当用户代码执行到此:

1
2
3
for(SPIInterface impl : spiImpls){
impl.execute();
}

将会执行LazyIterator类的hasNext()

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

总之会调用到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
private boolean hasNextService() {    
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 拿到配置文件名
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;
}
// 解析配置文件,返回一个ArrayList的迭代器对象
pending = parse(service, configs.nextElement());
}
//nextName指向迭代器指向的那个对象,是实现类的全类名
nextName = pending.next();
return true;
}

拿到实现类的全类名,现在开始实例化:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

调用到nextService()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 {
Sp = 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
}

使用反射的方式实例化实现类

总结:

1、 Jdk的spi 会一次性加载并实例化扩展点的所有实现,就是如果在MATA-INF/services下的文件里面加了N个实现类,那么JDK启动的时候都会一次性全部实例化,那么如果有的扩展点初始化很耗时,且运行时并没有用到,那么就会很浪费资源(堆)

2、 扩展点加载失败,会导致调用方报错,而且这个错误很难定位到时这个原因。

因此Dubbo在使用SPI时,对其做了很多的优化。