JDK SPI 机制原理分析
最近开始看
Dubbo
源码, 而Dubbo
的一大优秀设计就是Dubbo SPI 机制
, 而Dubbo
的 SPI 是对 JDK 的 SPI 的增强, 所以先对JDK SPI 机制
准备做一个分析.同时也建议大家多读优秀框架的源码.
1 SPI 简介
1.1 什么是 SPI
SPI全称 Service Provider Interface ,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI 的作用就是为这些被扩展的 API 寻找服务实现。
1.2 SPI和API的使用场景
- API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
- SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
2 JDK SPI 快速上手
先从
HelloWorld级
的代码入手, 让大家体验一下SPI
机制.
2.1 定义接口 Action
(名字随意, 我这里就叫 Action 了)
package com.inetsoft.spi;
public interface Action {
String doAction(String name) throws Exception;
}
2.2 为接口添加两个实现--- UploadAction
和 DownloadAction
- UploadAction
package com.inetsoft.spi.impl;
import com.inetsoft.spi.Action;
public class UploadAction implements Action {
public UploadAction() {
System.out.println("Init UploadAction...");
}
@Override
public String doAction(String name) {
return "Upload " + name;
}
}
- DownloadAction
package com.inetsoft.spi.impl;
import com.inetsoft.spi.Action;
public class DownloadAction implements Action {
@Override
public String doAction(String name) throws Exception {
if(name == null) {
throw new Exception("Unsupport action for " + name);
}
return "Download " + name;
}
}
2.3 添加 SPI 描述文件
在resources目录下新建
META-INF/services
目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名
2.4 通过 ServiceLoader
加载实现类并调用接口方法
import com.inetsoft.spi.Action;
import java.util.Iterator;
import java.util.ServiceLoader;
public class App {
public static void main(String[] args) throws Exception {
// 创建 ServiceLoader 对象, ServiceLoader 实现了 Iterable, 因此可迭代遍历
ServiceLoader<Action> actions = ServiceLoader.load(Action.class);
// 获取 ServiceLoader 的迭代器
Iterator<Action> iterator = actions.iterator();
while(iterator.hasNext()) {
// next 方法中会根据配置文件对接口实现类进行实例化(反射: newInstance)
Action action = iterator.next();
// 调用具体方法
System.out.println(action.doAction("JDK SPI"));
}
}
}
到这里一个简单的 JDK SPI 的 HelloWorld 就完成了, 可以看到其中最为核心的就是通过
ServiceLoader
的load
方法. 下面原理分析我们就一起从这里入手分析一下 JDK SPI 机制的原理
3 JDK SPI 机制的原理
精髓都在代码中进行了注释, 而且代码顺序帅帅已经调整好, 也将不相关的代码移除了, 因此大家可以自上而下将下面代码读一遍就可以了.
上面其实也已经说过了, JDK SPI 机制的原理我们可以从
ServiceLoader
的load
方法入手.
// 配置文件所在目录
private static final String PREFIX = "META-INF/services/";
// 创建ServiceLoader时获得的访问控制上下文
private final AccessControlContext acc;
// 缓存的提供者,按实例化顺序排序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 惰性查找迭代器, 用于扫描 SPI 配置文件(包括所有引用的 jar 中的) ----> 只有调用迭代器的 Next 方法时才会去创建实现类对象(反射)
private LazyIterator lookupIterator;
// ...
// SPI 入口方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 上下文类加载器 ----> 为什么获取当前线程的 ContextClassLoader ? -----> 这牵扯到 JVM 的类加载机制, 后面会单独拿出来说, 核心就在 JVM 类加载机制的 <<双亲委派模型>>
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 调用重载方法
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 创建 ServiceLoader 对象, 核心在构造
return new ServiceLoader<>(service, loader);
}
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;
// reload 实例
reload();
}
public void reload() {
// 先清空缓存的, 第一次访问本身就为空
providers.clear();
// 创建惰性加载迭代器
lookupIterator = new LazyIterator(service, loader);
// 看到这里, 实际上 ServiceLoader.load 方法就已经执行完了, 所以可以发现实现类对象还没有在这个过程中创建
}
// 内部类 ---- 惰性加载迭代器
private class LazyIterator
implements Iterator<S>
{
// 接口对象
Class<S> service;
// 类加载器
ClassLoader loader;
// 将配置文件信息封装为 URL 对象的迭代器, 是一个 CompoundEnumeration 对象
Enumeration<URL> configs = null;
// 循环解析配置文件的迭代器, 辅助遍历 configs/CompoundEnumeration
Iterator<String> pending = null;
// 下一个待遍历元素的名称
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
// 迭代去判断是否还有元素
private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 第一次访问 configs 为空
if (configs == null) {
try {
// 获取配置文件的路径: META-INF/services/com.inetsoft.spi.Action
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 类加载不为空, 通过 ClassLoader 将配置文件信息封装为 CompoundEnumeration<URL> 枚举
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 通过 pending 对象遍历 configs
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 将配置文件信息解析到 pending ---> 这里会通过 utf-8 的格式读 META-INF/services/com.inetsoft.spi.Action
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 subtype");
}
try {
// 反射创建类对象
S p = service.cast(c.newInstance());
// 缓存实例对象 ---> reload 会清空
providers.put(cn, p);
// 返回实例对象
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
// ...
}
- URL 对象
- 基本流程图
4. Java SPI 的弊端
从上面的java spi的原理中可以了解到,java的spi机制有着如下的弊端:
- 只能遍历所有的实现,并全部实例化。
- 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
- 扩展如果依赖其他的扩展,做不到自动注入和装配。
- 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持。
5. 预览 Dubbo SPI 机制
正如文章开头所说,
Dubbo SPI
从 JDK 标准的 SPI 扩展点发现机制加强而来, 因此我们后边会单独拿出来一篇文章来说, 这里先进行一个简单的说明. 下面拿出Dubbo
官网对于Dubbo SPI
的介绍