当前位置: 首页 > news >正文

SpringBoot源码深度解析

        今天,聊聊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框架的源码算是讲完了,我个人觉得应该是讲的很全面的,如果在讲解的过程中,有漏讲或者讲错的,欢迎指出,感谢~

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • # Redis 入门到精通(九)-- 主从复制(1)
  • buu做题(6)
  • 时间卷积网络(TCN):序列建模的强大工具(附Pytorch网络模型代码)
  • 入门 git
  • MySQL:SELECT 语句
  • Android 11 HAL层集成FFMPEG
  • Flink源码学习资料
  • 机体坐标系和导航坐标系
  • 【中项】系统集成项目管理工程师-第2章 信息技术发展-2.1信息技术及其发展-2.1.1计算机软硬件与2.1.2计算机网络
  • springboot防止重复提交的方案有哪些
  • [2019红帽杯]Snake
  • 纯前端导出xlsx表格
  • 深入理解并使用 MySQL 的 SUBSTRING_INDEX 函数
  • STM32中PC13引脚可以当做普通引脚使用吗?如何配置STM32的TAMPER?
  • docker搭建普罗米修斯监控gpu
  • 时间复杂度分析经典问题——最大子序列和
  • “大数据应用场景”之隔壁老王(连载四)
  • 30秒的PHP代码片段(1)数组 - Array
  • bearychat的java client
  • C++类中的特殊成员函数
  • Dubbo 整合 Pinpoint 做分布式服务请求跟踪
  • ECMAScript入门(七)--Module语法
  • JAVA SE 6 GC调优笔记
  • MySQL的数据类型
  • Object.assign方法不能实现深复制
  • Python代码面试必读 - Data Structures and Algorithms in Python
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • 百度小程序遇到的问题
  • 大快搜索数据爬虫技术实例安装教学篇
  • 后端_ThinkPHP5
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 利用DataURL技术在网页上显示图片
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 如何用纯 CSS 创作一个货车 loader
  • # Java NIO(一)FileChannel
  • #define,static,const,三种常量的区别
  • #laravel部署安装报错loadFactoriesFrom是undefined method #
  • #我与Java虚拟机的故事#连载16:打开Java世界大门的钥匙
  • $.ajax,axios,fetch三种ajax请求的区别
  • (09)Hive——CTE 公共表达式
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (Java企业 / 公司项目)点赞业务系统设计-批量查询点赞状态(二)
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (笔试题)分解质因式
  • (附源码)计算机毕业设计SSM疫情居家隔离服务系统
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (学习日记)2024.02.29:UCOSIII第二节
  • (转)EOS中账户、钱包和密钥的关系
  • (轉貼) UML中文FAQ (OO) (UML)
  • (轉貼) 寄發紅帖基本原則(教育部禮儀司頒布) (雜項)
  • 、写入Shellcode到注册表上线
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • .mkp勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .Net Core webapi RestFul 统一接口数据返回格式