Dubbo SPI 源码分析

news/2024/10/30 23:24:17/

上一章,我简单演示了 Dubbo SPI 的使用方法。我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。该方法的逻辑比较简单,本章就不就行分析了。下面我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。

public T getExtension(String name) {if (name == null || name.length() == 0)throw new IllegalArgumentException("Extension name == null");if ("true".equals(name)) {// 获取默认的拓展实现类return getDefaultExtension();}// Holder 仅用于持有目标对象,没其他什么逻辑Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<Object>());holder = cachedInstances.get(name);}Object instance = holder.get();if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {// 创建拓展实例,并设置到 holder 中instance = createExtension(name);holder.set(instance);}}}return (T) instance;
}

上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建拓展对象的过程是怎样的。

private T createExtension(String name) {// 从配置文件中加载所有的拓展类,形成配置项名称到配置类的映射关系Class<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw findException(name);}try {T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {// 通过反射创建实例EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);}// 向实例中注入依赖injectExtension(instance);Set<Class<?>> wrapperClasses = cachedWrapperClasses;if (wrapperClasses != null && !wrapperClasses.isEmpty()) {// 循环创建 Wrapper 实例for (Class<?> wrapperClass : wrapperClasses) {// 将当前 instance 作为参数创建 Wrapper 实例,然后向 Wrapper 实例中注入属性值,// 并将 Wrapper 实例赋值给 instanceinstance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}}return instance;} catch (Throwable t) {throw new IllegalStateException("...");}
}

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:

  1. 通过 getExtensionClasses 获取所有的拓展类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 将拓展对象包裹在相应的 Wrapper 对象中

以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的章节中,我将会重点分析 getExtensionClasses 方法的逻辑,以及简单分析 Dubbo IOC 的具体实现。

3.1 获取所有的拓展类

我们在通过名称获取拓展类之前,首先需要根据配置文件解析出名称到拓展类的映射,也就是 Map<名称, 拓展类>。之后再从 Map 中取出相应的拓展类即可。相关过程的代码分析如下:

private Map<String, Class<?>> getExtensionClasses() {// 从缓存中获取已加载的拓展类Map<String, Class<?>> classes = cachedClasses.get();if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {// 加载拓展类classes = loadExtensionClasses();cachedClasses.set(classes);}}}return classes;
}

这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则加载拓展类。以上代码的写法是典型的双重检查锁,前面所分析的 getExtension 方法中有相似的代码。关于双重检查就说这么多,下面分析 loadExtensionClasses 方法的逻辑。

private Map<String, Class<?>> loadExtensionClasses() {// 获取 SPI 注解,这里的 type 是在调用 getExtensionLoader 方法时传入的final SPI defaultAnnotation = type.getAnnotation(SPI.class);if (defaultAnnotation != null) {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("...");}// 设置默认名称,cachedDefaultName 用于加载默认实现,参考 getDefaultExtension 方法if (names.length == 1) {cachedDefaultName = names[0];}}}Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();// 加载指定文件夹配置文件loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);loadDirectory(extensionClasses, DUBBO_DIRECTORY);loadDirectory(extensionClasses, SERVICES_DIRECTORY);return extensionClasses;
}

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {// fileName = 文件夹路径 + type 全限定名 String fileName = dir + type.getName();try {Enumeration<java.net.URL> urls;ClassLoader classLoader = findClassLoader();if (classLoader != null) {// 根据文件名加载所有的同名文件urls = classLoader.getResources(fileName);} else {urls = ClassLoader.getSystemResources(fileName);}if (urls != null) {while (urls.hasMoreElements()) {java.net.URL resourceURL = urls.nextElement();// 加载资源loadResource(extensionClasses, classLoader, resourceURL);}}} catch (Throwable t) {logger.error("...");}
}

loadDirectory 方法代码不多,理解起来不难。该方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {try {BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));try {String line;// 按行读取配置内容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) {// 以 = 为界,截取键与值。比如 dubbo=com.alibaba....DubboProtocolname = line.substring(0, i).trim();line = line.substring(i + 1).trim();}if (line.length() > 0) {// 加载解析出来的限定类名loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);}} catch (Throwable t) {IllegalStateException e = new IllegalStateException("...");}}}} finally {reader.close();}} catch (Throwable t) {logger.error("...");}
}

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。loadClass 方法有点名不副实,它的功能只是操作缓存,而非加载类。该方法的逻辑如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {if (!type.isAssignableFrom(clazz)) {throw new IllegalStateException("...");}if (clazz.isAnnotationPresent(Adaptive.class)) {    // 检测目标类上是否有 Adaptive 注解if (cachedAdaptiveClass == null) {// 设置 cachedAdaptiveClass缓存cachedAdaptiveClass = clazz;} else if (!cachedAdaptiveClass.equals(clazz)) {throw new IllegalStateException("...");}} else if (isWrapperClass(clazz)) {    // 检测 clazz 是否是 Wrapper 类型Set<Class<?>> wrappers = cachedWrapperClasses;if (wrappers == null) {cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();wrappers = cachedWrapperClasses;}// 存储 clazz 到 cachedWrapperClasses 缓存中wrappers.add(clazz);} else {    // 程序进入此分支,表明是一个普通的拓展类// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常clazz.getConstructor();if (name == null || name.length() == 0) {// 如果 name 为空,则尝试从 Extension 注解获取 name,或使用小写的类名作为 namename = findAnnotationName(clazz);if (name.length() == 0) {throw new IllegalStateException("...");}}// 切分 nameString[] names = NAME_SEPARATOR.split(name);if (names != null && names.length > 0) {Activate activate = clazz.getAnnotation(Activate.class);if (activate != null) {// 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,// 存储 name 到 Activate 注解对象的映射关系cachedActivates.put(names[0], activate);}for (String n : names) {if (!cachedNames.containsKey(clazz)) {// 存储 Class 到名称的映射关系cachedNames.put(clazz, n);}Class<?> c = extensionClasses.get(n);if (c == null) {// 存储名称到 Class 的映射关系extensionClasses.put(n, clazz);} else if (c != clazz) {throw new IllegalStateException("...");}}}}
}

如上,loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,该方法没有其他什么逻辑了,就不多说了。

到此,关于缓存类加载的过程就分析完了。


http://www.ppmy.cn/news/21392.html

相关文章

【每日CSS3代码】

1-1 两栏布局【1/27】 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevi…

数学建模与数据分析 || 3. 面向数据的特征提取方法: 探索性数据分析

面向数据的特征提取方法: 探索性数据分析 文章目录面向数据的特征提取方法: 探索性数据分析1. 原始数据的准备1.1 导入 python 模块1.2 导入数据集并进行宏观认识1.3 数据集描述2. 数据的预处理2.1 缺失数据的甄别2.2 类别规模的评估3. 数据特征的处理3.1 第一个因变量- 分析范…

我们为什么要学习servlet? servlet是干嘛的?

最近刚刚学习完servlet&#xff0c;明白了一个事情&#xff0c;servlet是用来干嘛的&#xff0c;为什么要学习servlet&#xff0c;我想如果我在刚刚开始学习servlet时就明白这件事的话&#xff0c;会更加有利于我带有目的的去学习servlet&#xff1b;所以记录以下文章&#xff…

一些关于Linux内核中的结构体函数指针的理解

一些关于Linux内核中常用的结构体函数指针的理解 动机 在看linux内核代码的时候经常能够看到一些结构体里面的成员跟我们以往见的到一些结构体不一样&#xff0c;常见的架构体如下面的代码&#xff1a; struct a{int i;char b;struct c; };而内核中又见有这样的一些结构体&a…

【MyBatis】| MyBatis查询语句专题(核心知识)

目录 一&#xff1a;MyBatis查询语句专题 1. 返回Car对象 2. 返回List<Car> 3. 返回Map 4. 返回List<Map> 5. 返回Map<String,Map> 6. resultMap结果映射 7. 返回总记录条数 一&#xff1a;MyBatis查询语句专题 前期准备&#xff1a; 模块名&#xf…

springboot+mongodb初体验

MongoDB 是一个基于分布式文件存储的数据库。由 C 语言编写&#xff0c;旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。 MongoDB 是一个介于关系数据库和非关系数据库之间的产品&#xff0c;是非关系数据库当中功能最丰富&#xff0c;最像关系数据库的。 1、mongodb服务…

CMOS器件与TTL器件CMOS电平与TTL电平

常用的数字芯片&#xff0c;按制造工艺主要分为TTL器件和CMOS器件。TTL器件是指其内部主要逻辑单元为双极性晶体管&#xff0c;CMOS器件是指其内部的主要逻辑单元为MOS管。现在绝大部分数字芯片使用的工艺都是CMOS工艺&#xff0c;在一些比较老的74系列芯片中还使用的是TTL工艺…

智能驾驶 车牌检测和识别(五)《C++实现车牌检测和识别(可实时车牌识别)》

智能驾驶 车牌检测和识别&#xff08;五&#xff09;《C实现车牌检测和识别&#xff08;可实时车牌识别&#xff09;》 目录 智能驾驶 车牌检测和识别&#xff08;五&#xff09;《C实现车牌检测和识别&#xff08;可实时车牌识别&#xff09;》 1. 前言 2. 车牌检测模型&a…