这篇文章内容很干,做好心理准备!本文详细讲解了 Dubbo SPI 诞生原因以及它的用法,并且详细解读了核心类 ExtensionLoader 的关键属性,再根据demo 对 SPI 的加载原理进行详细解读。
文章较长,建议收藏!文末有原理图。
点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达
我们接着上次的文章讲,既然已经有了 JDK SPI 为什么还需要 Dubbo SPI 呢?
技术的出现通常都是为了解决现有问题,通过之前的 demo,不难发现 JDK SPI 机制就存在以下一些问题:
实现类会被全部遍历并且实例化,假如我们只需要使用其中的一个实现,这在实现类很多的情况下无疑是对机器资源巨大的浪费,
无法按需获取实现类,不够灵活,我们需要遍历一遍所有实现类才能找到指定实现。
Dubbo SPI 以 JDK SPI 为参考做出了改进设计,进行了性能优化以及功能增强,Dubbo SPI 机制的出现解决了上述问题。除此之外,Dubbo 的 SPI 还支持自适应扩展以及 IOC 和 AOP 等高级特性。
JDK SPI 原理,请移步这篇文章:
提到 Dubbo,为什么一定要谈SPI?|原创
Dubbo SPI 的配置做出了改进,在 Dubbo 中有三种不同的目录可以存放 SPI 配置,用途也不同。
META-INF/services/ 目录:此目录配置文件用于兼容 JDK SPI 。
META-INF/dubbo/ 目录:此目录用于存放用户自定义 SPI 配置文件。
META-INF/dubbo/internal/ 目录:此目录用于存放 Dubbo 内部使用的 SPI 配置文件。
并且配置文件中可以配置成Key-Value 形式,key 为扩展名,value 为具体实现类。有了扩展名就可以动态根据名字来定位到具体的实现类,并且可以针对性的实例化需要的实现类。比如指定 zookeeper,就知道要使用 zookeeper 作为注册中心的实现。
zookeeper=org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory
Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。
除了对应目录的配置,还需要 @SPI 注解的配合,被修饰的接口表示这是一个扩展接口,@SPI
的 value 表示这个扩展接口的默认扩展实现的扩展名,扩展名就是配置文件中对应实现类配置信息的 key。
//dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
@SPI("dubbo")
public interface Protocol {
}
除此之外,还需要 @Adaptive注解配合,被他修饰的类会生成Dubbo的适配器,Adaptive注解比较复杂,关于这个注解我们后续文章会再详细讲解。
之前的文章我们提到过,dubbo-common 模块中的 extension 包包含了 Dubbo SPI 的逻辑,而ExtensionLoader 就位于其中,Dubbo SPI 的核心逻辑几乎都封装在 ExtensionLoader 这个类里,其地位你可以类比为 JDK SPI 中的 java.util.ServiceLoader。
学习 ExtensionLoader 首先了解以下几个关键属性。
1、EXTENSION_LOADERS
// 扩展接口和ExtensionLoader的关系
private static final ConcurrentMap, ExtensionLoader>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
这个字段保存了扩展接口和ExtensionLoader之间的映射关系,一个扩展接口对应一个ExtensionLoader。key 为接口,value 为 ExtensionLoader。
2、EXTENSION_INSTANCES
//扩展接口实现类和实例对象的关系
private static final ConcurrentMap, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);
这个字段保存了扩展接口扩展实现类和实例对象之间的关系,key 为扩展实现类,value为实例对象。
3、strategies 加载策略
//加载策略
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
import static java.util.ServiceLoader.load;
private static LoadingStrategy[] loadLoadingStrategies() {return stream(load(LoadingStrategy.class).spliterator(), false).sorted().toArray(LoadingStrategy[]::new);
}
strategies 对应的是Dubbo内部实现的三个加载策略,分别对应之前提到的三个不同的SPI配置目录,接口的继承关系如下。他们的实现中无非就是包含了两个信息:加载目录和优先级。
每个实现类都继承了优先级接口Prioritized
,所以不同加载策略的加载优先级不同,对应的优先级是:
DubboInternalLoadingStrategy
大于>DubboLoadingStrategy
大于>ServicesLoadingStrateg
对应目录的优先级分别是:
META-INF/dubbo/internal/
>META-INF/dubbo/
>META-INF/services/
LoadingStrategy是如何加载的?
需要注意的是,LoadingStrategy
的控制了Dubbo内部实现的加载策略,那他自身又是如何加载的呢?
其实根据上面的代码就可以发现LoadingStrategy
正是依赖 JDK SPI 机制来加载的,在loadLoadingStrategies()
方法中调用了ServiceLoader.load()
方法,从META-INF/services
文件夹下的org.apache.dubbo.common.extension.LoadingStrategy
文件中读取到了具体实现类并且依次加载。所以一定程度上来说,Dubbo SPI 机制正是依赖于 JDK SPI 机制。
1、type:与当前 ExtensionLoader 绑定的扩展接口类型。
2、cachedNames:存储了扩展实现类和扩展名的关系,key 为扩展实现类,value 为对应扩展名。
3、cachedClasses:存储了扩展名和扩展实现类之间的关系。key 为扩展名,value 为扩展实现类,这个 map 可以和 cachedNames 对应着理解,他们存贮内容是相反的关系,算是一种空间换时间的技巧。
4、cachedInstances:存储了扩展实现类类名与实现类的实例对象之间的关系,key 为扩展实现名,value 为实例对象。
5、cachedDefaultName:@SPI 注解配置的默认扩展名。
//扩展接口类型
private final Class> type;
//存储了扩展实现类和扩展名的关系
private final ConcurrentMap, String> cachedNames = new ConcurrentHashMap<>();
//存储了扩展名和扩展实现类之间的关系
private final Holder
为了方便调试并且抓住核心原理,其实可以跟 JDK SPI 一样写一个 demo,我们直接复用上次的扩展接口和实现类,但是这次是用Dubbo SPI 的形式加载。配置文件需要重新写一份,写成key-value形式,并且放在META-INF/dubbo
文件夹下。
/*** @author 后端开发技术*/
@SPI
public interface MySPI {void say();
}
public class HelloMySPI implements MySPI{@Overridepublic void say() {System.out.println("HelloMySPI say:hello");}
}
public class GoodbyeMySPI implements MySPI {@Overridepublic void say() {System.out.println("GoodbyeMySPI say:Goodbye");}
}
public static void main(String[] args) {ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(MySPI.class);MySPI hello = extensionLoader.getExtension("hello");hello.say();MySPI goodbye = extensionLoader.getExtension("goodbye");goodbye.say();
}
//配置文件 META-INF/dubbo/org.daley.spi.demo.MySPI
goodbye=org.daley.spi.demo.GoodbyeMySPI
hello=org.daley.spi.demo.HelloMySPI// 输出结果
HelloMySPI say:hello
GoodbyeMySPI say:Goodbye
一定记得给接口标记@SPI注解,否则会抛出以下错误。
Exception in thread "main" java.lang.IllegalArgumentException: Extension type (interface org.daley.spi.demo.MySPI) is not an extension, because it is NOT annotated with @SPI!
在上述 demo 中,我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。
在调用getExtensionLoader()
时会首先尝试从EXTENSION_LOADERS
中根据扩展点类型获得ExtensionLoader
,如果未命中缓存,则会新创建一个ExtensionLoader
,然后返回。
public static ExtensionLoader getExtensionLoader(Class type) {……省略校验逻辑ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);if (loader == null) {EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);}return loader;
}
在ExtensionLoader
的构造方法中,会绑定扩展点接口类型,并且会绑定扩展对象工厂ExtensionFactory objectFactory
,这也是一个扩展点,为了避免越套越深,这里不做深入追踪。
private ExtensionLoader(Class> type) {this.type = type;objectFactory =(type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
创建好ExtensionLoader
之后,便可以调用getExtension()
方法。核心在于获得扩展实现,首先检查缓存,缓存未命中则创建扩展对象,然后设置到 holder 中。
public T getExtension(String name) {return getExtension(name, true);
}public T getExtension(String name, boolean wrap) {if (StringUtils.isEmpty(name)) {throw new IllegalArgumentException("Extension name == null");}if ("true".equals(name)) {// 获取默认的扩展实现类return getDefaultExtension();}//用于持有目标对象final Holder
创建扩展实例方法createExtension()
主要包含以下逻辑:
通过 getExtensionClasses()
获取所有的拓展类
通过反射创建拓展对象
向拓展对象中注入依赖(可以想象到,这里存在依赖其他SPI扩展点的情况,递归注入)
将拓展对象包裹在相应的 Wrapper 对象中
如果实现了Lifecycle
接口,执行生命周期的初始化方法
步骤 1 是加载拓展类的关键,步骤 3 和 4 是 Dubbo IOC 与 AOP 的具体实现,注意 IOC 和 AOP 是一种思想,别和 Spring 的 IOC、AOP 混淆。
private T createExtension(String name, boolean wrap) {//关键代码!从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表Class> clazz = getExtensionClasses().get(name);if (clazz == null || unacceptableExceptions.contains(name)) {throw findException(name);}try {// 通过反射创建实例T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);}// 向实例中注入依赖injectExtension(instance);if (wrap) {List> wrapperClassesList = new ArrayList<>();if (cachedWrapperClasses != null) {wrapperClassesList.addAll(cachedWrapperClasses);wrapperClassesList.sort(WrapperComparator.COMPARATOR);Collections.reverse(wrapperClassesList);}if (CollectionUtils.isNotEmpty(wrapperClassesList)) {// 循环创建 Wrapper 实例for (Class> wrapperClass : wrapperClassesList) {Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);if (wrapper == null|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}}}}//执行生命周期初始化方法 initialize()initExtension(instance);return instance;} catch (Throwable t) {……}
}
我们在根据名称获得指定拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射Map(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系 Map 中根据 name 取出相应的拓展类即可。
在获得全部扩展名与扩展类关系classes
时,如果还未创建,会有个双重检查的过程以防止并发加载。通过检查后会调用 loadExtensionClasses()
加载扩展类。相关过程的代码分析如下:
private Map> getExtensionClasses() {// 从缓存中获取已加载的拓展类Map> classes = cachedClasses.get();// DBC 双重检查if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {// 加载所有拓展类逻辑classes = loadExtensionClasses();cachedClasses.set(classes);}}}return classes;
}
loadExtensionClasses()
主要做了两件事,一是解析@SPI注解设置默认扩展名,二是在这里会扫描 Dubbo 那三个配置文件夹下的所有配置文件,具体注释如下。
private Map> loadExtensionClasses() {cacheDefaultExtensionName();Map> extensionClasses = new HashMap<>();// 从三种加载策略中获得扩展实现类,对应加载指定文件夹下的配置文件,这里的加载策略已经根据优先级排好了顺序,数字越小越优先for (LoadingStrategy strategy : strategies) {loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),strategy.overridden(), strategy.excludedPackages());// 此处为了兼容阿里巴巴到Apache的版本过度loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());}return extensionClasses;
}// 设置默认扩展名
private void cacheDefaultExtensionName() {// 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的final SPI defaultAnnotation = type.getAnnotation(SPI.class);if (defaultAnnotation == null) {return;}//从@SPI注解获得默认实现扩展实现名String value = defaultAnnotation.value();if ((value = value.trim()).length() > 0) {// 对 SPI 注解内容进行切分String[] names = NAME_SEPARATOR.split(value);// 检测 SPI 注解内容是否合法,不合法则抛出异常if (names.length > 1) {throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()+ ": " + Arrays.toString(names));}if (names.length == 1) {// 设置默认名称,参考 getDefaultExtension 方法cachedDefaultName = names[0];}}
}
可以看出,关键的逻辑应该在loadDirectory()
方法中。loadDirectory()
方法先通过 classLoader
获取所有资源链接,然后再通过 loadResource
方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。
private void loadResource(Map> extensionClasses, ClassLoader classLoader,java.net.URL resourceURL, boolean overridden, String... excludedPackages) {try {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {String line;String clazz = null;// 按行读取配置内容while ((line = reader.readLine()) != null) {// 定位 # 字符final int ci = line.indexOf('#');if (ci >= 0) {// 截取 # 之前的字符串,# 之后的内容为注释,需要忽略line = line.substring(0, ci);}line = line.trim();if (line.length() > 0) {try {String name = null;// 以等于号 = 为界,截取键与值int i = line.indexOf('=');if (i > 0) {name = line.substring(0, i).trim();clazz = line.substring(i + 1).trim();} else {clazz = line;}if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {// 加载类,并通过 loadClass 方法对类进行缓存loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);}} catch (Throwable t) {IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +", cause: " + t.getMessage(), t);exceptions.put(line, e);}}}}} catch (Throwable t) {logger.error("Exception occurred when loading extension class (interface: " +type + ", class file: " + resourceURL + ") in " + resourceURL, t);}
}
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法用于主要用于操作缓存,该方法的逻辑如下:
private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class> clazz, String name,boolean overridden) throws NoSuchMethodException {if (!type.isAssignableFrom(clazz)) {throw new IllegalStateException("Error occurred when loading extension class (interface: " +type + ", class line: " + clazz.getName() + "), class "+ clazz.getName() + " is not subtype of interface.");}// 检测目标类上是否有 Adaptive 注解if (clazz.isAnnotationPresent(Adaptive.class)) {// 设置 cachedAdaptiveClass缓存cacheAdaptiveClass(clazz, overridden);// 检测 clazz 是否是 Wrapper 类型} else if (isWrapperClass(clazz)) {// 存储 clazz 到 cachedWrapperClasses 缓存中cacheWrapperClass(clazz);} else {// 程序进入此分支,表明 clazz 是一个普通的拓展类clazz.getConstructor();// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常if (StringUtils.isEmpty(name)) {// 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 namename = findAnnotationName(clazz);if (name.length() == 0) {throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);}}// 切分 nameString[] names = NAME_SEPARATOR.split(name);if (ArrayUtils.isNotEmpty(names)) {cacheActivateClass(clazz, names[0]);for (String n : names) {// 存储 Class 到名称的映射关系cacheName(clazz, n);// 存储名称到 Class 的映射关系saveInExtensionClass(extensionClasses, clazz, n, overridden);}}}
}
如上,loadClass 方法操作了不同的缓存,比如解析@Adaptive注解设置到cachedAdaptiveClass缓存;如果是wrapper类设置到cachedWrapperClasses 缓存;保存拓展类和拓展名到 cachedNames 缓存等等。
到这里 Dubbo SPI 加载机制的主要逻辑就讲完了,汇总了一下核心流程如下图:
在我们了解了各种实现细节后,最后可以总结一下各种知识点。
dubbo 的 ExtensionLoader 对应 JDK 的ServiceLoader,包含了加载所需要的各种上下文信息,一个扩展接口对应一个 ExtensionLoader,关系维护在一个 Map 中。
在尝试获取拓展类的时候,才开始实例化,按需加载,这种懒加载是一种对 JDK SPI 的优化。
Dubbo SPI 会从三个配置目录按照优先级加载配置,加载策略是依赖 JDK SPI 加载的。
加载配置的时候会加载所有配置的Class类型,然后把拓展名和拓展类的类型维护一个映射(key-value),这也是按需加载,灵活使用的基础。
如果觉得对你有帮助,欢迎点赞、标🌟或分享!
update在MySQL中是怎样执行的,一张图牢记|原创
2022-11-19
讲真,这篇最全HashMap你不能错过!|原创
2022-11-17
MySQL主从数据不一致,怎么办?
2022-11-11