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

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)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于魏格纳函数和焦散线方法的自加速光束matlab模拟与仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于魏格纳函数和焦散线方法的自加速光束matlab模拟与仿真。通过魏格纳函数法&#xff0c;来产生多种自加速的光束&#xff0c;设计自加速光束方法&#xff0c;模…

springBoot+springSecurity基本认证流程

springBootspringSecurity认证流程 整合springSecurity 对应springboot版本&#xff0c;直接加依赖&#xff0c;这样版本不会错 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId…

vue实战----网易云音乐案例

1 . 能够掌握Vant组件库使用2 . 熟练查阅Vant组件库文档3 . 能够完成网易云音乐案例 案例-网易云音乐 1.本地接口 目标&#xff1a;下载网易云音乐node接口项目, 在本地启动, 为我们vue项目提供数据支持 2.本地接口启动 目标&#xff1a;启动本地node服务_拿到数据 文档: …

Golang创建文件夹

方法 package zdpgo_fileimport ("os" )// AddDir 创建文件夹 func AddDir(dir string) error {if !IsExist(dir) {return os.MkdirAll(dir, os.ModePerm)}return nil }测试 package zdpgo_fileimport "testing"func TestAddDir(t *testing.T) {data : […

二叉树(详解)

在了解二叉树之前呢我们先来了解一下树形结构&#xff0c;因为二叉树就是树形结构的一种特殊情况&#xff0c;它有这非常好的性质&#xff0c;是很常用的一种结构。 目录 一.什么是树形结构&#xff1f; 二.树形结构常见的名词 三.树的存储 四.二叉树 1.二叉树的概念 2.…

学习记录16-反电动势

一、反电动势公式 在负载下反电势和端电压的关系式为&#xff1a;&#x1d448;&#x1d43c;&#x1d445;&#x1d43f;*&#xff08;&#x1d451;&#x1d456; / &#x1d451;&#x1d461;&#xff09;&#x1d438; E为线圈电动势、 &#x1d713; 为磁链、f为频率、N…

网络协议——Modbus-TCP

目录 1、简介 2、Modbus-TCP与Modbus-RTU的区别 3、消息格式 4、功能码01H 5、功能码02H 6、功能码03H 7、功能码04H 8、功能码05H 9、功能码06H 10、功能码0FH 11、功能码10H 1、简介 Modbus-TCP&#xff08;Modbus Transmission Control Protocol&#xff09;是一…

基于Django的美团药品数据分析与可视化系统,有多用户功能,可增删改查数据

背景 随着电子商务和健康产业的迅速发展&#xff0c;药品行业数据的分析和可视化变得愈发重要。基于Django的美团药品数据分析与可视化系统的研究背景凸显了对药品数据的深入挖掘和分析的需求。该系统不仅具备多用户功能&#xff0c;允许不同角色的用户进行数据管理和分析&…

2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版

简介&#xff1a; 2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版 下载地址 https://www.kuaiyuanya.com/product/article/index/id/33.html 图片&#xff1a;

【无重复字符的最长子串】python,滑动窗口+哈希表

滑动窗口哈希表 哈希表 seen 统计&#xff1a; 指针 j遍历字符 s&#xff0c;哈希表统计字符 s[j]最后一次出现的索引 。 更新左指针 i &#xff1a; 根据上轮左指针 i 和 seen[s[j]]&#xff0c;每轮更新左边界 i &#xff0c;保证区间 [i1,j] 内无重复字符且最大。 更新结…

高铁VR虚拟全景展示提升企业实力和形象

步入VR的神奇世界&#xff0c;感受前所未有的汽车展示体验。VR虚拟现实技术以其独特的沉浸式模拟&#xff0c;让你仿佛置身于真实展厅之中&#xff0c;尽情探索汽车的每一处细节。 一、定制化展示&#xff0c;随心所欲 VR汽车虚拟展厅打破空间束缚&#xff0c;让汽车制造商能够…

区块链开发:区块链软件开发包装相关解析

区块链开发是指设计、构建和维护基于区块链技术的应用程序或系统的过程。区块链是一种分布式账本技术&#xff0c;它通过去中心化的方式记录和验证数据&#xff0c;确保数据的透明性、不可篡改性和安全性。区块链开发者使用各种编程语言和框架来创建这些应用程序。 在加密货币领…

C++ sort排序的总和应用题

第1题 sort排序1 时限&#xff1a;1s 空间&#xff1a;256m 输入n个数&#xff0c;将这n个数从小到大排序&#xff0c;输出。 输入格式 第1行&#xff0c;一个正整数n&#xff08;n<100&#xff09; 第2行&#xff0c;n个正整数&#xff0c;小于100 输出格式 n个整…

Windows安装mingw32/w64

1.下载 MinGW-w64 WinLibs - GCCMinGW-w64 compiler for Windows Releases niXman/mingw-builds-binaries (github.com) MinGW-w64、UCRT 和 MSVCRT 是 Windows 平台上常用的 C/C 运行库&#xff0c;它们有以下不同点&#xff1a; MinGW-w64&#xff1a;是一个基于 GCC 的…

【Hive SQL 每日一题】分析电商平台的用户行为和订单数据

需求描述 假设你是一位数据分析师&#xff0c;负责分析某电商平台的用户行为和订单数据&#xff0c;平台上有多个用户&#xff0c;用户可以在不同的日期下单&#xff0c;每个订单包含多个商品。请你完成相关业务分析&#xff0c;帮助平台优化运营策略和用户体验。 数据准备 …

NDIS小端口驱动(五)

在需要的时候&#xff0c;我们也许需要NDIS微型端口程序信息&#xff0c;下面会从多个方面来讨论如何查询NDIS微型端口驱动。 查询无连接微型端口驱动程序 若要查询无连接微型端口驱动程序维护的 OID&#xff0c;绑定协议调用 NdisOidRequest 并传递 一个NDIS_OID_REQUEST 结…

【SQL每日一练】查询“OCCUPATIONS”中的“Occupation”列并按Doctor、Professor、Singer、Actor列输出

文章目录 题目一、分析二、题解1.SqlServer2.MySQL3.Oracle 总结 题目 查询“OCCUPATIONS”中的“Occupation”列&#xff0c;使每个姓名按字母顺序排序&#xff0c;并显示在其相应的“职业》下方。输出列标题应分别为Doctor、Professor、Singer和Actor。 注意&#xff1a;当不…

【ChatGPT】 Microsoft Edge 浏览器扩展使用 GPT

【ChatGPT】添加 Microsoft Edge 浏览器插件免费使用 GPT 文章目录 准备工作添加扩展注意事项 使用 ChatGPT 可以更高效的搜索到想要的内容&#xff0c;有效节约在搜索引擎中排查正确信息的时间。 准备工作 准备一台可上网的电脑电脑上安装有 Windows 自带的 Microsoft Edge …

【Makefile】Makefile 编译 Keil 工程(Linux 环境)

本文使用的开发板为 stm32f103C8T6&#xff0c;使用的驱动库为stm32标准库。 目录 一、软件下载 1、stm32 标准库 2、arm-none-eabi 工具链 3、烧录器 二、Keil 工程改造 1、Keil 工程 2、基本 Makefile 工程 3、添加启动文件 4、添加链接脚本 5、去掉 core_cm3.c 三…

App Inventor 2 如何接入ChatGPT:国内访问OpenAI的最佳方式

如何接入OpenAI 由于国内无法访问OpenAI&#xff0c;KX上网可选大陆及香港&#xff08;被屏蔽&#xff09;以外才行。因此对于大多数人来说&#xff0c;想体验或使用ChatGPT就不太便利&#xff0c;不过App Inventor 2 为我们提供了相对便利的一种方式&#xff0c;即“试验性质…