
5.4 SpringApplication启动流程解析
Spring Boot项目通过运行启动类中的run()方法就可以将整个应用启动。那么这个方法究竟做了哪些神奇的事情呢?SpringApplication启动流程又做了哪些操作呢?接下来通过源码一探究竟。
点击启动类中的run()方法进入SpringApplication类,源码及注释如下所示:



Spring Boot项目启动步骤分析如下所示。
(1)实例化SpringApplication对象。
在执行run()方法前,使用new SpringApplication()构造SpringApplication对象。SpringApplication类的构造方法如下所示:

这一步主要是构造SpringApplication对象,并为SpringApplication的属性赋值,在构造完成后,开始执行run()方法。
比较重要的一个知识点是webApplicationType值的设置,其目的是获取当前应用的类型,对后续步骤构造容器环境和Spring容器的初始化起到作用。该值的获取是通过调用WebApplicationType.deduceFromClasspath()方法得到的,该方法源码及注释如下所示:



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()方法的源码如下所示:

这里会调用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所示。

图5-6 META-INF/spring.factories文件
通过debug模式也可以得出该类为org.springframework.boot.context. event.Event PublishingRunListener。在“listeners.starting();”代码前输入一个断点,之后通过debug模式启动项目,可以看出此时加载的listener为EventPublishingRunListener,如图5-7所示。

图5-7 EventPublishingRunListener类
(5)回调SpringApplicationRunListener对象的starting()方法。
(6)解析run()方法的args参数并封装为DefaultApplicationArguments类。
(7)prepareEnvironment()方法的作用与它的方法名的含义相同,就是为当前应用准备一个Environment对象,也就是运行环境。它主要完成对ConfigurableEnvironment的初始化工作。该方法的源码及解析如下所示:


由于项目中存在spring-boot-starter-web依赖,webApplicationType的值为WebApplicationType.SERVLET,所以getOrCreateEnvironment()方法返回的是StandardServletEnvironment对象,是一个标准的Servlet环境。StandardServletEnvironment是整个Spring Boot项目运行环境的实现类,后续关于环境的设置都基于此类。
在创建环境完成后,接下来是配置环境,configureEnvironment()方法的源码如下所示:

该方法主要加载一些默认配置,在执行完这一步骤后,会触发监听器(主要触发ConfigFileApplicationListener),将会加载application.properties或者application.yml配置文件。
(8)设置系统参数,configureIgnoreBeanInfo()方法的源码如下所示:

查看源码可知,该方法会获取spring.beaninfo.ignore配置项的值,即使未获取也没有关系。代码的最后还是给该配置项输入了一个默认值true,表示跳过对BeanInfo类的搜索,它无特别含义,不用深究该步骤。
(9)获取需要打印的Spring Boot启动Banner对象,源码如下所示:

首先判断当前是否允许打印Banner,默认会打印到控制台上,之后获取Banner对象。而Spring Boot目前支持图片Banner和文字Banner,如果开发人员做了Banner配置则会在控制台打印开发人员配置的Banner,否则打印默认Banner。默认Banner的实现类为org.springframework.boot.SpringBootBanner,源码如下所示:


BANNER变量就是在默认情况下打印在控制台上的Banner。而printBanner()方法,就是把定义好的Banner和Spring Boot的版本号打印出来。
其实在Banner打印流程中也能够看出Spring Boot框架约定优于配置的特性。开发人员配置Banner就使用开发人员配置的,如果没有,就使用Spring Boot默认的。
Spring Boot框架的约定优于配置理念正是“你配置就用你配置的,你不配置就用约定好的”。
(10)创建Spring容器ApplicationContext,createApplicationContext()方法的源码如下所示:


通过源码可以看出createApplicationContext()方法的执行逻辑:根据webApplicationType决定创建哪种contextClass。webApplicationType变量赋值的过程在前文中已经介绍过。因为该类型为WebApplicationType.SERVLET类型,所以会通过反射装载对应的字节码DEFAULF_SERVLET_WEB_CONTEXT_CLASS创建。创建的容器类型为AnnotationConfigServletWebServerApplicationContext,在后续步骤中的操作都会基于该容器。
(11)准备ApplicationContext实例,prepareContext()方法的源码如下所示:


在创建对应的Spring容器后,程序会进行初始化、加载主启动类等预处理工作。至此,主启动类加载完成,容器准备好。
(12)刷新容器,refreshContext()方法的源码如下所示:

程序首先注册一个Hook函数,然后调用refresh()方法,经过层层调用,程序执行ServletWebServerApplicationContext类中的refresh()方法,源码如下所示:


ServletWebServerApplicationContext会调用父类AbstractApplicationContext的refresh()方法,因此最终执行的refresh()方法源码如下所示:


该方法是Spring Bean加载的核心,用于刷新整个Spring上下文信息,定义整个Spring上下文加载的流程。其包括实例的初始化和属性设置、自动配置类的加载和执行、内置Tomcat服务器的启动等步骤。在后续章节中笔者也会结合源码对这些过程进行介绍。
(13)调用afterRefresh()方法,执行Spring容器初始化的后置逻辑,默认实现是一个空的方法:

(14)代码执行时间的监控停止,即知道了启动应用所花费的时间。
(15)发布容器启动事件。
(16)在ApplicationContext完成启动后,程序会对ApplicationRunner和CommandLineRunner进行回调处理,查找当前ApplicationContex中是否注册有CommandLineRunner,如果有,则遍历执行它们。
另外,在SpringApplication启动过程中,如果出现问题会由异常处理器接管,并对异常进行统一处理,源码如下所示:


本章讲解的源码都来自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框架的一个增强版的应用启动器。