第四节 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值。注意返回的是 listpublic 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 = valueProperties 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.EnableAutoConfigurationList<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.mzph.cn/diannao/14027.shtml

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

相关文章

问题与解决:element ui垂直菜单展开后显示不全

比如我这个垂直菜单展开后&#xff0c;其实系统管理下面还有其他子菜单&#xff0c;但是显示不出来了。 解决方法很简单&#xff0c;只需要在菜单外面包一层el-scrollbar&#xff0c;并且将高度设置为100vh。

Laravel 11 PHP8

一直都是用laravel 7 左右的&#xff0c;现在要求将项目升级到laravel 11 和使用PHP8&#xff0c;随手记录一些小问题&#xff0c;laravel 11的包是领导给的&#xff0c;没有使用composer 安装&#xff0c;所以我也不确定和官方的是否一致 遇到这问题 可以这样 env 中默认的数…

基于若依的旅游推荐管理系统(spring boot+vue+mybatis+Ajax)

一、项目目的 随着社会的高速发展&#xff0c;人们生活水平的不断提高&#xff0c;以及工作节奏的加快&#xff0c;旅游逐渐成为一个热门的话题&#xff0c;因为其形式的多样&#xff0c;涉及的面比较广&#xff0c;成为人们放松压力&#xff0c;调节情绪的首要选择。 传统的旅…

上位机图像处理和嵌入式模块部署(mcu的按键输入)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 做技术的同学&#xff0c;大部分都会把精力放在技术本身&#xff0c;却忽视了学的东西有什么实际的用途。就拿gpio来说&#xff0c;一般我们点灯也…

正确认识IP地址和子网掩码的联系

IP地址和子网掩码是计算机网络中两个非常重要的概念&#xff0c;它们共同确定了设备在局域网中的地址以及该地址所属的子网&#xff0c;只要两者结合&#xff0c;就能确定唯一地址IP66_ip归属地在线查询_免费ip查询_ip精准定位平台。 IP地址是用于标识计算机网络中的每台设备的…

Ajax用法总结(包括原生Ajax、Jquery、Axois)

HTTP知识 HTTP&#xff08;hypertext transport protocol&#xff09;协议『超文本传输协议』&#xff0c;协议详细规定了浏览器和万维网服务器之间互相通信的规则。 请求报文 请求行: GET、POST /s?ieutf-8...&#xff08;url的一长串参数&#xff09; HTTP/1.1 请求头…

Buzz库网络爬虫实例:快速爬取百度搜索实时热点

前言 随着互联网的发展&#xff0c;信息获取已经成为了人们日常生活和工作中的重要一环。而在信息获取的过程中&#xff0c;网络爬虫作为一种自动化的数据采集工具&#xff0c;为我们提供了极大的便利。本文将介绍如何利用PHP编写一个简单而高效的网络爬虫&#xff0c;实现快速…

R实验 参数检验(二)

实验目的&#xff1a;掌握正态分布和二项分布中&#xff0c;功效与样本容量之间的关系&#xff1b;学会利用R软件完成一个正态总体方差和两个正态总体方差比的区间估计和检验。 实验内容&#xff1a; &#xff08;习题5.28&#xff09;一种药物可治疗眼内高压&#xff0c;目的…

Mac安装 Intellij IDEA,亲测有效M1、M2可用

引言 最近开始学习使用spring boot写一个简单的后端项目&#xff0c;使用Intellij IDEA软件&#xff0c;Intellij IDEA为新用户提供了30天的免费试用。 方案 1.官网下载Intellij IDEA IntelliJ IDEA – the Leading Java and Kotlin IDE 或者直接网盘连接下载&#xff1a;…

第一份工资

当我拿到我人生的第一份工资时&#xff0c;那是一种难以言表的激动。我记得那个下午&#xff0c;阳光透过窗户洒在了我的办公桌上&#xff0c;我看着那张支票&#xff0c;心中满是欣喜和自豪。那是我独立生活的开始&#xff0c;也是我对自己能力的一种肯定。 我记得我是如何支配…

SQL注入:pikachu靶场中的SQL注入通关

目录 1、数字型注入&#xff08;post&#xff09; 2、字符型注入&#xff08;get&#xff09; 3、搜索型注入 4、XX型注入 5、"insert/update"注入 Insert&#xff1a; update&#xff1a; 6、"delete"注入 7、"http header"注入 8、盲…

C#实现KMP算法,在长字符串中找到第一个符合要求的子字符串

KMP&#xff08;Knuth-Morris-Pratt&#xff09;算法是一种高效的字符串搜索算法&#xff0c;它可以在一个文本字符串&#xff08;Text&#xff09;中搜索一个词&#xff08;Pattern&#xff09;&#xff0c;时间复杂度为O(nm)&#xff0c;其中n是文本字符串的长度&#xff0c;…

vite前端UI框架使用详解(2024-05-24)

Vite&#xff08;发音同 "veet"&#xff09;是一种新型前端构建工具&#xff0c;能够显著提升前端开发体验。它主要由两部分组成&#xff1a; 一个开发服务器&#xff0c;它基于原生的ES模块提供了丰富的内建功能&#xff0c;如速度快到惊人的 模块热更新&#xff08…

【Linux安全】Firewalld防火墙

目录 一.Firewalld概述 二.Firewalld和iptables的关系 1.firewalld和iptables的联系 2.firewalld和iptables的区别 三.Firewalld区域 1.概念 2.九个区域 3.区域介绍 4.Firewalld数据处理流程 四.Firewalld-cmd命令行操作 1.查看 2.增加 3.删除 4.修改 五.Firewa…

arping 一键检测网络设备连通性(KALI工具系列二)

目录 1、KALI LINUX简介 2、arping工具简介 3、在KALI中使用arping 3.1 目标主机IP&#xff08;win&#xff09; 3.2 KALI的IP 4、操作示例 4.1 IP测试 4.2 ARP测试 4.3 根据存活情况返回 5、总结 1、KALI LINUX简介 Kali Linux 是一个功能强大、多才多艺的 Linux 发…

表现层框架设计之使用XML设计表现层

使用XML设计表现层&#xff0c;统一Web Form与Windows Form的外观。 1.XML&#xff08;可扩展标记语言&#xff09; XML&#xff08;可扩展标记语言&#xff09;与HTML类似&#xff0c;是一种标记语言。与主要用于控制数据的显示和外观的HTML标记不同&#xff0c;XML标记用于定…

PostgreSQL的扩展(extensions)-常用的扩展之pg_rman

PostgreSQL的扩展&#xff08;extensions&#xff09;-常用的扩展之pg_rman pg_rman 是 PostgreSQL 社区提供的一个备份和恢复管理工具。它能够简化和自动化 PostgreSQL 数据库的备份和恢复过程&#xff0c;并支持全量备份、增量备份和差异备份。pg_rman 提供了方便的命令行接…

【机器学习与大模型】驱动下的电子商务应用

摘要&#xff1a; 随着信息技术的飞速发展&#xff0c;电子商务已经成为当今商业领域中最为活跃和重要的部分之一。而机器学习和大模型的出现&#xff0c;为电子商务带来了新的机遇和挑战。本文深入探讨了机器学习与大模型在电子商务中的应用&#xff0c;包括个性化推荐、精准营…

Java 18:开启Java平台的新纪元

Java 18&#xff1a;探索Java平台的最新飞跃 随着Java 18的发布&#xff0c;Java平台再次证明了其不断创新和适应现代软件开发需求的能力。作为长期支持&#xff08;LTS&#xff09;版本&#xff0c;Java 18不仅带来了性能上的提升&#xff0c;还引入了一系列令人兴奋的新特性…

基于双向长短期记忆 Bi-LSTM 对消费者投诉进行多类分类

前言 系列专栏:【深度学习:算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域,讨论了各种复杂的深度神经网络思想,如卷积神经网络、循环神经网络、生成对抗网络、门控循环单元、长短期记…