SpringBoot源码深度解析

news2024/12/26 13:49:06

        今天,聊聊SpringBoot的源码,本博客聊的版本为v2.0.3.RELEASE。目前SpringBoot的最新版为v3.3.2,可能目前有些公司使用的SpringBoot版本高于我这个版本。但是没关系,因为版本越新,新增的功能越多,反而对SpringBoot源码的研究带来更多的困难,我觉得没必要刻意追求最新,只要掌握其核心流程即可,万变不离其宗。另外,前面我花了大量的时间,一共写了六篇博客,也是为了讲SpringBoot框架做铺垫,Spring/SpringMVC的原理,如果没看的话,建议先看这部分的博客(《Spring源码深度解析(上)、《SpringMVC源码深度解析(上)》),不然直接看SpringBoot源码,会有一定难度。因为我理解的SpringBoot框架,是对Spring FrameWork框架的进一步封装。OK,话不多说,进入正题。

        先看看项目的层级目录:

        依赖也很简单,如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-project</artifactId>
		<version>2.0.3.RELEASE</version>
	</parent>
	<modelVersion>4.0.0</modelVersion>
	<artifactId>my-spring-boot</artifactId>

	<properties>
		<maven.compiler.source>8</maven.compiler.source>
		<maven.compiler.target>8</maven.compiler.target>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>2.0.3.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<version>2.0.3.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
			<version>2.0.3.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.24</version>
		</dependency>
	</dependencies>

</project>

        使用过SpringBoot框架的朋友都知道,SpringBoot会有一个启动类,启动类是被@SpringBootApplication注解修饰的。看看App.class的代码:

        那就以SpringApplication这个类最为切入点讲解。先看@SpringBootApplication注解,代码如下:

        可以看出,@SpringBootApplication注解也是可以添加包扫描路径的,最终添加的包扫描路径会设置到@ComponentScan注解中的scanBasePackages或者scanBasePackageClasses属性中去。但是我们一般不会指定,默认扫描的包路径为:App类所在的包及其子包。然后,在@SpringBootApplication注解注解上,还添加了几个注解,分别是:@ComponentScan、@SpringBootConfiguration、@EnableAutoConfiguration等。@ComponentScan自不必说,@SpringBootConfiguration注解实际上又是被@Configuration注解修饰的,如果把@SpringBootConfiguration注解替换成@Configuration注解,也是没有任何问题的。

因此,被@SpringBootApplication注解修饰的类可以直接当配置类使用,也就是可以在类中添加其它类到Spring容器中。重点来了,就是@EnableAutoConfiguration注解,这是SpringBoot实现自动装配的关键,代码如下:

        可以看出,@EnableAutoConfiguration注解可以排除一些类,除此之外,这个注解上面也被其他注解所修饰,分别是:@AutoConfigurationPackage注解和@Import注解。代码中的注释对@AutoConfigurationPackage注解,我说的很清楚:作用就是往Spring容器中注入BasePackages对象,该类存有扫描的包信息,用于其他框架整合SpringBoot的时候,方便获取到这个v包,进行它自己的扫描,需要这样操作的框架还挺多的,如Mybatis、Dubbo、Open Feign等。当然,其他框架用不用这个类是它们的事,但是SpringBoot有提供这样的方式。

        再看看@Import注解,熟悉Spring框架的朋友应该对这个注解很熟悉,它的作用是向Spring容器中注入@Import注解配置的Class。看看AutoConfigurationImportSelector类,代码如下:

        可以看出,AutoConfigurationImportSelector实现了DeferredImportSelector、BeanClassLoaderAware、ResourceLoaderAware、BeanFactoryAware、EnvironmentAware等接口,Spring框架在初始化AutoConfigurationImportSelector的时候,会多次调用回调方法,比如给AutoConfigurationImportSelector设置ConfigurableListableBeanFactory、Environment、ClassLoader、ResourceLoader等对象。其中,DeferredImportSelector接口很重要,根据这个接口的特点:当Spring在解析配置类的时候,当解析完这一轮配置类后,才回调用DeferredImportSelector#selectImports()方法,由于有着一个延迟解析的特点,才能实现这样一个功能:比如Servlet容器有很多种,如Tomcat、Jetty、Undertow等,默认使用Tomcat作为Servlet容器,如果此时开发人员不想用Tomcat,想用Jetty,那应该怎么做呢?很简单,引入Jetty的依赖,排除Tomcat相关依赖即可。这里涉及到ServletWebServerFactoryConfiguration类,代码如下:

        除了我刚刚说的,引入Jetty的依赖,再排除Tomcat相关依赖,可以改成使用Jetty服务器;还有一个方法也可以做到,即不用排除Tomcat的依赖,只需要再引入Jetty的依赖,在自己的配置类中添加JettyServletWebServerFactory即可。因为ServletWebServerFactoryConfiguration这个配置类是Spring在解析完程序员自定义的配置类后再解析的,因此通过@ConditionalOnMissingBean注解进行判断的时候会发现,此时Spring容器中已经有了之前注入的JettyServletWebServerFactory对象了,因此,ServletWebServerFactoryConfiguration中配置三个ServletWebServerFactory对象都不会注入到Spring容器中,最后调用ServletWebServerFactory#getWebServer()方法,得到的只有JettyWebServer,代码如下:

        因此可以知道,AutoConfigurationImportSelector实现DeferredImportSelector接口的作用就是保证程序员的配置大于默认配置!当然,讲到这里,其实还不是SpingBoot的自动装配,自动装配的话,还是要看AutoConfigurationImportSelector#selectImports()方法,代码如下:

        再看看AutoConfigurationImportSelectorget#CandidateConfigurations()方法,看看它是如何获取配置类的,代码如下:

可以看出,上面的逻辑是,通过ClassLoader读取classpath下的META-INF/spring.factories文件,获取文件中的内容,看看spring.factories,如下:

        其中有一个EnableAutoConfiguration类的全限定名,而且确实方法中也传入了EnableAutoConfiguration类的全限定名,因此可以猜到:程序读取spring.factories文件,并通过EnableAutoConfiguration类的全限定名作为Key,获取对用的value,也就是截图中的一大堆类的全限定名,并返回,这就是所有待解析的配置类。其中也不乏我们熟悉的类,如:RabbitAutoConfiguration、AopAutoConfiguration、ElasticsearchDataAutoConfiguration等等。那是不是把这些配置类全部返回,并进行加载解析就行了呢?当然不行,准确的说是没必要,因为这些配置类都是要有相关的依赖,才会起作用,因此需要过滤,当然,如果通过@SpringBootApplication配置,排除配置类或者配置类名,这种也需要过滤。

        以上就是SpringBoot自动装备的原理。如果我们自己要写一个工具,怎么与SpringBoot整合呢?其实很简单,自己写一个项目,在META-INF目录下建一个spring.factories文件,目录为:

在该文件中内容为:org.springframework.boot.autoconfigure.EnableAutoConfiguration=你的配置的全限定名。如果有多个配置类,就用","隔开。这样在pom文件中,引入你写的工具的依赖,SpringBoot就会加载这个配置类,再配合配置上加的条件注解即可。

        回到App类中,看看main方法:

        在该方法中,核心的类是SpringApplication,先看看它的有参构造方法,传入的是App.class,代码如下:

        将传入的App.class存入LinkedHashSet,并赋值给primarySources属性。然后调用SpringApplication#deduceWebApplicationType(),推断应用类型,代码如下:

        再调用SpringApplication#getSpringFactoriesInstances()方法,传入ApplicationContextInitializer.class,加载ApplicationContextInitializer.class接口的实现类,代码如下:

        并将获取到的ApplicationContextInitializer对象的集合,赋值给SpringApplication的initializers属性,代码如下:

        同理,从spring.factories中获取到所有ApplicationListener对象的集合,赋值给SpringApplication的listeners属性中。最后调用SpringApplication#deduceMainApplicationClass()方法,推断主类,代码如下:

         最终获取到的也是App.calss,并赋值给SpringApplication的mainApplicationClass属性。

        以上,就是SpringApplication的有参构造方法。这也这只是完成了SpringApplication初始化工作,但是要让服务跑以来,核心的就是调用SpringApplication#run(String[] args)方法,代码如下:

        其实我的注释写的很详细的,不过我还是带着大家看看。首先是看看SpringApplication#getRunListeners()方法,代码如下:

        还是通过通过spring.factories文件获取SpringApplicationRunListener对象的集合,实际上只有一个实现类,就是 EventPublishingRunListener,在创建这个对象的时候,会调用它的有参构造,传入 SpringApplication对象,有参构造的代码为:

        最终将EventPublishingRunListener对象在设置到SpringApplicationRunListeners对象中,后续在进行时间发布的时候,调用的是SpringApplicationRunListeners的某些方法,代码如下:

        回到SpringApplication#run()方法,接着就是调用SpringApplicationRunListeners#starting()方法,发布ApplicationStartingEvent,调用ApplicationListener#onApplicationEvent()方法,调用之前会先判断,哪些ApplicationListener对象是对ApplicationStartingEvent事件“感兴趣”的。这里没有太多好说的,就不说了,继续往下看,再调用SpringApplication#prepareEnvironment(),这里是处理环境变量,配置就是在这个方法中解析读取的,需要重点看看,代码如下:

        先看看SpringApplication#getOrCreateEnvironment()方法,代码如下:

        看看StandardServletEnvironment的类继承图,如下:

        看看父类的构造,发现AbstractEnvironment父类构造中有做一些初始化的操作,代码如下:

        到这里,可以知道,此时在环境变量中,应该设置了四种属性,顺序(顺序代表着优先级)分别是:StubPropertySource(servletConfigInitParams)、StubPropertySource(servletContextInitParams)、MapPropertySource(systemProperties)、SystemEnvironmentPropertySource(systemEnvironment),只不过前两个,此时还没有任何值,毕竟还没有设置值。打断点看看,我说的是否正确:

        要想获取main方法中的args参数,需要先设置在Idea中设置,设置如下:

        可以知道,最终通过main方法传入的args参数,封装成SimpleCommandLinePropertySource对象,并放入环境变量属性的最前面,此时环境变量有五种属性了。打断点看看:

        再看看SpringApplication#configureProfiles()方法,代码如下:

        重点看看SpringApplicationRunListeners#environmentPrepared()方法,代码如下:

        看这个事件名,可以猜到是处理配置相关的,继续往下看,代码如下:

        这里我可以明确的告诉你,调用的是ConfigFileApplicationListener#onApplicationEvent()方法(我看过SpringBoot v2.6的版本,这个版本中没有使用ConfigFileApplicationListener来解析配置文件了,最低是什么版本就没有再使用ConfigFileApplicationListener类了,这我就不确定了),这个方法会处理配置文件,该方法的代码如下:

        可以知道,会创建RandomValuePropertySource(random)对象,放在SystemEnvironmentPropertySource(systemEnvironment)后面,到目前为止,环境变量一共有六种属性了,打断点看看,代码如下:

        OK,再看看Loader#Loader()方法,代码如下:

        再看看Loader#initializeProfiles()方法:

        可以知道,此时在profiles中有两个对象,也是个空的Set对象,另一个是Profile(default)。回到Loader#Loader()方法继续往下看,接着就是遍历profiles对象,代码如下:

        核心是调用重载方法 Loader#Loader(),代码如下 :

        因此从源码可以知道,环境变量设置:spring.config.location、spring.config.additional-location,可以指定读取文件的路径,如果没有设置的话,默认读取的路径为:file:./config/ 、file:./、classpath:/config/、classpath:/ 等四个路径(顺序即为读取路径的优先级)。并且设置 spring.profiles.active,可以设置文件后缀,如设置为 dev,最后读取的文件为:xx-dev。

然后就是对这四个路径进行遍历,判断那个路径下,有配置文件。除了知道文件路径外,还要知道读取的文件名叫什么,这个也有默认值,当然也可以通过配置去修改默认的文件名,代码如下:

        有了路径和文件名,就可以准备读取了,代码如下:

        看看PropertySourceLoader是如何赋值的,代码如下:

        可以知道,也是从spring.factories文件中读取PropertySourceLoader接口的实现类并实例化,一共有两个,分别是PropertiesPropertySourceLoader 和 YamlPropertySourceLoader。前者用于解析xml和propeties后缀的文件,后者解析yml和yaml后缀的文件,如下:

        继续往下看,代码如下:

        看看Loader#loadDocuments()方法,代码如下:

        到这里就行了,感兴趣的可以自己研究,回到Loader#load()方法,代码如下:

        调用consumer#accept()方法,也就是前面传入的λ表达式,即:

        到目前为止解析的还是application,由于我在application.yml配置了profile,因此还会继续读取:

        回到Loader#load()方法,由于在前面已经读取到application.yml中设置的dev了,并放入Loader的profiles属性中,而且还是在遍历profiles,因此最终会解析application-dev.yml文件,代码如下:

        读取配置的逻辑一样,这里不再赘述,到现在为止,读取的配置还只是存在Loader的loaded属性中,需要放如环境变量中,也就是调用下面的代码,代码如下:

        在读取配置的时候,会多次调用Collections.reverse()方法,改变顺序,其实这就是配置优先级的关键,继续往下看:

        到现在为止,环境变量中已经有八个配置了,其中application-dev.yml的配置在application.yml之前。如果通过环境变量取值的话,就是按照这个顺序来取值的,也就是说,只有前面七个配置中找不到,才会到第八个配置中找!到目前为止,我觉得SpringBoot配置读取这块,应该是讲的很详细了。

        回到SpringApplication#run()方法,继续往下看,代码如下:

        再看看SpringApplication#prepareContext()方法,代码如下:

        看看BeanDefinitionLoader#load()方法,代码如下:

        回到SpringApplication#run()方法,再看看SpringApplication#refreshContext()方法,代码如下:

        看看它的继承关系图:

        AbstractApplicationContext#refresh()方法有多重要,想必就不用我多说了吧,这块的代码在我之前的博客(《Spring源码深度解析(上)》)讲的很详细了,有兴趣的可以看看,其中有两个方法,即onRefresh()方法和finishRefresh()方法,需要我说一下,先看看onRefresh()方法,代码如下:

        其中ServletWebServerApplicationContext#initPropertySources()方法,会将ServletContext属性值设置到环境变量中,代码如下:

        再看ServletWebServerApplicationContext#createWebServer()方法,代码如下:

        再看看finishRefresh()方法,代码如下:

        最后再回到SpringApplication#run()方法看看剩下的代码,如下:

        到这里位置SpringBoot框架的源码算是讲完了,我个人觉得应该是讲的很全面的,如果在讲解的过程中,有漏讲或者讲错的,欢迎指出,感谢~

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

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

相关文章

如何将几百兆的包优化到几十兆----记一次vue项目的打包优化过程

打包优化 现象 前段时间开发的时候遇到客户反馈的一个问题 界面无法打开&#xff0c;显示白屏&#xff0c;控制台无报错 经过我们在开发环境&#xff0c;测试环境反复测试都没复现出客户的问题&#xff0c;然后我们又不停的在生产环境上找问题&#xff0c;也没复现出来 最…

一文带你读懂MLIR论文,理解MLIR设计准则.

论文MLIR: Scaling Compiler Infrastructure for Domain Specific Computation MLIR&#xff1a;针对特定领域计算扩展编译器基础设施 文章目录 论文MLIR: Scaling Compiler Infrastructure for Domain Specific Computation1. 论文下载2. TVM关于MLIR的讨论3. 论文正文0. 摘要…

5个人人都应该学会的电脑小技巧

今天分享几个电脑常用的快捷键&#xff0c;可以让你的工作事半功倍&#xff0c;建议收藏&#xff01; 不想被突然来的人看到正在浏览的网站&#xff0c;用CtrlW或者AltF4可以关闭当前页面&#xff0c;另外如果你正在看小电影也可以用这个办法关闭。 2. 同事或大Boos走了之后想…

Anthropic的Claude安卓版能否赢得用户青睐?

Anthropic的Claude安卓版能否赢得用户青睐&#xff1f; 前言 Anthropic 就在7月18日&#xff0c;这家以"可控AI"著称的初创公司再次出手&#xff0c;推出了Claude的Android版本应用。这款APP不仅支持实时语言翻译&#xff0c;更传承了Anthropic一贯的隐私保护政策。C…

腾讯云COS托管静态网站,以及如何解决访问出现了下载网页的情况

腾讯云对象存储&#xff08;Cloud Object Storage&#xff0c;简称COS&#xff09;&#xff0c;与其他云厂商所提供的云对象存储都是面向非结构化数据&#xff0c;只是每个云厂商的叫法有别于他家&#xff0c;或许是更能彰显厂商的品牌吧&#xff01; 但不管云厂商怎么给云对象…

错误:PHP:Deprecated: Required parameter $xxx follows optional parameter $yyy

前言 略 错误 Deprecated: Required parameter $xxx follows optional parameter $yyy 解决办法 设置 error_reporting E_ALL & ~E_DEPRECATED & ~E_STRICT 参考 https://blog.csdn.net/lxw1844912514/article/details/100028023

MATLAB学习日志DAY13

13.矩阵索引&#xff08;1&#xff09; 13.1 下标 上图&#xff01; A 的行 i 和列 j 中的元素通过 A(i,j) 表示。 例如&#xff0c;A(4,2) 表示第四行和第二列中的数字。 在幻方矩阵中&#xff0c; A(4,2) 为 15。 A(1,4) A(2,4) A(3,4) A(4,4) 用来计算 A 第四列中的…

【PG】PostgreSQL高可用之repmgr事件通知

目录 描述 结合脚本 占位符 repmgr命令 生成的事件&#xff1a; repmgrd 生成的事件&#xff08;流复制模式&#xff09;&#xff1a; 描述 每次repmgr或repmgrd执行重大事件时&#xff0c;都会将该事件的记录连同时间戳、失败或成功的标识以及进一步的详细信息&#xff08…

黑马商城启动流程(微服务拆分项目)

1.虚拟机ssh&#xff08;docker中布置了 mysql nacos seata&#xff08;分布式事务&#xff09;三个容器在同一个网络hm-net中&#xff09; 2.idea&#xff08;启动所有的微服务项目 &#xff09; 3.nginx 在cmd上启动 4.navicat 连接数据库 5.登录前端页面 http://localho…

Stable Diffusion AI入门介绍

Stable Diffusion模型 在上一篇的文章中我们介绍了&#xff0c;AIGC的相关知识以及AI绘画的历史——AIGC是什么&#xff0c;与AI绘画有什么关系&#xff0c;一篇文章带你了解AI绘画的前世今生。 我们知道了Stable Diffusion是一种潜在扩散模型&#xff0c;由慕尼黑大学的Comp…

将github上的项目导入到vscode并创建虚拟环境

1、将github上的项目导入到vscode 直接从github上下载到本地&#xff0c;用vscode打开&#xff08;Open file&#xff09; 2、创建虚拟环境 python -m venv <name> <name>\Scripts\activate ps: 1、退出虚拟环境 deactivate 2、如果运行python -m venv <…

【数据结构】详解堆

一、堆的概念 堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵 完全二叉树的 数组对象。 堆是非线性数据结构&#xff0c;相当于一维数组&#xff0c;有两个直接后继。 如果有一个关键码的集合K { k₀&#xff0c;k₁&#xff0c;k₂ &#xff0…

统信UOS

统信UOS 如何检测是否为适配的cpu 打开国产系统或linux系统的终端&#xff0c; 输入&#xff1a; uname -m 如&#xff1a; 软件安装 把下载的压缩包解压到当前目录 如果您确认电脑cpu为ARM64架构CPU:【飞腾、鲲鹏、海思麒麟、瑞芯微】&#xff0c; 请双击com.agicall.ub-ph…

JVM--双亲委派模型

1.双亲委派模型 定义&#xff1a; 站在Java虚拟机的角度来看&#xff0c;只存在两种不同的类加载器&#xff1a;一种是启动类加载器&#xff08;Bootstrap ClassLoader&#xff09;&#xff0c;这个类加载器使用C语言实现&#xff0c;是虚拟机自身的一部分&#xff1b;另外一种…

【C++】一、Visual Studio 2017使用教程:内存窗口、预处理文件、obj文件,调试优化

文章目录 概述编译期&#xff08;Compile&#xff09;查看预处理后的文件查看obj文件开启编译器调试优化 链接期&#xff08;Linking&#xff09;报错信息概述自定义入口点 调试内存窗口值转16进制查看查看汇编代码 注意 概述 记录一下Cherno的vs配置下载地址 https://thecher…

Kubernetes集群安装步骤

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、安装要求 在开始之前&#xff0c;部署Kubernetes集群集群需要满足以下几个条件&#xff1a; 一台多多台机器&#xff0c;操作系统CentOS.x-86_x…

分布式锁、Lua脚本、redisson、运行lua脚本优化代码

20240721 一、分布式锁1. 什么是分布式锁2. 分布式锁的实现3. 基于redis的分布式锁4 总结 二、对于lua脚本可以保证事务&#xff0c;要么成功要么失败。1. 在redis中调用lua脚本 三、Redisson1 步骤2. Redisson的总结3. 几种分布式锁的区别 三、优化我们的秒杀1. 我们在创建优惠…

Mybatis-Plus代码生成器配置方法

Mybatis-Plus网址&#xff1a;https://baomidou.com/pages/779a6e/#%E4%BD%BF%E7%94%A8 第一步&#xff1a;引入依赖 <!-- 代码生成器 --> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId>…

IFIX6.5变量创建进阶-标签组用法

IFIX6.5变量创建进阶-标签组用法 普通变量创建 https://blog.csdn.net/hmxm6/article/details/140507111 OPC数据连接 项目介绍和应用场景 在实际项目中可能有好几个相同标签变量属于一类的 比如现在有三台风机&#xff0c;都有启动停止风量的三个数据 数据库里面需要创…

EI成稿丨明年上检索

基于xxxx嵌入式系统的无线传感网络路由优化算法研究基于网络安全xxxx全球互联网经济发展趋势分析基于深度学习的大数据xxxxx基于全IP物联网网络xxxxx安全管理中的优化研究xxxxx农业可持续发展评价及升级路径研究xxxxx分层轮廓数字化转型风险评估与防范策略基于Kano模型与xxxxx公…