SPI 机制是实现可扩展性的一种方式。
Dubbo SPI
是从JDK SPI
(Service Provider Interface)扩展点发现机制加强而来。本文首先分析JDK SPI
的使用姿势,再分析其基本原理,在下一章再分析Dubbo SPI
的相关使用和基本原理。
一、作用
- 为接口自动寻找实现类。
二、实现方式
- 标准制定者制定接口(eg. JDK 制定 SPI 接口:
java.sql.Driver
) - 不同厂商编写针对于该接口的实现类,并在jar的
classpath:META-INF/services/全接口名称
文件中指定相应的实现类全类名(eg.mysql-connector-java
实现了java.sql.Driver
这个 SPI 接口,实现类是com.mysql.cj.jdbc.Driver
)
- 开发者直接引入相应的 jar,就可以实现为接口自动寻找实现类的功能
三、使用方法
标准接口 io.study.jdk.spi.Log
package io.study.jdk.spi;
/**
* SPI 接口
*/
public interface Log {
void execute();
}
具体实现1 io.study.jdk.spi.Log4j
package io.study.jdk.spi;
/**
* Log4j 实现类
*/
public class Log4j implements Log {
@Override
public void execute() {
System.out.println("this is log4j!");
}
}
具体实现2 io.study.jdk.spi.Logback
package io.study.jdk.spi;
/**
* Logback 实现类
*/
public class Logback implements Log {
@Override
public void execute() {
System.out.println("this is logback!");
}
}
配置文件 classpath:META-INF/services/io.study.jdk.spi.Log
io.study.jdk.spi.Logback
- 这里指定了实现类 Logback,那么加载的时候就会自动为 Log 接口指定实现类为 Logback。
- 这里也可以指定两个实现类,那么在实际中使用哪一个实现类,就需要使用额外的手段来控制。
加载实现主类 io.study.jdk.Test
public class Test {
public static void main(String[] args) {
// 1. 获取 ServiceLoader
ServiceLoader<Log> loader = ServiceLoader.load(Log.class);
// 2. 获取迭代器
Iterator<Log> iterator = loader.iterator();
// 3. 迭代读取实现类 调用 hasNext 方法的时候会去加载配置文件进行解析
while (iterator.hasNext()) {
// 调用 next 方法的时候进行实例化并缓存
Log log = iterator.next();
// 4. 执行实现类
log.execute();
}
}
}
注意
不是实例化 ServiceLoader 以后,就去读取配置文件中的具体实现,并进行实例化的。而是等到使用迭代器去遍历的时候,
调用 hasNext 方法的时候会去加载配置文件进行解析
,调用 next 方法的时候进行实例化并缓存
- 即懒加载
。具体见“源码分析”。
四、源码解析
1、获取ServiceLoader
ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);
首先来看一下 ServiceLoader 的重要属性:
// 定义实现类的接口文件所在的目录
private static final String PREFIX = "META-INF/services/";
// SPI 接口
private final Class<S> service;
// 定位、加载、实例化实现类
private final ClassLoader loader;
// 以初始化的顺序缓存 <接口全名称, 实现类实例>
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 真正进行迭代的迭代器
private LazyIterator lookupIterator;
其中 LazyIterator
是 ServiceLoader 的一个内部类,在迭代部分会说。
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;
...
reload();
}
public void reload() {
// 清空 SPI 接口和实现类缓存
providers.clear();
// 创建 LazyIterator 实例
lookupIterator = new LazyIterator(service, loader);
}
这样一个 ServiceLoader 实例就创建成功了。在创建的过程中,我们看到还实例化了一个LazyIterator,该类下边会说。
2、获取迭代器并迭代
// 2. 获取迭代器
Iterator<Log> iterator = loader.iterator();
// 3. 迭代读取实现类 调用 hasNext 方法的时候会去加载配置文件进行解析 + 存储实现类名
while (iterator.hasNext()) {
// 调用 next 方法的时候进行实例化并缓存
Log log = iterator.next();
// 4. 执行实现类
log.execute();
}
外层迭代器:
public Iterator<S> iterator() {
// 创建返回 Iterator 实例
return new Iterator<S>() {
// 获取 providers 缓存的迭代器
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
// 获取是否有下一个 SPI 实现类
public boolean hasNext() {
// 首先从 providers 缓存中获取,如果缓存中存在,则直接返回 true
if (knownProviders.hasNext())
return true;
// 如果缓存中不存在,则需要使用在创建 ServiceLoader 时创建的 LazyIterator 来进行查找
return lookupIterator.hasNext();
}
// 获取下一个 SPI 实现类
public S next() {
// 如果 providers 缓存中有下一个 SPI 实现类的元素,则直接返回下一个 SPI 实现类
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 如果缓存中不存在,则需要使用在创建 ServiceLoader 时创建的 LazyIterator 来进行查找
return lookupIterator.next();
}
...
};
}
从查找过程hasNext()和迭代过程next()来看。
- hasNext():先从 provider(
缓存
)中查找,如果有,直接返回 true;如果没有,通过LazyIterator
来进行查找。 - next():先从 provider(
缓存
)中直接获取,如果有,直接返回实现类对象实例;如果没有,通过LazyIterator
来进行获取。
下面来看一下,LazyIterator
这个类。首先看一下他的属性:
Class<S> service; // SPI 接口
ClassLoader loader; // 类加载器
Enumeration<URL> configs = null; // 存放配置文件(可能会有多个)
Iterator<String> pending = null; // 存放配置文件中的内容,并存储为 ArrayList,即存储多个实现类名称
String nextName = null; // 当前处理的实现类名称
其中,service 和 loader 在上述实例化 ServiceLoader 的时候就已经实例化好了。
下面看一下 hasNext():
public boolean hasNext() {
...
return hasNextService();
...
}
private boolean hasNextService() {
// 如果当前正在处理的实现类存在,直接返回 true
if (nextName != null) {
return true;
}
// 如果配置文件为 null,加载配置文件
if (configs == null) {
...
// 加载配置文件 META-INF/services/io.study.jdk.spi.Log
String fullName = PREFIX + service.getName();
...
configs = loader.getResources(fullName);
...
}
// 如果 pending 为 null 或者 pending 中没有下一个元素了
while ((pending == null) || !pending.hasNext()) {
// 则检测加载的多个配置文件 configs 中是否还有元素,如果没有了,直接返回false;如果有,获取下一个元素(即下一个文件 URL),执行解析操作
if (!configs.hasMoreElements()) {
return false;
}
// 将 configs.nextElement() 这个文件中的每一行(即每一个 SPI 实现类的全类名添加到 ArrayList 中,并返回该 ArrayList 的迭代器)
pending = parse(service, configs.nextElement());
}
// 设置当前获取的元素名
nextName = pending.next();
return true;
}
hasNextService()中,核心实现
如下:
- 首先使用 loader 加载 SPI 配置文件,此时找到了
META-INF/services/io.study.jdk.spi.Log
文件;- 然后解析这个配置文件,并将各个实现类名称存储在 pending 这个迭代器所代表的 ArrayList 中;此时列表为 [ io.study.jdk.spi.Logback ]
- 最后指定当前获取的 nextName;此时
String nextName = "io.study.jdk.spi.Logback"
下面看一下next():
public S next() {
...
return nextService();
...
}
private S nextService() {
// 如果已经没有元素了,直接抛错
if (!hasNextService())
throw new NoSuchElementException();
// 获取要获取的 SPI 实现全类名 nextName(该属性在 hasNextService() 方法中进行了初始化)
String cn = nextName;
nextName = null;
...
// 加载 SPI 实现类为 Class
Class<?> c = Class.forName(cn, false, loader);
....
// 检测 c(SPI 实现类)是不是 service(SPI 接口)的实现类,如果不是,直接抛错
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
...
// 创建 SPI 实现类实例,之后向上转型为 SPI 接口类型
S p = service.cast(c.newInstance());
// 将 <SPI 接口名,SPI 实现类实例> 存储在 providers 缓存中
providers.put(cn, p);
// 最后返回 SPI 实现类实例
return p;
...
}
nextService()中,核心实现
如下:
- 首先加载 nextName(要获取的 SPI 实现全类名,该属性在 hasNextService() 方法中进行了初始化) 代表的类 Class,这里为io.study.jdk.spi.Logback;
- 之后创建该类的实例,并转型为所需的接口类型
- 最后存储在 provider 缓存中,供后续查找,最后返回转型后的实现类实例。
在 next() 之后,拿到实现类实例后,就可以执行其具体的方法了。
五、JDK SPI 的缺点
dubbo 官网:扩展点配置
- JDK 标准的 SPI 会一次性
实例化
扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。(Dubbo 使用 key = value 的形式,可以方便的实现按需实例化
,eg. 根据配置的 key="dubbo" 直接获取到 value=DubboProtocol 实例)- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。(
疑问
)- 缺乏对扩展点 IOC 和 AOP 的支持,一个扩展点无法直接 setter 注入其它扩展点。