第四节 Starter 加载时机和源码理解

news2024/9/22 3:46:01

tips:每个 springBoot 的版本不同,代码的实现存会存在不同。

上一章,我们聊到 mybatis-spring-boot-starter; 简单分析了它的结构。 这一章我们将着重分析 Starter 的加载机制,并结合源码进行分析理解。

一、加载实际

1.1 何时被加载

引入的 Starter 何时被加载到 Spring 容器里面。 解决了这个过程,那么 Starter 的加载机制就明白大半了。

1.2 加载时机

给出几个步骤。(为了突出重点,时序图中忽略后置处理器等,下一章会给出全局时序图)

  1. 应用启动始于主启动类(通常使用 @SpringBootApplication 注解)中的main方法,调用SpringApplication.run(...)
  2. 在自动配置过程中,Spring Boot会使用 SpringFactoriesLoader 类来加载所有可见的 spring.factories 文件。SpringFactoriesLoader 查找类路径上所有 META-INF/spring.factories 实例
  3. 通过后置处理器,spring.factories文件中的自动配置类(通过EnableAutoConfiguration指定)将被实例化。这些自动配置类通常使用@Configuration注解,且可能包含@Conditional注解

整个加载过程,比较关键的两个类:

  • SpringFactoriesLoader 加载 spring.factories 文件
  • EnableAutoConfigurationImportSelector 导入 autoconfiguration 并进行加载

spring.factories 文件是帮助 SpringBoot 项目包以外的bean(即在pom文件中添加依赖中的bean)注册到SpringBoot 项目的 Spring 容器中。由于 @ComponentScan注解只能扫描 spring-boot 项目包内的 bean 并注册到 Spring 容器中,因此需要 @EnableAutoConfiguration 注解来注册项目包外的 bean。而spring.factories文件,则是用来记录项目包外需要注册的 bean 类名。

1.3 SPI 机制

SPI(Service Provider Interface)服务提供接口,在Java中是一种发现和加载可插拔实现的标准机制。目的是实现对组件的动态发现和加载

通过java.util.ServiceLoader类来发现和加载META-INF/services/目录下相应接口的实现类。

关于 Java 本身提供的 SPI 实现细节

在 SpringBoot 中,SPI 机制允许开发人员在模块中定义一些服务接口,并且可以为这些接口提供多个可插拔的实现。实现了进一步的便利性和更强大的整合特性。 通过定义约定的 spring.factories 文件,来实现自动配置和条件装配

接下来,我们通过 debug 的方式来探索 Starter 被加载的过程。

特别说明:本文 debug 的源码是:mybatis-starter-apply, 如果需要跟着 debug 走步骤流程, 下载相关源码。uzong-starter-learning: 学习 SpringBoot Starter 的工程案例

二、源码理解

在理解整个 Starter 之前,我们先来了解一下几个核心类。它将是整个解密 Starter 最为关键的几个类

2.1 SpringFactoriesLoader 类

这个类的作用, 解析 META-INF/spring.factories 文件,并将结果方法到一个 Map 中。

spring.factories 的结果和 properties 非常相似,我们可以查看 mybatis-spring-boot-starter 文件中的 spring.factories

注意: spring.factories 中的 value 值是可以用逗号分隔。

使用的分割方法是 org.springframework.util.StringUtils#commaDelimitedListToStringArray

public static String[] commaDelimitedListToStringArray(@Nullable String str) {
    return delimitedListToStringArray(str, ",");
}

将 spring.factories 中的 key = value 解析后放入到 MultiValueMap<String, String>。 这是一个特殊的 map。 它的 value 值是一个 List

public interface MultiValueMap<K, V> extends Map<K, List<V>> {
    .....
}

继承至 Map<K, List<V>>。

SpringFactoriesLoader 将当前类加载器下所有的 "META-INF/spring.factories" 文件进行解析,并将解析结果放到一个 Map 中进行缓存。注意:目前都是 String, 还不是 class

附上部分 SpringFactoriesLoader 源码以及注释。

核心逻辑:加载 META-INF/spring.factories 文件数据; 把里面的key、value值放入到一个特殊的 Map 中。


public abstract class SpringFactoriesLoader {

	// JAR 文件的路径
	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

	// 将所有类路径的名称加载到这个 Map 中
	private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

	// 根据类名获取value值。注意返回的是 list
	public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
		String factoryClassName = factoryClass.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
	}

	// 解析 spring.factories  文件,将解析的 key=value 放入到 cache 中。
	private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null)
			return result;
		try {
			// jar 文件路径
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
            // 遍历所路径,加载文件资源
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				// 解析成 Properties 对象,即 key = value
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    // 逗号分割值
					List<String> factoryClassNames = Arrays.asList(
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
					result.addAll((String) entry.getKey(), factoryClassNames);
				}
			}
			cache.put(classLoader, result);
			return result;
		}
        ......
	}

	// 反射,加载对象
	@SuppressWarnings("unchecked")
	private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
		try {
            // 反射创建对象
			Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
			if (!factoryClass.isAssignableFrom(instanceClass)) {
				throw new IllegalArgumentException(
						"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
			}
			return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();
		}
    	.....
	}
}

通过 debug 断点,查看 cache 中的数据

以 debug 方式启动 com.uzong.instance.MyApplication 类,查看 Map 数据。

断点地址:SpringFactoriesLoader#142 行

可以看到,mybatis-spring-boot-starter中的 auto-configuration 类被加载了。

回到 mybatis-spring-boot-starter 的 spring.factories 确认一下。

org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 确认一致。

读取 spring.factories 抽取了几个关键步骤。如下所示:

补充: idea 中 debug 窗口,为执行栈帧,如下所示。可以清楚的知道运行经过的链路。

总结两个要点:

  • cache 包含多种类型,不仅仅是 Starter 中指定的 org.springframework.boot.autoconfigure.EnableAutoConfiguration类型。 后续通过指定类型获取 list 值。
  • 此处的 Map 中的值还是 String,不是 Class,那么哪一步才能将 String 变成 Class 呢,接下来关注 AutoConfigurationImportSelector

2.2 AutoConfigurationImportSelector 类

org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports

这个方法的作用是将 org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的 List 值进行加载到 application 中进行类的初始化

相关源码,主要从 cache map 中读取 org.springframework.boot.autoconfigure.EnableAutoConfiguration 值。并进行加载

入口:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports

	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		 .....
		try {
			AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
					.loadMetadata(this.beanClassLoader);
			AnnotationAttributes attributes = getAttributes(annotationMetadata);
            // 加载 org.springframework.boot.autoconfigure.EnableAutoConfiguration
			List<String> configurations = getCandidateConfigurations(annotationMetadata,
					attributes);
		    .....
			return StringUtils.toStringArray(configurations);
		}
		......
	}

在这个类中,会读取 spring.factories 中的 org.springframework.boot.autoconfigure.EnableAutoConfiguration 值进行加载。

getCandidateConfigurations()

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
			AnnotationAttributes attributes) {
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
				getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
		......
		return configurations;
	}

返回 org.springframework.boot.autoconfigure.EnableAutoConfiguration

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    return EnableAutoConfiguration.class;
}

特别注意:不同版本源码略有不同;在 selectImports 中, 也对返回的 list 做了过滤。

在 filter() 方法中,过滤一些不满足的 configuration,不进一步加载。比如:ConditionalOnClass 条件注解。如果找不到依赖的那个类,则直接过滤掉。

·

下面就是基于ConditionalOnClass org.springframework.boot.autoconfigure.condition.OnClassCondition#getOutcomes 规则做过滤。

如果我们引入的 Starter 发现没有生效,则可以通过断点到这一行,来排查对应的文件是否被引入。

2.3 加载过程

找到符合的全路径名称后,就交给 org.springframework.context.annotation.ConfigurationClassParser 类进行解析。然后把整个 configuration 所在的 Bean 都一一进行加载。

关于 ConfigurationClassParser 是非常重要的,它是 springframework core 中的核心类。以递归性的解析加载所有类;也是非常繁琐和重要的,后面会用单独章节进行详细讲解。

2.4 整个加载过程

加载过程,包括 ConfigurationClassParser 以及后置处理器也添加进来的时序图。

时序图中的几个关键方法,可以关注一下:

关键方法一:refreshContext

org.springframework.boot.SpringApplication#prepareContext

关键方法二:invokeBeanFactoryPostProcessors。 激活各种后置处理器

org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors

关键方法三:processDeferredImportSelectors,处理延迟导入 importSelector 。

org.springframework.context.annotation.ConfigurationClassParser#processDeferredImportSelectors

Spring 的核心扩展接口。 SpringBoot 的 AutoConfigurationImportSelector 类,实现 ImportSelector(DeferredImportSelector)方法,从而实现 Starter 的扩展装配能力。

三、本章小结

本文只通过局部了解到 Starter 中的类被加载的时机,主要有两个核心类

  • SpringFactoriesLoader
  • AutoConfigurationImportSelector

一个是加载 spring.factories 加载资源放入 Map ; 另外一个触发 Map 中读取数据并交给 ConfigurationClassParser 解析。

如果仅仅从局部理解加载过程是局限的。接下来,我们从整个 SpringBoot 的加载顺序理解全貌。

已同步发布到公众号:面汤放盐 第四节 Starter 加载时机和源码理解 (qq.com)

掘金账号:第四节 Starter 加载时机和源码理解 - 掘金 (juejin.cn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1685260.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

若依解决使用https上传文件返回http路径问题

若依通过HTTPS请求进行文件上传时却返回HTTP的文件链接地址&#xff0c;主要原因是使用了 request.getRequestURL 获取链接地址。 解决办法&#xff1a; 在nginx配置文件location处加上&#xff1a;proxy_set_header X-Forwarded-Scheme $scheme; 然后代码通过request.getHea…

在程序运行中动态改变变量

场景 出于测试目的&#xff0c;需要在程序运行中去改变程序中的参数 思路 动态的去读第三方存储&#xff0c;比如数据库&#xff0c;缓存&#xff0c;甚至是文件 我的实现 这里使用了gflags&#xff0c;通过flaks实现一个api提供flag的修改 Gflags 是 Google 内部使用的命…

人工智能(Educoder)-- 机器学习 -- 神经网络(初级)

第一关 注&#xff1a; 神经网络的起源和应用 起源&#xff1a;神经网络最早由心理学家和神经学家开创&#xff0c;目的是模拟生物神经系统对真实世界物体的交互反应。应用&#xff1a;现代神经网络用于分类&#xff08;如图像识别、文本分类&#xff09;和数值预测&#xff08…

docker启动clickhouse

docker启动clickhouse 创建clickhouse目录拉取镜像启动临时容器, 生成配置文件正式启动 clickhouse越来越流行&#xff0c;本地想安装个测试环境 创建clickhouse目录 后续作为挂载卷使用 mkdir -p /home/gugu/ckdata/data mkdir -p /home/gugu/ckdata/conf mkdir -p /home/gu…

信捷XD系列PLC通讯失败程序无法下载如何设置

如题:最近在使用信捷PLC&#xff0c;有时会出现通讯不上的问题&#xff0c;下面将通讯配置步骤及注意事项分享。 一、确保PLC通电&#xff0c;电脑使用USB通讯线和PLC连接。 二、打开程序&#xff0c;点击串口标识&#xff0c;会弹出通信配置窗口。 三、双击USB通讯这条进行设…

【Python小案例】Python+mysql+PyQt5健康体检报告查询

下载安装Python3.7.8 python官网&#xff1a;https://www.python.org/ PyQt5配置 安装PyQt5 pip install PyQt5pip install qt5_toolspytcharm创建项目 配置外部工具 QTDesigner的Arguments语句不用填 QTDesigner的Working directory语句:$ProjectFileDir$ Pyuic的Argume…

【GDAL】GDAL库学习(C#版本)

1.GDAL 2.VS2022配置GDAL环境&#xff08;C#&#xff09; VS2022工具–NuGet包管理器–管理解决方案的NuGet程序包&#xff0c;直接安装GDAL包。 并且直接用应用到当前的控制台程序中。 找一张tiff格式的图片&#xff0c;或者用格式转换网站&#xff1a;https://www.zamzar.c…

go升级后 编译的exe在win7上无法正常运行

D:/Go/src/runtime/sys_windows_amd64.s:65 x75 fpx22fca sp-0x22fc8日 升级到go 1.21后报一堆错误&#xff0c;要死了啊 原来是go 1.21不支持win7了&#xff0c;必须把go退回到1.20版本 谷歌发布编程语言 Go 1.21 版本&#xff1a;取消支持微软 Win7/8 及苹果 macOS 10.13/10…

爬虫技术升级:如何结合DrissionPage和Auth代理插件实现数据采集

背景/引言 在大数据时代&#xff0c;网络爬虫技术已经成为数据收集的重要手段之一。爬虫技术可以自动化地从互联网上收集数据&#xff0c;节省大量人力和时间成本。然而&#xff0c;当使用需要身份验证的代理服务器时&#xff0c;许多现有的爬虫框架并不直接支持代理认证。这就…

[力扣]——231.2的幂

题目描述&#xff1a; 给你一个整数 n&#xff0c;请你判断该整数是否是 2 的幂次方。如果是&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 如果存在一个整数 x 使得 n 2x &#xff0c;则认为 n 是 2 的幂次方。 bool isPowerOfTwo(int n){ if(n0)retur…

vue3结合element-plus之如何优雅的使用表格

背景 表格组件的使用在后台管理系统中是非常常见的,但是如果每次使用表格我们都去一次一次地从 element-plus 官网去 复制、粘贴和修改成自己想要的表格。 这样一来也说得过去,但是如果我们静下来细想不难发现,表格的使用都是大同小异的,每次都去复制粘贴,对于有很多表格…

深度学习之基于YOLOV5的口罩检测系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景 随着全球公共卫生事件的频发&#xff0c;口罩成为了人们日常生活中不可或缺的一部分。在公共场所&am…

slam14讲(第8讲、前端里程计)LK光流、直接法

直接法的引出 因为第7讲大部分都是讲特征点法&#xff0c;通过提取orb特征点和点的描述子&#xff0c;来构建两帧图像之间的特征点对应关系。这种方法会有缺点&#xff1a; 关键点和描述子提取计算耗时&#xff0c;如果相机的频率高&#xff0c;则slam算法大部分耗时被占。特…

轻量SEO分析报告程序网站已开心去授权

轻量SEO分析报告程序网站已开心去授权&#xff0c;可以让你生成有洞察力的、 简洁的、易于理解的SEO报告&#xff0c;帮助你的网页排名和表现更好 网站源码免费下载地址抄笔记 (chaobiji.cn)https://chaobiji.cn/

算法学习:快速排序

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 &#x1f680; 引言&#x1f4cc; 快速排序算法核心思想1. 选择基准值&#xff08;Pivot&#xff09;2. 分区操作&#xff08;Partitioning&#xff09;3. 递归排序子序列 &#x1f4cc; JavaScript 实现1. 快速排序主函数2…

OGG几何内核-BRepBuilderAPI_MakeEdge学习

OGG几何内核fork自OCCT 7.7.0&#xff0c; BRepBuilderAPI_MakeEdge是几何内核的一个重要和基础的功能&#xff0c;也十分复杂&#xff0c;因为要支持line、circle、ellipse&#xff0c;parabola&#xff0c;hyperbola&#xff0c;circle&#xff0c;beziercurve&#xff0c;b…

Web前端开发技术-格式化文本 Web页面初步设计

目录 Web页面初步设计 标题字标记 基本语法&#xff1a; 语法说明&#xff1a; 添加空格与特殊符号 基本语法&#xff1a; 语法说明: 特殊字符对应的代码: 代码解释&#xff1a; 格式化文本标记 文本修饰标记 计算机输出标记 字体font标记 基本语法&#xff1a; 属…

《TortoiseSVN》简单使用说明

##################工作记录#################### 常用图标说明 一个新检出的工作副本 修改过的文件 更新过程遇到冲突的文件 你当前对文件进行了锁定&#xff0c;不要忘记不使用后要解锁&#xff0c;否则别人无法使用 当前文件夹下的某些文件或文件夹已经被调度从版本控制…

BUUCTF靶场 [reverse]easyre、reverse1、reverse2

工具&#xff1a; DIE&#xff1a;下载&#xff1a;https://download.csdn.net/download/m0_73981089/89334360 IDA&#xff1a;下载&#xff1a;https://hex-rays.com/ida-free/ 新手小白勇闯逆向区&#xff01;&#xff01;&#xff01; [reverse]easyre 首先查壳&#xf…

一刷后日谈

后日谈 1.前言 ​ 今天是一刷结束的日子&#xff0c;回顾一路走过来的路&#xff0c;还是得对自己说声谢谢的&#xff1b; ​ 我是一个向来都很抗拒编程的人&#xff0c;那繁琐的符号让我觉得很是头疼&#xff0c;本科期间数据结构与算法都重修了三次&#xff0c;最后临近毕…