SPI机制是Dubbo框架的基础知识,学习Dubbo框架之前有必要深入理解SPI机制。下面对SPI的概念,作用,使用以及原理作一个深入的介绍。
1.SPI是什么?SPI有什么用?
SPI的全称是Service Provider Interface,可以翻译为“提供服务接口”;举一个实际场景:模块A提供一个Service接口,模块B和模块C分别实现该Service接口,那么模块A如何在无感知的情况下找到模块B和模块C中Service接口的实现类? SPI机制能解决这个问题。
很明显,如果没有SPI机制,那么模块B和模块C对Service接口的实现类必定需要硬编码在模块A中,这就不符合“开闭原则”。所以SPI机制可以帮助模块A自动寻找Service接口在其他jar包中的实现,简单来说就是寻找接口的服务实现。
2.SPI如何使用?
下面具体用代码演示SPI机制。
第一步:新建Maven项目ModelA。
ModelA项目只提供服务接口SPIService,没有实现。
package com.starry.service;
public interface SPIService {
String spiService();
}
同时通过maven install
命令打包并上传至本地仓库,便于其他模块依赖。
第二步:新建Maven项目ModelB。
ModelB项目依赖ModelA的jar包,并且实现SPIService接口。
package com.starry.service.impl;
import com.starry.service.SPIService;
public class ModelBSPIServiceImpl implements SPIService {
@Override
public String spiService(){
return "model B spiService...";
}
}
与此同时,在resources目录下新建名为META-INF/services的package,并新建名为“com.starry.service.SPIService”的文件,文件内容则为“com.starry.service.impl.ModelBSPIServiceImpl”,即实现类的类路径名。
项目目录结构如下图所示:
通过maven install命令对ModelB项目打包并且上传至本地仓库,供ModelA模块依赖使用。
第三步:新建Maven项目ModelC。
ModelC项目依赖ModelA的jar包,并且实现SPIService接口。
package com.starry.service.impl;
import com.starry.service.SPIService;
public class ModelCSPIServiceImpl implements SPIService {
@Override
public String spiService(){
return "model c spiService...";
}
}
同样在resources目录下新建名为META-INF/services的package,并新建名为“com.starry.service.SPIService”的文件,文件内容则为“com.starry.service.impl.ModelCSPIServiceImpl”,即实现类的类路径名。
项目的目录结构如下图所示:
通过maven install命令对ModelC项目打包并且上传至本地仓库,供ModelA模块依赖使用。
第四步:ModelA模块依赖ModelB和ModelC,并且寻找到SPIService接口的实现。
ModeA模块的pom.xml增加依赖:
<dependencies>
<dependency>
<groupId>Model-B</groupId>
<artifactId>Model-B</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>Model-C</groupId>
<artifactId>Model-C</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
测试类:
package com.starry.demo;
import com.starry.service.SPIService;
import java.util.Iterator;
import java.util.ServiceLoader;
public class Client {
public static void main(String[] args) {
ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class);
Iterator<SPIService> serviceIterator = loader.iterator();
while(serviceIterator.hasNext()) {
SPIService service = serviceIterator.next();
System.out.println(service.spiService());
}
}
}
很明显,发现服务实现类的重要工具类是 ServiceLoader,表面现象是通过load(SPIService.class)方法获取服务接口的实现类,然后通过iterator()方法枚举服务实现类。
这里我们不禁会想以下两个问题:
- ServiceLoader类如何发现服务接口的实现类?
- ServiceLoader类是如何对接口实现类进行加载并且实例化的?
带着这两个问题,深入分析ServiceLoader类的源码。
3.SPI原理分析
针对Client类中的代码深入分析。
步骤1:
ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class);
代码表面意思是通过ServiceLoader类的load方法返回SPIService接口的加载器;
调用链路:
load(Class)-load(Class,ClassLoader)-ServiceLoader(Class,ClassLoader)-reload()
整条链路完成下面三件事:
- a. 生成SPIService接口的类加载器serviceLoader对象;
- b. 清理providers的内容;providers是一个有顺序的映射,作用是缓存服务实现的对象;此处重新生成服务类的加载器会清理缓存。
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
- c. 生成LazyIterator类的对象lookupIterator;LazyIterator类是延迟迭代器,顾名思义迭代器的内容只有在迭代过程中才会产生;主要逻辑在这个类里面;
步骤2:
Iterator<SPIService> serviceIterator = loader.iterator();
基于步骤1产生的服务加载器loader对象产生迭代对象serviceIterator。
方法内部返回一个iterator对象,主要实现hasNext()和next()方法。注意步骤1中providers对象刚刚被clear掉,所以此刻knownProviders对象也为空。
步骤3:
while(serviceIterator.hasNext())
方法调用链比较长,避免混乱,先给出整体调用链和方法调用结果:
Iterator.hasNext()-LazyIterator.hasNext()-LazyIterator.hasNextService()-lazyIterator.parse(Class,URL)
方法主要完成以下几件事:
- a. 根据约定(PREFIX常量值)和服务接口的路径名组成文件资源的路径fileName;
- b. 通过loader加载器获取fileName路径下的文件资源configs;
- c. 按行读取configs对象的字符流,并存储在迭代器pending内;
- d. 循环获取迭代器pending的内容,即服务实现类的类路径名;
感兴趣的可以看一下下面的具体调用过程,感觉枯燥的可以直接看步骤4。
很明显,当providers对象为空时,主要的逻辑都由lookupIterator对象完成。因此调用LazyIterator类的hashNext()方法。
此时acc对象为null,调用LazyIterator类的hasNextService();该方法是核心方法,单独拧出来分析一下。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX = “META-INF/services/”
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 类加载器获取jar包指定路径下的文件资源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 按行读取configs文件资源内的内容,并且作为String放置在迭代器pending中
pending = parse(service, configs.nextElement());
}
// 获取迭代器中内容
nextName = pending.next();
return true;
}
看到PREFIX常量的值(META-INF/services/)就很熟悉,方法会把PREFIX和接口的路径名组合成一个文件资源路径fullName,通过类加载的getResources()方法去获取该路径下的资源文件,然后通过parse(Class,URL)获取文件资源内的内容,我们知道其内容为服务实现类路径名;所以既然能够自动获取到实现类的路径名,那么就可以利用反射生成服务实现类的对象;一切都是那么的顺其自然。
步骤4:
SPIService service = serviceIterator.next();
分析这么久,该方法终于获取到服务实现类的对象了。方法调用链:
Iterator.next()-LazyIterator.next()-LazyIterator.nextService()
具体过程如下:
目前为止,providers依旧为空,所以调用lookupIterator对象的next()方法。
调用nextService()方法,该方法也比较核心,所以单独拧出来分析。
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName; // nextName为pending迭代器中的第一个内容,即遍历的首个服务实现类名
nextName = null;
Class<?> c = null;
try {
// 通过反射获取该服务实现类的class对象
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());
// 以类名为k,以对象为v,放入map;放入本地缓存供后面使用
providers.put(cn, p);
return p; // 返回服务实现类的对象
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
方法实现生成服务实现类对象以及把对象缓存到本地内存两大功能。
步骤5:调用对象的方法。
4.总结
到这里整个调用的过程分析完毕,可以总结下SPI的机制:SPI机制有一个约定,即模块B和模块C实现模块A提供的Service接口,那么模块B和模块C在其jar包的META-INF/services/目录下创建一个以服务接口命名的文件,该文件内容就是实现该接口的实现类的路径名;当模块A依赖模块B和模块C时,就能通过jar包META-INF/services/目录下的文件找到实现类的类名,然后通过反射实现类的装载以及实例化,并完成模块的注入。通过这种寻找接口实现类的机制,可以实现模块间的解耦。
SPI是一个很巧妙的设计,模块A在毫无感知的情况下能够获取模块B和模块C对SPIService接口的实现类对象,而这仅仅依靠一个“约定”,即扫描META-INF/services/目录下的文件,文件名则为服务接口的路径名,文件内容放置实现类的类路径名即可。“约定大于配置”的思想,在Spring框架中也有很强的体现。但SPI也有缺点,无法精准获取服务实现类的对象
,无法控制服务实现类实例化
以及SPI的非线程安全
等;针对这些问题,Dubbo在SPI的基础上改善后得以解决。
参考资料:
http://www.spring4all.com/article/260
https://www.jianshu.com/p/46b42f7f593c