SPI 概念
SPI(Service Provider Interface),是一种模块间相互引用的机制,可以用来启用框架和替换组件,一般的流程是服务的提供者在classpath指定配置实现类的全名,由调用方读取和加载使用,调用方无需修改代码,通常以jar包的形式引入需要使用的实现,Dubbo,Soul等项目使用了SPI机制,但给使用者提供了更丰富便捷的选择,可以由用户安优先级,名称等方式选择使用那个实现。
SPI 标准流程:
- 定义标准接口
- 编写不同的实现,配置classpath目录指定位置
- 使用方加载使用
JDK内置的SPI
JDK内置的SPI 机制 ,约定好的classpath目录下META_INFO/services/ 创建一个以服务接口命名的文件,该文件中内容为该接口的实现类(完全名称即包含package名称)通过java.util.ServiceLoader加载指定接口
举个栗子🌰
-
我们定义一个LearnSpi interface
package com.cuicui.bootcamp
public interface LearnSpi {
void readBook();
} -
我们一个school的学习工程打包为school.jar 里面实现类:
package com.cuicui.bootcamp
import com.cuicui.bootcamp.LearnSpi
public class SchoolLearnSpi implements LearnSpi {
public void readBook(){
System.out.println(“School readBook”);
}
}
需要在school工程的classpath目录下META_INFO/services/ 中新建一个名为
com.cuicui.bootcamp. LearnSpi的文件
文件的内容为
com.cuicui.bootcamp.SchoolLearnSpi
- 调用者可以引入school.jar 并通过java.util.ServiceLoader加载获取
ServiceLoader serviceLoader = ServiceLoader.load(LearnSpi.class);
for (LearnSpi LearnSpi : serviceLoader) {
LearnSpi.readBook();
}
// 在终端就会打印 : School readBook
JDK默认的SPI机制在使用的时候我们没有办法获取到我们想要的特定的实现,只能通过for循环一个一个遍历匹配,也没有命名,实际很难使用。
Soul中的spi
Soul 的spi机制,是由Soul-spi这个项目负责实现,并借鉴Dubbo SPI的实现
ExtensionLoader 提供了比JDK ServiceLoader更为强大的功能。
ExtensionLoader
我们这里重点分析ExtensionLoader
类成员有:
// soul spi的扩展目录
private static final String SOUL_DIRECTORY = "META-INF/soul/";
// 不同扩展接口对应的ExtensionLoader的缓存类,静态成员
private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
// 扩展接口类
private final Class<T> clazz;
// 自定的Holder 类型,cachedClasses 用于存储不同扩展对应的实现类们
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
// key 是class 名称扩展类对应的instance缓存
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// key 是Class对象 扩展的instance
private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
// 每一种扩展对应的default扩展名称,loadclass时候会判断注解的value是否为空获得
private String cachedDefaultName;
如果单纯的看Soul里面的实现,cachedInstances 和joinInstances这可能会让人产生困惑,为什么要加两个缓存??,这里就要溯源去看dubbo里面的实现了,joinInstances里面是一对一的 value不会重复,cachedInstances里面不同的name,value是可以相同的 目前soul里面应该还没有利用起来????。
ExtensionLoader 类中维护的ConcurrentHashMap类型的 LOADERS 会为每个SPI 扩展都维护一个ExtensionLoader instance使用者通过静态方法getExtensionLoader来lazyload
public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
if (clazz == null) {
throw new NullPointerException("extension clazz is null");
}
if (!clazz.isInterface()) {
throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
}
// 必须显示声明
if (!clazz.isAnnotationPresent(SPI.class)) {
throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + " Annotation");
}
ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
if (extensionLoader != null) {
return extensionLoader;
}
LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz));
return (ExtensionLoader<T>) LOADERS.get(clazz);
}
如果没有会通过私有的构造方法实例化一个ExtensionLoader。
private ExtensionLoader(final Class<T> clazz) {
this.clazz = clazz;
if (clazz != ExtensionFactory.class) {
ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClasses();
}
}
实例化一个ExtensionLoader的同时也会加载实现该扩展的类,即通过getExtensionCLasses方法
具体的流程是:
loadExtensionClass -> loadDirectory -> 循环调用 loadResources -> 循环调用 loadClass
这里需要注意的一点是在loadDirectory函数中!
Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fileName)
: ClassLoader.getSystemResources(fileName);
// 这里使用了getResources 和getSystemResources 而不是getResource
if (urls != null) {
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
loadResources(classes, url);
}
}
Soul SPI使用分析
以Divide插件为例子,在选择负载均衡器的时候便是通过SPI的机制拿到了对应算法的LoadBalance。
public static DivideUpstream selector(final List<DivideUpstream> upstreamList, final String algorithm, final String ip) {
LoadBalance loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getJoin(algorithm);
return loadBalance.select(upstreamList, ip);
}