![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=1739571471-esGr44z4oQtxdX9HEVLflCRLPlXhhskb-0-5c075ad4f728c1e7f8b3f97b4dc67261)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/085-1.jpg?sign=1739571471-oJCRHjzjhncUHn0H8A8g9iGNCTARz9Ok-0-f742b379d78e9a46e802e5db0708f45a)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-1.jpg?sign=1739571471-Jqei765V1Qthoa6FoTOZJFOtZIxBfDHF-0-56fa9050245fd1f094c1894f472aa37a)
Spring Boot项目启动步骤分析如下所示。
(1)实例化SpringApplication对象。
在执行run()方法前,使用new SpringApplication()构造SpringApplication对象。SpringApplication类的构造方法如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-2.jpg?sign=1739571471-a5Em4EJyTaEf3gcdCuXLR0f7cGGBUKjA-0-327e33016dfc49a25072b8880597f950)
这一步主要是构造SpringApplication对象,并为SpringApplication的属性赋值,在构造完成后,开始执行run()方法。
比较重要的一个知识点是webApplicationType值的设置,其目的是获取当前应用的类型,对后续步骤构造容器环境和Spring容器的初始化起到作用。该值的获取是通过调用WebApplicationType.deduceFromClasspath()方法得到的,该方法源码及注释如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/086-3.jpg?sign=1739571471-sWboimWBkTMb6EFq7RNaMAs0AqKqHZcf-0-7162016ca563ff020626461c638e1afe)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/087-1.jpg?sign=1739571471-OyLBIIfOQsCtuGgKXUdNZq1R16N9KNrD-0-54798e3f013726cec9ddf93a41b5fc16)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/088-1.jpg?sign=1739571471-WRxQigwvrauUVXZ2m2FqVFDjn3RtG82H-0-08ea748e39def8fc8e86732be6c14e84)
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=1739571471-CoW1RYGSTECqE0mfEvGxYit9L3a9Hdyo-0-cb48cac58e0832e4ef8f855e8a087140)
这里会调用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=1739571471-0sewkrHagkbHAt7XpdTOf9h2vUXvuXLj-0-4433ed3b7c3c234c99ef087586bb37b9)
图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=1739571471-kXQ0deROgJ61xwRVEbnNwY50rQRsmqjh-0-e9d31915685fec329b852c86af3e45ac)
图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=1739571471-dWrd8UQ782yMgdf7H6ymoNcKW7lf8Ykt-0-698c0b2a9af25aaf33d3e3001a03d2dc)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/091-1.jpg?sign=1739571471-uBuGfc4waKXFLyumFCbijvgg3BC7xjsR-0-05cbd5878f58b13fa41ea7ccc029524c)
由于项目中存在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=1739571471-N061p5VRrwE4RDpAslPBMA5lg56Pioes-0-d283d96bbdcc1fb1251def37893cf772)
该方法主要加载一些默认配置,在执行完这一步骤后,会触发监听器(主要触发ConfigFileApplicationListener),将会加载application.properties或者application.yml配置文件。
(8)设置系统参数,configureIgnoreBeanInfo()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/091-3.jpg?sign=1739571471-dCN2MrXpFHfGqknz8va8jxppyBQkzSFf-0-340f008c011571bd2fb3ffdd252454e0)
查看源码可知,该方法会获取spring.beaninfo.ignore配置项的值,即使未获取也没有关系。代码的最后还是给该配置项输入了一个默认值true,表示跳过对BeanInfo类的搜索,它无特别含义,不用深究该步骤。
(9)获取需要打印的Spring Boot启动Banner对象,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/092-1.jpg?sign=1739571471-yARfvKaPppZDK8YjzBRAOaRrUIS1tRlS-0-fe66b61d841dab254e31c016dcb81dde)
首先判断当前是否允许打印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=1739571471-dUJ4N6IO8JLPja0e4TXSGl63sAZNj9BZ-0-74888427808620a87d337cbaa16d2460)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/093-1.jpg?sign=1739571471-rzbeDMbRTh1NOM47ndHmYxxVIr1lkVeO-0-f07f26e8331660738d70af3bff3e6d3a)
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=1739571471-a8AZ48JooWAa0cDUslfR6nUeTKIFdqgB-0-ad63db413d7af14f81c85944b36239b1)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/094-1.jpg?sign=1739571471-KG78TElIU74EoU4VvceAgfgULVoeNFdn-0-daee3db7bda5a05e2a2f482e57dab2cc)
通过源码可以看出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=1739571471-8Hkv5fdOAiaQIZwcbNbyfehXZmfpdcgF-0-ae992300e82f438417ae779c643025c7)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-1.jpg?sign=1739571471-BCAOWtbtQyzZf1NJ04evTw4ozoiOf9BX-0-091e93a22399768b6ca204076160df9a)
在创建对应的Spring容器后,程序会进行初始化、加载主启动类等预处理工作。至此,主启动类加载完成,容器准备好。
(12)刷新容器,refreshContext()方法的源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-2.jpg?sign=1739571471-D6qQ7xXGMgUNJk5ROPNqDp87ARJ4ea1z-0-f2baa12864342bdd425d29afb87fa7eb)
程序首先注册一个Hook函数,然后调用refresh()方法,经过层层调用,程序执行ServletWebServerApplicationContext类中的refresh()方法,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/096-3.jpg?sign=1739571471-X0B8YzDj25CESSOmj0chfaH6gD1JePG6-0-696c18d2432a9584e0a1dcf72e4d3002)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/097-1.jpg?sign=1739571471-UmojIVA0uKRLkcauWImFTTmeeGi094ZU-0-6c9e31d14d25cd31462c8a082aad1ee7)
ServletWebServerApplicationContext会调用父类AbstractApplicationContext的refresh()方法,因此最终执行的refresh()方法源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/097-2.jpg?sign=1739571471-6FcZS45ID95lzKAmicEWJfmHdFrmi0dB-0-d2c576a5a91be3a35ca7c3cbbd15d23f)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-1.jpg?sign=1739571471-J9s3i2qMJAOh7IGj6cyzjKd85bOl1qgy-0-846d15c0056fd1437eb9ae46dc26e18a)
该方法是Spring Bean加载的核心,用于刷新整个Spring上下文信息,定义整个Spring上下文加载的流程。其包括实例的初始化和属性设置、自动配置类的加载和执行、内置Tomcat服务器的启动等步骤。在后续章节中笔者也会结合源码对这些过程进行介绍。
(13)调用afterRefresh()方法,执行Spring容器初始化的后置逻辑,默认实现是一个空的方法:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-2.jpg?sign=1739571471-g3p8dn3i8Ak9mH7BJYDtG8jNIJbVPwrE-0-b89795bc09dd74de551bdd581c691422)
(14)代码执行时间的监控停止,即知道了启动应用所花费的时间。
(15)发布容器启动事件。
(16)在ApplicationContext完成启动后,程序会对ApplicationRunner和CommandLineRunner进行回调处理,查找当前ApplicationContex中是否注册有CommandLineRunner,如果有,则遍历执行它们。
另外,在SpringApplication启动过程中,如果出现问题会由异常处理器接管,并对异常进行统一处理,源码如下所示:
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/098-3.jpg?sign=1739571471-HjztPGxKDlDi8wXSUGizaiQLglLY5TsW-0-bb73ff4a2de204f902e720ee7559b1d8)
![](https://epubservercos.yuewen.com/3069E4/20862583308964806/epubprivate/OEBPS/Images/099-1.jpg?sign=1739571471-6Oq0ChgRTaetw6cLdGdQtlfjXDLIgzax-0-b0d2e00058858f82dd5c00c0c7c2063a)
本章讲解的源码都来自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框架的一个增强版的应用启动器。