![Spring Boot实战:从0开始动手搭建企业级项目](https://wfqqreader-1252317822.image.myqcloud.com/cover/850/40107850/b_40107850.jpg)
5.4 SpringApplication启动流程解析
Spring Boot项目通过运行启动类中的run()方法就可以将整个应用启动。那么这个方法究竟做了哪些神奇的事情呢?SpringApplication启动流程又做了哪些操作呢?接下来通过源码一探究竟。
点击启动类中的run()方法进入SpringApplication类,源码及注释如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/084-2.jpg?sign=1739597752-pJFs485PfrnFnanwwd04PiraQ3nlCBuF-0-87d1d700a11351c44fa2282ea26e7035)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/085-1.jpg?sign=1739597752-PZVozh0wi1J4efZe49g8d0XsmnBAWLyb-0-927adc61997e6ec1d972483ccfd3df7a)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-1.jpg?sign=1739597752-r4ZehhlTJYTaTndrONhtSHNQsHts2Ciq-0-c6832f41cf301eb3b763a543c16c9e02)
Spring Boot项目启动步骤分析如下所示。
(1)实例化SpringApplication对象。
在执行run()方法前,使用new SpringApplication()构造SpringApplication对象。SpringApplication类的构造方法如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-2.jpg?sign=1739597752-fEBBdk0MKsNBxZVGRzvGAF1SoQUtIRAh-0-f053d5c4df0fd8185aeb1aa01c40895c)
这一步主要是构造SpringApplication对象,并为SpringApplication的属性赋值,在构造完成后,开始执行run()方法。
比较重要的一个知识点是webApplicationType值的设置,其目的是获取当前应用的类型,对后续步骤构造容器环境和Spring容器的初始化起到作用。该值的获取是通过调用WebApplicationType.deduceFromClasspath()方法得到的,该方法源码及注释如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-3.jpg?sign=1739597752-BbSBlAibqX9RbAsnZ7up1EYxLT2aN6Hs-0-eee8e1c39fa09b085ec30e29c3ac4059)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/087-1.jpg?sign=1739597752-5fiqeBjQ7unvTb9lcqkb8IvBJSx3lZBp-0-14f19872e507b5b1ae1403e938d3f50e)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/088-1.jpg?sign=1739597752-eDia6A3wJeNUb6T35pV7cwNELitOpqcp-0-ffdcb67867453253c6a2b3018bb10fe8)
WebApplicationType的值有3个,分别如下所示。
①SERVLET:Servlet环境。
②REACTIVE:Reactive环境。
③NONE:非Web环境。
在deduceFromClasspath()方法中代码多次调用ClassUtils.isPresent()方法,以此判断在常量中的类是否存在。该方法的最终实现原理通过Class.forName加载某个类,如果成功加载,则证明这个类存在,反之则代表该类不存在。
deduceFromClasspath()方法的实现逻辑如下:先判断webflux相关的类是否存在,存在则认为当前应用为REACTIVE类型;不存在则继续判断SERVLET相关的类是否存在,都不存在则为NONE类型;否则,当前应用为SERVLET类型。具体的类加载判断方法可以直接查看源码,相关的代码注释笔者也已经标注在代码中。
以newbee-mall项目举例,由于项目中引用了spring-boot-starter-web且并未引用webflux相关的类,所以newbee-mall项目类型为SERVLET类型。
(2)开始执行run()方法,代码执行时间的监控开启,在Spring Boot应用启动成功后会打印启动时间。
(3)配置headless属性,java.awt.headles是J2SE的一种模式,用于在缺失显示屏、鼠标或者键盘时的系统配置,默认为true。通俗而言,该行代码的作用是Spring Boot应用在启动时,没有检测到显示器也能够继续执行后面的步骤。
(4)获取SpringApplicationRunListeners,getRunListeners()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/089-1.jpg?sign=1739597752-KuLvMNKXDWIqTA2POaAcso8giCMTyMXL-0-e33caf5ba6e0d4d687223d0cb7ad1a39)
这里会调用SpringFactoriesLoader类中的loadFactoryNames()方法。该方法在介绍自动配置时已经讲解过,与获取自动配置类的类名相同。也就是在getRunListeners()方法中调用该方法是从类路径META-INF/spring.factories中获取SpringApplication RunListener指定类的。在spring-boot-2.3.7.RELEASE.jar包中的META-INF目录下找到了spring.factories文件,当前文件中只有一个RunListener,即org.springframework. boot.context.event.EventPublishingRunListener,如图5-6所示。
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/089-2.jpg?sign=1739597752-BDQRINWKvQqfPxC5iXOjVbYqLxQAvQkM-0-11eb2cf6585b42274cd541ee7d7ffa24)
图5-6 META-INF/spring.factories文件
通过debug模式也可以得出该类为org.springframework.boot.context. event.Event PublishingRunListener。在“listeners.starting();”代码前输入一个断点,之后通过debug模式启动项目,可以看出此时加载的listener为EventPublishingRunListener,如图5-7所示。
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/090-1.jpg?sign=1739597752-pc02fqKw5xtJ3vlyCWOXqibHti9pXSSx-0-097ffd4e6a1d0c1cc624430c65e15486)
图5-7 EventPublishingRunListener类
(5)回调SpringApplicationRunListener对象的starting()方法。
(6)解析run()方法的args参数并封装为DefaultApplicationArguments类。
(7)prepareEnvironment()方法的作用与它的方法名的含义相同,就是为当前应用准备一个Environment对象,也就是运行环境。它主要完成对ConfigurableEnvironment的初始化工作。该方法的源码及解析如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/090-2.jpg?sign=1739597752-vNnIQn59G6K2T6lXBST00bfzPWUCDzNu-0-78eff7e3632459c9d11c675672652ab3)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/091-1.jpg?sign=1739597752-PffUfNIfVeJyh2WHrSQDIe2rCmhkN2EN-0-2e0b5fad634dffed6ecb4a67be34210e)
由于项目中存在spring-boot-starter-web依赖,webApplicationType的值为WebApplicationType.SERVLET,所以getOrCreateEnvironment()方法返回的是StandardServletEnvironment对象,是一个标准的Servlet环境。StandardServletEnvironment是整个Spring Boot项目运行环境的实现类,后续关于环境的设置都基于此类。
在创建环境完成后,接下来是配置环境,configureEnvironment()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/091-2.jpg?sign=1739597752-MPDuO8l67hYZv6Eg4C8vNt2jMmdZfsya-0-115a5755fdaa95c8eac4f5c1d8a8ec22)
该方法主要加载一些默认配置,在执行完这一步骤后,会触发监听器(主要触发ConfigFileApplicationListener),将会加载application.properties或者application.yml配置文件。
(8)设置系统参数,configureIgnoreBeanInfo()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/091-3.jpg?sign=1739597752-3RJnqGhym1WqOLjc8w5vz68rr713NT8G-0-2255ab816b8b3f4b4de461b916370ad4)
查看源码可知,该方法会获取spring.beaninfo.ignore配置项的值,即使未获取也没有关系。代码的最后还是给该配置项输入了一个默认值true,表示跳过对BeanInfo类的搜索,它无特别含义,不用深究该步骤。
(9)获取需要打印的Spring Boot启动Banner对象,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/092-1.jpg?sign=1739597752-FJhK4ByrAkYDTkaGc6O19CkSuzBpQQRS-0-2bf5b8f9223b51acb80ff5c254d61ecb)
首先判断当前是否允许打印Banner,默认会打印到控制台上,之后获取Banner对象。而Spring Boot目前支持图片Banner和文字Banner,如果开发人员做了Banner配置则会在控制台打印开发人员配置的Banner,否则打印默认Banner。默认Banner的实现类为org.springframework.boot.SpringBootBanner,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/092-2.jpg?sign=1739597752-prEFuW2SnU6u0LQFKkYfKK8QovSaNfpw-0-b54fa2f7cec3e14c2ebd4d8726cd1807)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/093-1.jpg?sign=1739597752-DoQejfVOQcL1WsdBszoYxjkqhA3AzdB8-0-52d3fc73ea0f8259664dda2c1d937958)
BANNER变量就是在默认情况下打印在控制台上的Banner。而printBanner()方法,就是把定义好的Banner和Spring Boot的版本号打印出来。
其实在Banner打印流程中也能够看出Spring Boot框架约定优于配置的特性。开发人员配置Banner就使用开发人员配置的,如果没有,就使用Spring Boot默认的。
Spring Boot框架的约定优于配置理念正是“你配置就用你配置的,你不配置就用约定好的”。
(10)创建Spring容器ApplicationContext,createApplicationContext()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/093-2.jpg?sign=1739597752-mXEBZtLdkajrRiUjeYJuQ1TlluORT9Ue-0-94db4dd0578eb9cb4c7a8f82c6db4553)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/094-1.jpg?sign=1739597752-o2gAJVayyLj8wzKEz5sLiQK4YLkyaohX-0-62ec89ba615cb0caec87272bf2e28f2d)
通过源码可以看出createApplicationContext()方法的执行逻辑:根据webApplicationType决定创建哪种contextClass。webApplicationType变量赋值的过程在前文中已经介绍过。因为该类型为WebApplicationType.SERVLET类型,所以会通过反射装载对应的字节码DEFAULF_SERVLET_WEB_CONTEXT_CLASS创建。创建的容器类型为AnnotationConfigServletWebServerApplicationContext,在后续步骤中的操作都会基于该容器。
(11)准备ApplicationContext实例,prepareContext()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/095-1.jpg?sign=1739597752-aCyAzDWaL7iw6ml0hsLzjMQ4fQJdbwYk-0-437e1aac074d2448f44f828dc5fce8ec)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-1.jpg?sign=1739597752-B7fFSODSweDTBdykLOtEJf0RCZzAvXj9-0-a5de4db32bc0de0a1d6f305af87452e0)
在创建对应的Spring容器后,程序会进行初始化、加载主启动类等预处理工作。至此,主启动类加载完成,容器准备好。
(12)刷新容器,refreshContext()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-2.jpg?sign=1739597752-V9LAwneU6HWiKCdtJEKpZw4ksy45Cqyc-0-8853e5dc56091c9866fed6986c8fa0a6)
程序首先注册一个Hook函数,然后调用refresh()方法,经过层层调用,程序执行ServletWebServerApplicationContext类中的refresh()方法,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-3.jpg?sign=1739597752-OvvPDFGQMYVgLwQHwfQ61od3RHApqy0r-0-12096c97b4e8558e2f4147c11cce41a6)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/097-1.jpg?sign=1739597752-AR0nwROqRSVUqEIeU3DgUqjdLkeZXlZo-0-6d277d06937d2b2a082badc3fe00dcb2)
ServletWebServerApplicationContext会调用父类AbstractApplicationContext的refresh()方法,因此最终执行的refresh()方法源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/097-2.jpg?sign=1739597752-viJpagx4sW1SsPugLCfjOSjAkTE3Xlbh-0-99dd200f01827bebf85a79aee4c65a36)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-1.jpg?sign=1739597752-vh7rWleqWAdhDb0Kt3x7Z9xAmCrQ5H94-0-a1dea4d98dfa8f07c0dc7174f6d732fd)
该方法是Spring Bean加载的核心,用于刷新整个Spring上下文信息,定义整个Spring上下文加载的流程。其包括实例的初始化和属性设置、自动配置类的加载和执行、内置Tomcat服务器的启动等步骤。在后续章节中笔者也会结合源码对这些过程进行介绍。
(13)调用afterRefresh()方法,执行Spring容器初始化的后置逻辑,默认实现是一个空的方法:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-2.jpg?sign=1739597752-b3yDPE5PanVpyX2o5GcumFE8Cv8DN5XN-0-6344a7a3353a2a4bf8a2f1ebb6989587)
(14)代码执行时间的监控停止,即知道了启动应用所花费的时间。
(15)发布容器启动事件。
(16)在ApplicationContext完成启动后,程序会对ApplicationRunner和CommandLineRunner进行回调处理,查找当前ApplicationContex中是否注册有CommandLineRunner,如果有,则遍历执行它们。
另外,在SpringApplication启动过程中,如果出现问题会由异常处理器接管,并对异常进行统一处理,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-3.jpg?sign=1739597752-jK1gnNQe2BdSs0D3puug0NNox22DxVQE-0-a72abc9e0c059310b815e91a2b9f3032)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/099-1.jpg?sign=1739597752-vtCOBUaiD1XRzhU7kS5uWNVz9ZZ00JU6-0-2618b9c3696168dbbbe329c1a5b838d7)
本章讲解的源码都来自Spring Boot2.3.7.RELEASE版本,它与其他版本的代码可能有些不同。读者想更好地理解Spring Boot及其启动过程的原理,可以参考本章给出的提示并自行通过debug模式进行调试。理论结合实践才能更好地理解Spring Boot在启动过程中的操作。
通过源码解读和启动流程的介绍,相信读者对于Spring Boot框架有了进一步的认识。Spring Boot的核心依然是Spring。它只是在Spring框架的基础之上,针对Spring应用启动流程进行了规范和封装。Spring的核心启动方法是refresh(),Spring Boot在启动时依然会调用该核心方法。在平时的Spring项目开发中,这些组件通常是通过XML配置文件进行定义和装载的,而Spring Boot将该过程简化并通过自动配置的方式实现该过程,减少了开发人员需要做的配置工作量。它更像是基于Spring框架的一个增强版的应用启动器。