![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=1739255863-IdJuz5ThAMMOEJkKHGZ6r4pFVu19idd2-0-210cfbea3ce0ad94420fc56eb782343e)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/085-1.jpg?sign=1739255863-CORhI3gKlGZ2QFS9BLKrswspHxpElLvs-0-37ef534079fc296d34aed37d5b175dce)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-1.jpg?sign=1739255863-eK1fiPEDmmvrrK8kFmNwC1Bfy6wJ88ho-0-39c9cb058ce15a0cebac881507bc3fcb)
Spring Boot项目启动步骤分析如下所示。
(1)实例化SpringApplication对象。
在执行run()方法前,使用new SpringApplication()构造SpringApplication对象。SpringApplication类的构造方法如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-2.jpg?sign=1739255863-wH6c14JPVld2wUQQMC726QxM5MPbzX0n-0-5ae357591adb2fd572fe970d624d4d0f)
这一步主要是构造SpringApplication对象,并为SpringApplication的属性赋值,在构造完成后,开始执行run()方法。
比较重要的一个知识点是webApplicationType值的设置,其目的是获取当前应用的类型,对后续步骤构造容器环境和Spring容器的初始化起到作用。该值的获取是通过调用WebApplicationType.deduceFromClasspath()方法得到的,该方法源码及注释如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-3.jpg?sign=1739255863-4H823y1bGVvaonObmsdWS3nPS7o5VjXx-0-1e66d69a95572f1690e25b6277c9277d)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/087-1.jpg?sign=1739255863-J8Ck9zfNahRqlSejl7MmkDPTSGC8EqRq-0-dd141ab9fc9aa51fb53ee5b5cdd14918)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/088-1.jpg?sign=1739255863-wefvw7s9OiaryQu0vy2jMWBZ9n6PaUsA-0-a4ce9b34710c9219a57d9975880cf612)
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=1739255863-AYopX96qazD1AIVXxKmMKlkzwtcX7mIb-0-72a7d02fb0d3e7982e96cca8172c1a8e)
这里会调用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=1739255863-NFsYqtIphb9Hz9sOnSwYukpvVRQSIqfe-0-10b4257b7b927bfc883e0c316e4a20cf)
图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=1739255863-69iZB2OaIT2Qip4fWSgZJDDo4Wk68dwJ-0-e352b84aed067552a3c1fa0dc9314600)
图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=1739255863-OB7thTd5SdEzMNkNDREaZKwaL7Yen4qf-0-8b8418452c7b85d8385bf2e479648610)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/091-1.jpg?sign=1739255863-l0JwPgWBrK6rQTMN7zSA1Cs5UVbTr1X4-0-0c37bf00db8ca6ee1bd91f7cf976fcae)
由于项目中存在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=1739255863-nHS3sLJ6PiJsPYchOjXZ5gbSUbeBcZPU-0-e3c34727a7b17cb0ffb42db0a3b1167b)
该方法主要加载一些默认配置,在执行完这一步骤后,会触发监听器(主要触发ConfigFileApplicationListener),将会加载application.properties或者application.yml配置文件。
(8)设置系统参数,configureIgnoreBeanInfo()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/091-3.jpg?sign=1739255863-WbOwCKUm5sH0uOqIQqP0Se0sDgS6vEne-0-2cacbf9be2f67ffeccdb675acbe7a39a)
查看源码可知,该方法会获取spring.beaninfo.ignore配置项的值,即使未获取也没有关系。代码的最后还是给该配置项输入了一个默认值true,表示跳过对BeanInfo类的搜索,它无特别含义,不用深究该步骤。
(9)获取需要打印的Spring Boot启动Banner对象,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/092-1.jpg?sign=1739255863-GyrzUntp9CExpeTZRRl48PmU0oc3suoK-0-a2f769b6e8474f1b08fff28a421309a2)
首先判断当前是否允许打印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=1739255863-oQdgbpINpZDRITCBBvMaW4p9iwIKVahJ-0-3c4907d962770ed553696b455dd1b88a)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/093-1.jpg?sign=1739255863-IlVZ98A6Xt1sGSaUu554S7uru8TWbWs6-0-0f8337dfaba6432c706c4ed11c4c8346)
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=1739255863-VEdxsiWOjohCrXgUCojiQGSqA2bOuWg1-0-24fdf5bbd80feb3ac5faf67fcdd94760)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/094-1.jpg?sign=1739255863-2Mh4vssdMhDAGnA53J3cEOXq7f0tQL6R-0-3fa7bb5a13ba761e9cf92132cd8339f9)
通过源码可以看出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=1739255863-j088ml4CJZIHVwH3eKHH8Eo1CTkXFRVG-0-6f5d056bfc6299e667c67be26adb58d8)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-1.jpg?sign=1739255863-Xe8yV1LWvskPTnB047o4LXBYhbG4xV2P-0-77b51cf1e0328a9190fb5a6372cdce77)
在创建对应的Spring容器后,程序会进行初始化、加载主启动类等预处理工作。至此,主启动类加载完成,容器准备好。
(12)刷新容器,refreshContext()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-2.jpg?sign=1739255863-E1pX321FF81KW8wk5oDQeLSjprA0aPya-0-2587bffa27d491574d9626fefff064d8)
程序首先注册一个Hook函数,然后调用refresh()方法,经过层层调用,程序执行ServletWebServerApplicationContext类中的refresh()方法,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-3.jpg?sign=1739255863-bOodyS24xJCFsw3yOaK72Ezcnq43nU1g-0-a374b23b7e73c0bd08d0d341d2587664)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/097-1.jpg?sign=1739255863-G7CSGd1JgHMgc5YMA5PhuZo66jNtJZFv-0-99d0e5b7bed63d85362c3e3cd69dfc16)
ServletWebServerApplicationContext会调用父类AbstractApplicationContext的refresh()方法,因此最终执行的refresh()方法源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/097-2.jpg?sign=1739255863-pdHy09zloMsyBcdEMkWkTfVWiPkgnxxM-0-82d5acdc0a28475bb9e73178d0cccc7b)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-1.jpg?sign=1739255863-DAOOGD6RGDAazVj4dTKwbgj5MinG2DIw-0-42d308903f3fbb7c3f89289421975542)
该方法是Spring Bean加载的核心,用于刷新整个Spring上下文信息,定义整个Spring上下文加载的流程。其包括实例的初始化和属性设置、自动配置类的加载和执行、内置Tomcat服务器的启动等步骤。在后续章节中笔者也会结合源码对这些过程进行介绍。
(13)调用afterRefresh()方法,执行Spring容器初始化的后置逻辑,默认实现是一个空的方法:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-2.jpg?sign=1739255863-e02xJpGKLHhwy9OQZjCqTFAL9A3qoiyw-0-7086d17b137bb6c13a017d9d5bf8efba)
(14)代码执行时间的监控停止,即知道了启动应用所花费的时间。
(15)发布容器启动事件。
(16)在ApplicationContext完成启动后,程序会对ApplicationRunner和CommandLineRunner进行回调处理,查找当前ApplicationContex中是否注册有CommandLineRunner,如果有,则遍历执行它们。
另外,在SpringApplication启动过程中,如果出现问题会由异常处理器接管,并对异常进行统一处理,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-3.jpg?sign=1739255863-6wyQlSoVCYmm9LXga89NIAJJb7436zLc-0-24de55ef6e2834a7533954bf62834028)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/099-1.jpg?sign=1739255863-J3nJHt4DGX8ZSIScbDzsNHmHOE0uzYox-0-0117826610ffc8657daea17ab10a345a)
本章讲解的源码都来自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框架的一个增强版的应用启动器。