Spi简介
此文是为了自己做理解整合使用,简书上我发现了一篇比我这个写得更加全面的文章,请移步深入理解SPI机制
笔者的Jdk版本为1.9,ServiceLoader
有所不同
概念
Jdk 1.6引入了ServiceLoader
,它主要是用来发现和装载一系列的service provider。
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
其包含四个部分:
-
服务提供者接口(Service Provider Interface): 规定了服务提供者的应该提供的方法
-
服务提供者(Service Providers):实现了服务提供者接口的具体实现类,也为真正提供服务的类
-
Spi 配置文件: 一个为了便于查找服务实现的指定文件,文件目录必须为
META-INF/services
,文件名必须为和服务提供者接口的相对路径完全一致。配置文件中的每一行,为一个具体服务提供者的相对路径 -
ServiceLoader: Java Spi主类,用于为服务提供者接口加载服务。
ServiceLoader
中有许多实用的方法,可以用于对应的实现,包括进行迭代或者重新加载服务
Spi Vs Api
- Api 是 类/接口/方法/... 的描述,调用的目的是为了实现某一目标
- Spi是 类/接口/方法/... 的描述,继承和实现的目的是为了实现某一目标
换句话说, Api 告诉你指定的某个类或者方法的作用,而Spi 告诉你必须实现哪些操作才能符合要求
一般来说 Api和Spi是不同的, 举个例子,在JDBC Driver
这个类是 Spi的一种,如果只是想使用JDBC,你不需要直接使用它,但是实现JDBC驱动的每个人都必须实现该类
但是有时候,他们的概念会重合。 Connection
接口既是Spi,又是Api。
示例
首先新建一个项目,项目结构如图所示

- 在
Spi-base
中新建一个接口
public interface SearchSpi {
/**
* 查询某一字段
* @param words
* @return
*/
void searchWords(String words);
}
- 在
Spi-es
中,maven依赖spi-base
,并且实现这个接口
public class EsSearchSpi implements SearchSpi {
@Override
public void searchWords(String words) {
System.out.println("This is es searching " + words);
}
}
并且在resources
目录下新建services
文件夹,在文件夹下建立与 SearchSpi
相对路径同名的文件
文件中的内容名为 具体实现类的相对路径
在此,文件名为com.wesley.SearchSpi
,文件中内容为com.wesley.EsSearchSpi
此处需要注意一个点,maven
中要加上以下配置,否则无法将resources中的文件打包
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
最后在spi-core
中新增启动类
public class App{
public static void main( String[] args )
{
ServiceLoader<SearchSpi> loader = ServiceLoader.load(SearchSpi.class);
Iterator<SearchSpi> spiIterable = loader.iterator();
while(spiIterable.hasNext()){
SearchSpi searchSpi = spiIterable.next();
searchSpi.searchWords("hello world");
}
}
}
启动后,日志为
Connected to the target VM, address: '127.0.0.1:54769', transport: 'socket'
This is es searching hello world
Disconnected from the target VM, address: '127.0.0.1:54769', transport: 'socket'
Process finished with exit code 0
表示Spi被成功装载并调用
这里有个坑,我直接使用 loader.iterator().hasNext()
的时候,不知道为什么一直为true,导致程序一直执行,这个留个爪,看后面能不能查出来
过程分析
1 初始化ServiceLoader
ServiceLoader<SearchSpi> loader = ServiceLoader.load(SearchSpi.class);
如文章开头描述,需要SeviceLoader
进行加载
其调用的方法及作用为
/**
* 用当前线程的的classLoader 生成一个新的ServiceLoader
**/
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
ServiceLoader
的构造方法中,最主要的是以下部分,初始化了ServiceLoader中的一些成员变量
private ServiceLoader(Class<?> caller, Class<S> svc, ClassLoader cl) {
Objects.requireNonNull(svc);
// ....省略部分代码
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时采取的访问控制上下文
2 获取迭代器
Iterator<SearchSpi> spiIterable = loader.iterator();
这条语句,作用为: 首先生成了一个包装了ModuleServicesLookupIterator
和 LazyClassPathLookupIterator
组合成的iterator ,这个iterator并不是loader.iterator
,而是 lookupIterator1
,之后在lookupIterator1
的基础上,加入一些校验后才包装成了loader.iterator
其调用方法如下,已加中文注解
public Iterator<S> iterator() {
// 当 lookupIterator1不存在时,创建吸你的lookUpIterator
if (lookupIterator1 == null) {
// 此处的逻辑为 若layer!= null->LayerLookupIterator
// 其余为 ModuleServicesLookupIterator 和 LazyClassPathLookupIterator组合成的iterator
// 当前程序便是layer == 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;
}
};
}
lookupIterator1 = newLookupIterator();
的具体实现如下
private Iterator<Provider<S>> newLookupIterator() {
assert layer == null || loader == null;
// 当前程序便是layer == 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>>() {
// 即分别判断 ModuleServicesLookupIterator 和 LazyClassPathLookupIterator
@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();
}
}
};
}
}
-
判断是否有下一元素(真正加载Spi)
while(spiIterable.hasNext())
首先看ServiceLoader
中的 ModuleServicesLookupIterator#hasNext
@Override
public boolean hasNext() {
while (nextProvider == null && nextError == null) {
// get next provider to load
while (!iterator.hasNext()) {
if (currentLoader == null) {
return false;
} else {
currentLoader = currentLoader.getParent();
// 入口
//返回一个迭代器,获取当前类加载器的模块中的实现
// 在本程序中为空的迭代器
iterator = iteratorFor(currentLoader);
}
}
// attempt to load provider
ServiceProvider provider = iterator.next();
try {
@SuppressWarnings("unchecked")
Provider<T> next = (Provider<T>) loadProvider(provider);
nextProvider = next;
} catch (ServiceConfigurationError e) {
nextError = e;
}
}
return true;
}
这里有获取到的迭代器是空的,进入下一步
java.util.ServiceLoader.LazyClassPathLookupIterator#hasNext
接下来我会直接在源代码中标明前后顺序,后续会补上时序图(也许)
@Override
public boolean hasNext() {
if (acc == null) {
// 1.进入此处
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
private boolean hasNextService() {
while (nextProvider == null && nextError == null) {
try {
// 2.获取providerClass
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 Class<?> nextProviderClass() {
if (configs == null) {
try {
// PREFIX = /META-INF/services/ service.getName为Spi的相对路径
String fullName = PREFIX + service.getName();
if (loader == null) {
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 {
// 3 用类加载器加载资源 获取到一个2位的数组,数组第0位是Enumeration 第1位是BuiltinClassLoader`
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
// 4 解析URLx信息
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;
}
}
java.lang.ClassLoader#getResources
源代码如下
public Enumeration<URL> getResources(String name) throws IOException {
Objects.requireNonNull(name);
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = BootLoader.findResources(name);
}
// tmp[1]里保留的是BuiltinClassLoader
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
BuiltinClassLoader
是 jdk9 中代替 URLClassLoader
的加载器,是 PlatformClassLoader
与 AppClassLoader
的父类。其继承了 SecureClassLoader
,其核心的方法主要是 loadClassOrNull(...)
方法,内部细节不再讨论,否则过深
java.util.ServiceLoader.LazyClassPathLookupIterator#parse
的截图及运行时信息如下

至此,我们找到了Spi被加载的位置以及解析的位置
剩余细节,不再赘述
Dubbo中的Spi 实现
来源:
Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。
Dubbo 改进了 JDK 标准的 SPI 的以下问题:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过
getName()
获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。 - 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
约定:
在扩展类的 jar 包内,放置扩展点配置文件 META-INF/dubbo/接口全限定名
,内容为:配置名=扩展实现类全限定名
,多个实现类用换行符分隔
从以上的信息,我们可以看到,Dubbo的 Spi基本上参考了Java Spi的思路,使用一个固定路径,使用一个类ExtensionLoader
加载对应的Spi。
Dubbo的Spi原理及过程会另外单开一篇文章进行讲解
参考文章
Java Service Provider Interface

网友评论