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

【Spring】Spring Boot启动过程源码解析

目录

一、启动入口

二、SpringApplication的构造过程

2.1 设置应用类型

2.2 设置初始化器(Initializer)

2.2.1 获取BootstrapRegistryInitializer对象

2.2.2 获取ApplicationContextInitializer对象

2.3 设置监听器(Listener)

2.4 总结

三、SpringApplication的执行:执行run(String... args)方法

3.1 run方法第一步:获取并启动监听器

3.1.1 如何获取运行监听器?

3.1.2 如何启动运行监听器?

3.1.3 总结

3.2 run方法第二步:环境搭建,解析配置文件

3.2.1 配置文件解析流程图

3.2.2 Spring Boot的配置优先级

3.3 run方法第三步:打印banner

3.4 run方法第四步:创建Spring IoC容器

3.5 run方法第五步:Spring IoC容器前置处理

3.5.1 调用初始化器

3.5.1.1 利用ApplicationContextInitializer初始化Spring容器对象

3.5.2 触发SpringApplicationRunListener的contextPrepared()

3.5.3 调用DefaultBootstrapContext对象的close()

3.5.4 判断是否开启懒加载

3.5.5 加载启动类,注入容器

3.5.6 触发SpringApplicationRunListener的contextLoaded()

3.6 run方法第六步:刷新容器【关键】

3.7 run方法第七步:spring容器后置处理 afterRefresh()

3.8 run方法第八步:发出结束执行的事件

3.9 run方法第九步:执行Runners

3.10 总结

四、Spring Boot启动过程流程图


我们在使用Spring Boot启动项目的时候,可能只需加一个注解,然后启动main,整个项目就运行了起来,但事实真的是所见即所得吗,还是Spring Boot在背后默默做了很多?本文会通过源码解析的方式深入理解Spring Boot启动全过程。

首先要明确一个事情,不要妄想把Spring Boot启动过程步骤全记下来,那是不可能的,神仙也记不住,我们重点是要理解启动的步骤,可以做到看到源码就知道这是怎么实现的,遇到某些问题知道能够在哪个步骤定位和解决就可以了。

一、启动入口

大家不要抗拒源码解析,这个非常优秀的代码,我们如果能够学会对自己代码编写水平大有裨益。

相信很多人尝试读过Spring Boot​的源码,但是始终没有找到合适的方法。那是因为你对Spring Boot的各个组件、机制不是很了解,研究起来就像大海捞针。

至于从哪入手不是很简单的问题吗,当然主启动类了,即是标注着@SpringBootApplication​注解并且有着main()方法的类,如下一段代码:

@SpringBootApplication
public class SpringDemoApplication {// 启动main方法public static void main(String[] args) {// 这里传入的是配置类,我们习惯的方法就是把启动类和配置类写成一个,但是其实这里也可以将传入的配置类设置为其他类,和启动类分开,并不是强制要求要写在一起的SpringApplication.run(SpringDemoApplication.class, args);}
}

一个是@SpringBootApplication,这个注解和自动配置有关,参考另一篇文章Spring Boot 自动配置原理分析。

另一个关键点是SpringApplication.run()方法,这是一个静态方法,我们详细看下代码:

/*** 静态方法*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class<?>[]{primarySource}, args);
}
  • primarySource - 要载入的主要源,即指定源,这里为传入的Application.class Class<?> :泛型决定了任何类都可以传入
  • args - 应用程序参数(通常从main方法传递)
  • 返回:正在运行的ApplicationContext

SpringApplication​中的静态run()方法并不是一步完成的,最终执行的源码如下:

/*** 调用此方法启动会使用默认设置和用户提供的参数args*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {// 实例化SpringApplication,然后调用runreturn new SpringApplication(primarySources).run(args);
}

很显然分为两个步骤,分别是创建SpringApplication执行run()方法,下面将分为这两个部分介绍。

二、SpringApplication的构造过程

可以看到代码new SpringApplication(),new了一个这个对象,然后调用run,我们先看看SpringApplication构造函数:

public SpringApplication(Class<?>... primarySources) {this(null, primarySources);
}public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");// 把SpringDemoApplication作为primarySources属性存储起来this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 从classpath中推断是否为web应用,也就是设置应用类型是Standard还是Webthis.webApplicationType = WebApplicationType.deduceFromClasspath();// 获取启动加载器this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));// 设置初始化器(Initializer),在启动的过程中会调用这些功能setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 设置一系列监听器(Listener),在启动过程中会触发setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 获取main方法所在的类,没什么具体的作用,逻辑是根据当前线程的调用栈来判断main()方法在哪个类,哪个类就是Main类this.mainApplicationClass = deduceMainApplicationClass();
}

基本就是做如下几件事情:

  1. 配置primarySources(这个就是传入的配置类,这个配置类就是@SpringBootApplication注解修饰的类,会将一些类自动配置到Spring Boot中)
  2. 配置环境是否为web环境
  3. 创建初始化构造器setInitializers
  4. 创建应用监听器
  5. 配置应用主方法所在类(就是main方法所在类,即启动类),准备启动run()方法

如下图中标注的注释,创建SpringApplication过程重要的其实就分为②​、③​、④这三个阶段,下面将会一一介绍每个阶段做了什么事。

2.1 设置应用类型

这个过程非常重要,直接决定了项目的类型,应用类型分为三种,都在WebApplicationType这个枚举类中,如下:

  1. NONE​:顾名思义,什么都没有,正常流程走,不额外的启动web容器​,即普通的Java程序。
  2. SERVLET​:基于servlet​的web程序,需要启动内嵌的servlet​web容器,比如Tomcat。
  3. REACTIVE​:基于reactive​的web程序,需要启动内嵌reactiveweb容器,作者不是很了解,不便多说。

判断的依据很简单,就是看是否加载了对应的类,比如加载了DispatcherServlet​等则会判断是Servlet的web程序。源码如下:

static WebApplicationType deduceFromClasspath() {/*** 检查是否为基于reactive​的web程序:* 这个判断逻辑用来检查类路径中是否存在WebFlux的指示类(`WEBFLUX_INDICATOR_CLASS`),* 同时确保Spring MVC的指示类(`WEBMVC_INDICATOR_CLASS`)和Jersey的指示类(`JERSEY_INDICATOR_CLASS`)不存在。* 如果满足这些条件,说明当前应用是一个反应式应用,方法返回`WebApplicationType.REACTIVE`。*/if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {return WebApplicationType.REACTIVE;}/*** 检查是否为非Web项目:* 这部分逻辑通过遍历一个包含Servlet指示类名称的数组(`SERVLET_INDICATOR_CLASSES`)。* 如果类路径中缺少任何一个指示类,那么推断结果为非Web应用,即`WebApplicationType.NONE`。*/for (String className : SERVLET_INDICATOR_CLASSES) {if (!ClassUtils.isPresent(className, null)) {return WebApplicationType.NONE;}}/*** 检查是否为基于servlet​的web程序:* 如果上面遍历完成后没有返回`WebApplicationType.NONE`,* 则说明类路径中存在所有指示类,可以认为当前应用是基于Servlet的Web应用,* 因此方法返回`WebApplicationType.SERVLET`。*/return WebApplicationType.SERVLET;
}

这段代码的核心作用是根据类路径中的类来动态推断Spring Boot应用的类型。这是通过检查特定的类是否存在来实现的:

  1. 如果项目依赖中存在org.springframework.web.reactive.DispatcherHandler,并且不存在org.springframework.web.servlet.DispatcherServlet,那么应用类型为WebApplicationType.REACTIVE
  2. 如果项目依赖中不存在org.springframework.web.reactive.DispatcherHandler,也不存在org.springframework.web.servlet.DispatcherServlet,那么应用类型为WebApplicationType.NONE
  3. 否则,应用类型为WebApplicationType.SERVLET

这种机制使得Spring Boot可以灵活地适配不同类型的Web应用开发,无论是传统的Servlet应用、Spring MVC应用,还是反应式Web应用,Spring Boot都能提供合适的配置和环境。

这里我引入了spring-boot-starter-web​,肯定是Servlet的web程序。

2.2 设置初始化器(Initializer

这里我们将一下设置的两个初始化器:BootstrapRegistryInitializer对象和ApplicationContextInitializer对象。

2.2.1 获取BootstrapRegistryInitializer对象

由上面的源码我们可以看出,是先获取BootstrapRegistryInitializer初始化器,再获取ApplicationContextInitializer初始化器。

执行流程如下:

  1. 从"META-INF/spring.factories"中读取key为BootstrapRegistryInitializer类型的扩展点,并实例化出对应扩展点对象
  2. BootstrapRegistryInitializer的作用是可以初始化BootstrapRegistry
  3. 源码中的DefaultBootstrapContext对象就是一个BootstrapRegistry,可以用来注册一些对象,这些对象可以用在从Spring Boot启动到Spring容器初始化完成的过程中
  4. 我的理解:没有Spring容器之前就利用BootstrapRegistry来共享一些对象,有了Spring容器之后就利用Spring容器来共享一些对象

2.2.2 获取ApplicationContextInitializer对象

初始化器ApplicationContextInitializer​是个好东西,顾名思义,ApplicationContextInitializer是用来初始化Spring容器ApplicationContext对象的,用于IoC​容器刷新之前初始化一些组件,比如可以利用ApplicationContextInitializer来向Spring容器中添加ApplicationListener、初始化ServletContextApplicationContextInitializer对象。

那么如何获取初始化器呢?跟着上图中的代码进入,在SpringApplication中的如下图中的方法:

相对重要的就是第一步获取初始化器的名称了,这个肯定是全类名​了,详细源码肯定在loadFactoryNames()​方法中了,跟着源码进入,最终调用的是#SpringFactoriesLoader.loadSpringFactories()方法

loadSpringFactories()​方法就不再详细解释了,其实就是从类路径META-INF/spring.factories​中加载ApplicationContextInitializer的值。从"META-INF/spring.factories"中读取key为ApplicationContextInitializer类型的扩展点,并实例化出对应扩展点对象。

在spring-boot-autoconfigure​的spring.factories文件中的值如下图:

上图中的只是一部分初始化器,因为spring.factories文件不止一个,几乎每一个集成到Spring Boot的依赖都会提供spring.factories来帮助完成自动配置。

下图中是我的demo中注入的初始化器,现实项目中并不止这些:

这也告诉我们自定义一个ApplicationContextInitializer​只需要实现接口,然后在spring.factories文件中设置即可。

2.3 设置监听器(Listener

监听器(ApplicationListener​)这个概念在Spring​中就已经存在,主要用于监听特定的事件(ApplicationEvent),比如IoC容器刷新、容器关闭等。

Spring Boot​扩展了ApplicationEvent​构建了SpringApplicationEvent​这个抽象类,主要用于Spring Boot启动过程中触发的事件,比如程序启动中、程序启动完成等。如下图:

  • 启动事件,ApplicationStartingEvent。一旦一个 SpringApplication 开始,事件就会尽早发布 - 在 or ApplicationContext 可用之前Environment,但在注册之后ApplicationListener。事件的来源是本身SpringApplication,但要注意不要在这个早期阶段过多地使用其内部状态,因为它可能会在生命周期的后期被修改
  • 失败事件,ApplicationFailedEvent
  • 准备事件,ApplicationPreparedEvent 事件发布为 当一个SpringApplication正在启动并且ApplicationContext已完全准备好但未refresh。将加载 Bean 定义,并在此阶段可以使用
  • ApplicationEnvironmentPreparedEvent
  • ContextClosedEvent

应用程序事件监听器跟监听事件是绑定的,如:

  • ConfigServerBootstrapApplicationListener只跟ApplicationEnvironmentPreparedEvent事件绑定
  • LiquibaseServiceLocatorApplicationListener只跟ApplicationStartedEvent事件绑定
  • LoggingApplicationListener跟所有事件绑定

监听器如何获取?从源码中知道其实和初始化器(ApplicationContextInitializer​)执行的是同一个方法,最终调用的都是#SpringFactoriesLoader.loadSpringFactories()方法,同样是从META-INF/spring.factories文件中获取监听器类。从"META-INF/spring.factories"中读取key为ApplicationListener类型的扩展点,并实例化出对应扩展点对象。

在spring-boot-autoconfigure​的spring.factories文件中的值如下图:

spring.factories文件不止一个,同样监听器也不止以上这些,Spring Boot引入的很多依赖都会提供监听器。

作者demo中注入的一些监听器如下图:

2.4 总结

SpringApplication​的构建都是为了run()方法启动做铺垫,构造方法中总共就有几行代码,最重要的部分就是设置应用类型、设置初始化器、设置监听器。

「注意」​:初始化器和这里的监听器都要放置在spring.factories​文件中才能在这一步骤加载,否则不会生效,因为此时IoC容器​还未创建,即使将其注入到IoC容器中也是不会生效的。所以如果想让初始化器和监听器在完成IoC容器创建前就起作用,必须要用spring.factories文件才行

作者简单的画了张执行流程图,仅供参考,如下:

new SpringApplication()基本上就是做以上这些必要的属性初始化和赋值,接下来我们看下关键方法run()。

三、SpringApplication的执行:执行run(String... args)方法

上面分析了SpringApplication的构建过程,一切都做好了铺垫,现在到了启动的过程了。

这里根据源码将启动过程分为了​「9步」​,下面将会一一介绍。

/*** 运行spring应用程序,创建并刷新一个新的 {@link ApplicationContext}.** @param args the application arguments (usually passed from a Java main method)* @return a running {@link ApplicationContext}*/
public ConfigurableApplicationContext run(String... args) {// 计时工具StopWatch stopWatch = new StopWatch();// 开始计时stopWatch.start();// 创建启动上下文对象DefaultBootstrapContext bootstrapContext = createBootstrapContext();ConfigurableApplicationContext context = null;configureHeadlessProperty();// 第一步:获取并启动监听器,这是从spring.factories中获取监听器// 获取监听器SpringApplicationRunListeners listeners = getRunListeners(args);// 启动监听器listeners.starting(bootstrapContext, this.mainApplicationClass);try {// 获取参数对象,将启动时传入的arg参数封装成ApplicationArguments对象ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 第二步:准备环境ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);configureIgnoreBeanInfo(environment);// 第三步:打印banner,就是启动的时候在console的spring boot图案Banner printedBanner = printBanner(environment);// 第四步:创建spring容器context = createApplicationContext();context.setApplicationStartup(this.applicationStartup);// 第五步:spring容器前置处理prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);// 第六步:刷新容器refreshContext(context);// 第七步:spring容器后置处理afterRefresh(context, applicationArguments);stopWatch.stop(); // 结束计时器并打印,这就是我们启动后console的显示的时间if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 第八步:发出启动结束事件listeners.started(context);// 第九步:执行runner的run方法callRunners(context, applicationArguments);} catch (Throwable ex) {// 异常处理,如果run过程发生异常handleRunFailure(context, ex, listeners);throw new IllegalStateException(ex);}try {listeners.running(context);} catch (Throwable ex) {// 异常处理,如果run过程发生异常handleRunFailure(context, ex, null);throw new IllegalStateException(ex);}// 返回最终构建的容器对象return context;
}

run()方法流程图:

基本流程如下:

  1. 启动一个计时器,启动完成后会打印耗时

  1. 获取并启动监听器 SpringApplicationRunListeners
  2. 配置环境 ConfigurableEnvironment
  3. Banner配置,就是控制台的那个spirng

  1. 应用上下文模块(前置处理、刷新、后置处理) ConfigurableApplicationContext
  2. 发出启动结束事件并结束计时

这里的每一个方法都是做了很多事情,接下来我们一步步深入看下。

3.1 run方法第一步:获取并启动监听器

SpringApplicationRunListener​这个监听器和ApplicationListener不同,它是用来监听应用程序启动过程的,接口的各个方法含义如下:

public interface SpringApplicationRunListener {/*** 当调用run方法后会立即调用,可以用于非常早期的初始化*/default void starting(ConfigurableBootstrapContext bootstrapContext) {starting();}/*** 环境准备好之后调用*/default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,ConfigurableEnvironment environment) {environmentPrepared(environment);}/*** 在加载资源之前,ApplicationContex准备好之后调用*/default void contextPrepared(ConfigurableApplicationContext context) {}/*** 在加载应用程序上下文ApplicationContext但在其刷新之前调用*/default void contextLoaded(ConfigurableApplicationContext context) {}/*** ApplicationContext上下文已经刷新且应用程序已启动且所有{@link CommandLineRunner commandLineRunner}* 和{@link ApplicationRunner ApplicationRunners}未调用之前调用*/default void started(ConfigurableApplicationContext context) {}/*** 当应用程序上下文ApplicationContext被刷新并且所有{@link CommandLineRunner commandLineRunner}* 和{@link ApplicationRunner ApplicationRunners}都已被调用时,在run方法结束之前立即调用。*/default void running(ConfigurableApplicationContext context) {}/*** 在启动过程发生失败时调用*/default void failed(ConfigurableApplicationContext context, Throwable exception) {}
}

这里的启动监听就是我们需要监听Spring Boot的启动流程,实现SpringApplicationRunListener类即可监听。

3.1.1 如何获取运行监听器?

在SpringApplication#run()方法中,源码如下:

//从spring.factories中获取监听器
SpringApplicationRunListeners listeners = getRunListeners(args);/*** 获取运行监听的监听者们,在对应的阶段会发送对应的事件到监听者* @param args* @return*/
private SpringApplicationRunListeners getRunListeners(String[] args) {Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};return new SpringApplicationRunListeners(logger,getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),this.applicationStartup);
}

跟进getRunListeners()​方法,其实还是调用了loadFactoryNames()方法从spring.factories文件中获取值,如下:

org.springframework.boot.SpringApplicationRunListener=\

org.springframework.boot.context.event.EventPublishingRunListener

最终注入的是EventPublishingRunListener这个实现类,创建实例过程肯定是通过反射了,因此我们看看它的构造方法,如下图:

这个运行监听器内部有一个事件广播器(SimpleApplicationEventMulticaster​),主要用来广播特定的事件(SpringApplicationEvent​)来触发特定的监听器ApplicationListener。

EventPublishingRunListener​中的每个方法用来触发SpringApplicationEvent中的不同子类。

3.1.2 如何启动运行监听器?

在SpringApplication#run()方法中,源码如下:

// 执行starting()方法
listeners.starting(bootstrapContext, this.mainApplicationClass);

执行SpringApplicationRunListeners​的starting()​方法,跟进去其实很简单,遍历执行上面获取的运行监听器,这里只有一个EventPublishingRunListener​。因此执行的是它的starting()方法,源码如下图:

上述源码中逻辑很简单,其实只是执行了multicastEvent()​方法,广播了ApplicationStartingEvent​事件。至于multicastEvent()​内部方法感兴趣的可以看看,其实就是遍历ApplicationListener​的实现类,找到监听ApplicationStartingEvent​这个事件的监听器,执行onApplicationEvent()方法。

3.1.3 总结

这一步其实就是广播了ApplicationStartingEvent​事件来触发监听这个事件的ApplicationListener监听器。

默认情况下SpringBoot提供了一个EventPublishingRunListener,它实现了SpringApplicationRunListener接口,默认情况下会利用EventPublishingRunListener发布一个ApplicationContextInitializedEvent事件,程序员可以通过定义ApplicationListener来消费这个事件。

因此如果自定义了ApplicationListener​并且监听了ApplicationStartingEvent(应用程序开始启动)事件,则这个监听器将会被触发,通过这个事件就可以知道应用程序什么时候开始启动了。

  • ApplicationListener是用来监听spring发布的事件
  • SpringApplicationRunListener在spring boot启动的过程中,会监听回调一些方法,它里面的每个方法都是表示做完了某个步骤或者该做某个步骤时要执行的方法。所以如果我们想在spring boot启动过程中间想要做一些事情,我们就可以通过这个监听器。可以自定义,spring boot也默认提供了一个实现类:

这个默认实现的SpringApplicationRunListener实现的各个阶段的方法就是发布的一个事件,发布告知某某步骤开始执行了,某某步骤执行完成了,这样我们同样可以利用ApplicationListener来监听这些事件,来实现在spring boot启动过程中间想要做一些事情的效果,也就类似于用自定义的SpringApplicationRunListener来完成了。

所以如果我们想要在spring boot启动过程中间想要做一些事情,就用上面两种方法。

3.2 run方法第二步:环境搭建,解析配置文件

这一步主要用于加载系统配置以及用户的自定义配置(application.properties,创建Environment对象。源码如下,在run()方法中:

ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

 Environment对象表示环境变量,该对象内部主要包含了:

  1. 当前操作系统的环境变量
  2. JVM的一些配置信息
  3. -D方式所配置的JVM环境变量

下面我们再去看环境搭建源码,prepareEnvironment​方法内部广播了ApplicationEnvironmentPreparedEvent事件,源码如下:

/*** 创建并配置Spring Boot应用将要使用的Environment** @param listeners* @param bootstrapContext* @param applicationArguments* @return*/
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {// 根据不同的web类型创建不同实现的Environment对象,比如webApplication为servlet,则就会返回StandardServletEnvironment实例ConfigurableEnvironment environment = getOrCreateEnvironment();// 配置环境,加载系统属性配置,这一步会涉及到添加存储配置键值对的对象configureEnvironment(environment, applicationArguments.getSourceArgs());ConfigurationPropertySources.attach(environment);// 广播环境已准备完成事件,触发监听器,就是通过触发的监听器加载了用户自定义配置(application.properties等配置文件)/*** 触发SpringApplicationRunListener的environmentPrepared()* 默认情况下会利用EventPublishingRunListener发布一个ApplicationEnvironmentPreparedEvent事件,* 程序员可以通过定义ApplicationListener来消费这个事件,* 比如默认情况下会有一个EnvironmentPostProcessorApplicationListener来消费这个事件,* 而这个ApplicationListener接收到这个事件之后,就会解析application.properties、application.yml文件,并将解析出来的键值对添加到Environment对象中去。* */listeners.environmentPrepared(bootstrapContext, environment);DefaultPropertiesPropertySource.moveToEnd(environment);// 根据命令行参数中spring.profiles.active属性配置Environment对象中的activeProfile(比如dev、prod、test)configureAdditionalProfiles(environment);// 为当前应用绑定环境,将环境中spring.main属性绑定到SpringApplication对象中bindToSpringApplication(environment);// 如果用户使用spring.main.web-application-type属性手动设置了webApplicationTypeif (!this.isCustomEnvironment) {// 将环境对象转换成用户设置的webApplicationType相关类型,它们是继承同一个父类,直接强转environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());}ConfigurationPropertySources.attach(environment);return environment;
}

这里主要有如下过程:

  1. 创建配置环境 ConfigurableEnvironment
  2. 加载属性文件资源
  3. 配置监听

环境构建这一步加载了系统环境配置、用户自定义配置并且广播了ApplicationEnvironmentPreparedEvent事件,触发监听器。

3.2.1 配置文件解析流程图

就是ConfigDataEnvironmentPostProcessor负责解析的properties和yaml配置文件。

3.2.2 Spring Boot的配置优先级

在加载配置数据的过程中,涉及到了对配置信息按照优先级进行存储。

这四个对象是有优先级的。这四个其实就相当于Map,加载配置文件的键值对就是存入的这四个map中,查询key对应的value时会根据这从上到下的优先级一个个查询,只要从一个map中查到了,就不会再去查后面的map了。每一个Map存储不同类型的配置数据,有的是存储JVM的(倒数第二个),有的是存储操作系统的(倒数第一个)。前两个暂时先不管了。

由此可见,配置是存在优先级的,优先级高的配置方式会覆盖掉优先级低的配置方式。

2.6以及以后的版本,properties配置文件的优先级比yaml文件要高,但是之前的版本yaml文件优先级比properties配置文件高。

不同位置的配置对应的优先级如下,从上到下的顺序:

file指的是应用项目的根目录,不是classpath这个装class文件的目录了,也就是IDEA平时我们写代码的根目录。

下面的file表示的是项目根目录。

配置文件这里写在后面的优先级更高。

官方文档中有配置优先级的更详细说明:

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

优先级由低到高:

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  3. Config data (such as application.properties files).
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env. 不管它
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

3.3 run方法第三步:打印banner

/*** 打印banner** @param environment* @return*/
private Banner printBanner(ConfigurableEnvironment environment) {// banner模式,可以是console、log、offif (this.bannerMode == Banner.Mode.OFF) {return null;}ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader: new DefaultResourceLoader(null);SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);if (this.bannerMode == Mode.LOG) {return bannerPrinter.print(environment, this.mainApplicationClass, logger);}return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}

基本就是依据不同情况打印banner而已,比较简单。

3.4 run方法第四步:创建Spring IoC容器

创建IoC容器,获取到ConfigurableApplicationContext上下文对象(IoC容器)。

源码在run()方法中,如下:

context = createApplicationContext();

跟进代码,真正执行的是ApplicationContextFactory方法,会利用ApplicationContextFactory.DEFAULT,根据应用类型创建对应的Spring容器。源码如下:

ApplicationContextFactory DEFAULT = (webApplicationType) -> {try {switch (webApplicationType) {case SERVLET:return new AnnotationConfigServletWebServerApplicationContext();case REACTIVE:return new AnnotationConfigReactiveWebServerApplicationContext();default:return new AnnotationConfigApplicationContext();}}catch (Exception ex) {throw new IllegalStateException("Unable create a default ApplicationContext instance, "+ "you may need a custom ApplicationContextFactory", ex);}
};
  1. 应用类型为SERVLET,则对应AnnotationConfigServletWebServerApplicationContext
  2. 应用类型为REACTIVE,则对应AnnotationConfigReactiveWebServerApplicationContext
  3. 应用类型为普通类型,则对应AnnotationConfigApplicationContext

根据webApplicationType​决定创建的类型,很显然,我这里的是servlet​,因此创建的是AnnotationConfigServletWebServerApplicationContext。

这一步仅仅是创建了IOC容器,未有其他操作。

3.5 run方法第五步:Spring IoC容器前置处理

这一步真是精华了,在刷新容器之前做准备,其中有一个非常关键的操作:将启动类注入容器,为后续的自动配置奠定基础。源码如下:

prepareContext(context, environment, listeners, applicationArguments,printedBanner);

prepareContext()源码解析如下,内容还是挺多的:

/*** Spring容器准备*/
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments, Banner printedBanner) {// 设置上下文(容器)环境,包括自定义配置和系统环境配置context.setEnvironment(environment);// 执行后置处理,来处理上下文(容器)postProcessApplicationContext(context);// 执行所有初始化器ApplicationContextInitializer对象的initialize方法(这些对象是通过读取spring.factories加载)applyInitializers(context);// 广播容器准备完成事件,触发监听器listeners.contextPrepared(context);bootstrapContext.close(context);if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);}// 注册springApplicationArguments和springBootBanner到bean工厂中ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 注册启动参数Bean,这里将容器的参数封装成了该Bean,注册到容器中beanFactory.registerSingleton("springApplicationArguments", applicationArguments);if (printedBanner != null) {// 设置Banner,将Banner注册到bean工厂中beanFactory.registerSingleton("springBootBanner", printedBanner);}if (beanFactory instanceof DefaultListableBeanFactory) {((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);}// 判断是否开启了懒加载if (this.lazyInitialization) {context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());}// Load the sources// 获取启动类指定的参数,可以是多个Set<Object> sources = getAllSources();Assert.notEmpty(sources, "Sources must not be empty");// 加载启动类,将启动类注册到容器中load(context, sources.toArray(new Object[0]));// 广播容器已加载完成事件,触发监听器listeners.contextLoaded(context);
}

从源码中可以看出步骤很多,下面将会详细介绍几个重点的内容。

3.5.1 调用初始化器

调用执行在SpringApplication​构建过程中设置的初始化器(就是从spring.factories取值的那些)。执行的流程很简单,遍历执行,源码如下图:

将自定义的ApplicationContextInitializer​放在META-INF/spring.factories中,在此时也是会被调用。

3.5.1.1 利用ApplicationContextInitializer初始化Spring容器对象

默认情况下SpringBoot提供了多个ApplicationContextInitializer,其中比较重要的有ConditionEvaluationReportLoggingListener,别看到它的名字叫XXXListener,但是它确实是实现了ApplicationContextInitializer接口的。在它的initialize()方法中会:

  1. 将Spring容器赋值给它的applicationContext属性
  2. 并且往Spring容器中添加一个ConditionEvaluationReportListener(ConditionEvaluationReportLoggingListener的内部类),它是一个ApplicationListener
  3. 并生成一个ConditionEvaluationReport对象赋值给它的report属性

ConditionEvaluationReportListener会负责接收ContextRefreshedEvent事件,也就是Spring容器一旦启动完毕就会触发ContextRefreshedEvent,ConditionEvaluationReportListener就会打印自动配置类的条件评估报告。

3.5.2 触发SpringApplicationRunListenercontextPrepared()

默认情况下会利用EventPublishingRunListener发布一个ApplicationContextInitializedEvent事件,默认情况下暂时没有ApplicationListener消费这个事件。

3.5.3 调用DefaultBootstrapContext对象的close()

没什么特殊的,忽略。

3.5.4 判断是否开启懒加载

下面这个设置可以让spring boot中所有的bean都变成懒加载。不设置这个参数的话,默认都是非懒加载的。

以上配置在源码中的体现。

LazyInitializationBeanFactoryPostProcessor其实就是把容器中所有的bean定义取出来,然后把它们的懒加载属性全部设置为true,这样这些bean就都是懒加载了。

3.5.5 加载启动类,注入容器

这一步就是将启动类作为配置类注册到Spring容器中(load()方法)。

将SpringApplication.run(MyApplication.class);中传入进来的类,比如MyApplication.class,作为Spring容器的配置类加载到IoC容器中,作为后续自动配置的入口

在SpringApplication​构建过程中将主启动类放置在了primarySources​这个集合中,此时的getAllSources()即是从其中取值,如下图:

这里取出的就是主启动类,当然你的项目中可能不止一个,接下来就是将其加载到IoC容器中了,源码如下:

load(context, sources.toArray(new Object[0]));

跟着代码进去,其实主要逻辑都在BeanDefinitionLoader.load()方法,如下图:

将主启动类加载到beanDefinitionMap,后续该启动类将作为开启自动配置化的入口,后续章节详细介绍。

3.5.6 触发SpringApplicationRunListenercontextLoaded()

默认情况下在容器加载完成后,会利用EventPublishingRunListener发布一个ApplicationPreparedEvent事件,通知容器已加载完成。

3.6 run方法第六步:刷新容器【关键】

刷新容器完全是Spring​的功能了,比如初始化资源,初始化上下文广播器等,这个在之前Spring章节已经讲过了,就不再详细介绍,有兴趣可以看看前面的Spring源码笔记。

/*** 刷新应用程序上下文** @param context*/
private void refreshContext(ConfigurableApplicationContext context) {// 注册一个关闭钩子,在jvm停止时会触发,然后退出时执行一定的退出逻辑if (this.registerShutdownHook) {try {// 添加:Runtime.getRuntime().addShutdownHook()// 移除:Runtime.getRuntime().removeShutdownHook(this.shutdownHook)context.registerShutdownHook();} catch (AccessControlException ex) {// Not allowed in some environments.}}// ApplicationContext真正开始初始化容器和创建bean的阶段refresh((ApplicationContext) context);
}protected void refresh(ApplicationContext applicationContext) {Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);refresh((ConfigurableApplicationContext) applicationContext);
}protected void refresh(ConfigurableApplicationContext applicationContext) {//调用创建的容器applicationContext中的refresh()方法applicationContext.refresh();
}

调用应用上下文对象的refresh()方法,接下来我们到ConfigurableApplicationContext类中去看下这个方法:

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {void refresh() throws BeansException, IllegalStateException;
}

这是一个接口,且这个类是在Spring框架中,并非Spring Boot中的类,它的实现类共有三个:

AbstractApplicationContext是一个抽象类,其余两个类都继承了它,我们来看看这个抽象类refresh()的代码:

@Override
public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");// 第一步:刷新上下文环境prepareRefresh();// 第二步:获取上下文内部BeanFactory。初始化BeanFactory,解析XML,相当于之前的XmlBeanFactory的操作。ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 第三步:对BeanFactory做预备工作/*** 为上下文准备BeanFactory,即对BeanFactory的各种功能进行填充,如常用的注解@Autowired @Qualifier等* 添加ApplicationContextAwareProcessor处理器* 在依赖注入忽略实现*Aware的接口,如EnvironmentAware、ApplicationEventPublisherAware等* 注册依赖,如一个bean的属性中含有ApplicationEventPublisher(beanFactory),则会将beanFactory的实例注入进去*/prepareBeanFactory(beanFactory);try {// 第四步:允许在上下文子类中对bean工厂进行post-processing,即即子类处理自定义的BeanFactoryPostProcesspostProcessBeanFactory(beanFactory);StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");// 第五步:调用上下文中注册为bean的工厂 BeanFactoryPostProcessor/*** 激活各种BeanFactory处理器,包括BeanDefinitionRegistryBeanFactoryPostProcessor和普通的BeanFactoryPostProcessor* 执行对应的postProcessBeanDefinitionRegistry方法 和  postProcessBeanFactory方法*/invokeBeanFactoryPostProcessors(beanFactory);// 第六步:注册拦截bean创建的拦截器/*** 注册拦截Bean创建的Bean处理器,即注册BeanPostProcessor,不是BeanFactoryPostProcessor,注意两者的区别* 注意,这里仅仅是注册,并不会执行对应的方法,将在bean的实例化时执行对应的方法*/registerBeanPostProcessors(beanFactory);beanPostProcess.end();// 第七步:初始化上下文中的资源文件,如初始化MessageSource(国际化相关)等initMessageSource();// 第八步:初始化容器事件广播器(用来发布事件),并放入applicatioEventMulticaster,如ApplicationEventPublisherinitApplicationEventMulticaster();// 第九步:初始化一些特殊的bean,比如在这一步就会启动Tomcat(如果设置Tomcat作为Web服务器的话)onRefresh();// 第十步:将所有监听器(在所有bean中查找listener bean)注册到前两步创建的事件广播器中registerListeners();// 第十一步:结束bean的初始化工作(主要将所有单例BeanDefinition实例化)/*** 设置转换器* 注册一个默认的属性值解析器* 冻结所有的bean定义,说明注册的bean定义将不能被修改或进一步的处理* 初始化剩余的非惰性的bean,即初始化非延迟加载的bean*/finishBeanFactoryInitialization(beanFactory);// 第十二步:afterRefresh。上下文(容器)刷新完毕,发布相应事件/*** 通过spring的事件发布机制发布ContextRefreshedEvent事件,以保证对应的监听器做进一步的处理* 即对那种在spring启动后需要处理的一些类,这些类实现了ApplicationListener<ContextRefreshedEvent>,* 这里就是要触发这些类的执行(执行onApplicationEvent方法)* 另外,spring的内置Event有ContextClosedEvent、ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、RequestHandleEvent* 完成初始化,通知生命周期处理器lifeCycleProcessor刷新过程,同时发出ContextRefreshEvent通知其他人*/finishRefresh();} catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("Exception encountered during context initialization - " +"cancelling refresh attempt: " + ex);}// Destroy already created singletons to avoid dangling resources.destroyBeans();// Reset 'active' flag.cancelRefresh(ex);// Propagate exception to caller.throw ex;} finally {// Reset common introspection caches in Spring's core, since we// might not ever need metadata for singleton beans anymore...resetCommonCaches();contextRefresh.end();}}
}

这个源码是不是看着很熟悉,这个就是我们在学习Spring初始化容器源码时的代码。详见:Spring容器的初始化过程(IoC容器的启动过程)

这里有非常多的步骤,上下文对象主要的bean也是在这里进行处理的,具体的说明可以看注释。

就是在refresh()的onRefresh()方法中,启动了Tomcat:

启动Tomcat(启动WebServer)

因为我们这里是web应用,所以实现类是ServletWebServerApplicationContext,我们看下这个类refresh()的代码:

@Override
public final void refresh() throws BeansException, IllegalStateException {try {super.refresh();}catch (RuntimeException ex) {WebServer webServer = this.webServer;if (webServer != null) {webServer.stop();}throw ex;}
}

主要还是调用父类方法,没有什么特殊的。

用Spring容器的refresh()方法,结合 3.4 run方法第四步:创建Spring IoC容器3.5.4 加载启动类,注入容器 这两个步骤,相当于执行了这样一个流程:

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(MyApplication.class)
applicationContext.refresh()

3.7 run方法第七步:spring容器后置处理 afterRefresh()

一个扩展方法,源码如下:

afterRefresh(context, applicationArguments);

该方法默认为空,如果有自定义需求可以重写,比如打印一些启动结束日志等。

protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}

3.8 run方法第八步:发出结束执行的事件

触发SpringApplicationRunListener的started(),发布ApplicationStartedEvent事件和AvailabilityChangeEvent事件,AvailabilityChangeEvent事件表示状态变更状态,变更后的状态为LivenessState.CORRECT。

LivenessState枚举有两个值:

  1. CORRECT:表示当前应用正常运行中
  2. BROKEN:表示当前应用还在运行,但是内部出现问题,暂时还没发现哪里用到了

这一步同样是EventPublishingRunListener​这个监听器,广播ApplicationStartedEvent事件。

但是这里广播事件和前几次不同,并不是广播给SpringApplication​中的监听器(在构建过程中从spring.factories​文件获取的监听器),现在IoC容器已经创建好了,可以广播给IoC容器中的监听器了。因此在IoC容器​中注入的监听器(使用@Component​等方式注入的)当前也能够生效。前面几个事件只有在spring.factories文件中设置的监听器才会生效。

跟着代码进入,可以看到started()方法源码如下:

这里并没有用事件广播器SimpleApplicationEventMulticaster广播事件,而是使用ConfigurableApplicationContext​直接在IoC容器中发布事件。

3.9 run方法第九步:执行Runners

Spring Boot​ 提供了两种Runner​让我们定制一些额外的操作,分别是CommandLineRunner​和ApplicationRunner,关于这两个的区别,后面文章详细介绍。

调用的源码如下:

callRunners(context, applicationArguments);

 跟进代码,其实真正调执行的是如下方法:

  1. 获取Spring容器中的ApplicationRunner类型的Bean
  2. 获取Spring容器中的CommandLineRunner类型的Bean
  3. 执行它们的run()

逻辑很简单,从IoC容器中获取,遍历调用它们的run()方法。

3.10 总结

Spring Boot 启动流程相对简单些,打印Banner那步并不是很重要,所以重要的就是以下八个步骤,希望能够帮助读者理解,流程图如下:

run方法启动后

主要做如下几件事情:

  • 发出启动结束事件
  • 执行实现ApplicationRunner或CommandLineRunner的run方法
  • 发布应用程序已启动(ApplicationStartedEvent)事件

run方法异常处理

如果run方法的处理过程中发生异常,则对exitCode进行相应处理

private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, SpringApplicationRunListeners listeners) {try {try {handleExitCode(context, exception);if (listeners != null) {listeners.failed(context, exception);}} finally {reportFailure(getExceptionReporters(context), exception);if (context != null) {context.close();}}} catch (Exception ex) {logger.warn("Unable to close ApplicationContext", ex);}ReflectionUtils.rethrowRuntimeException(exception);
}

至此,所有Spring Boot的启动流程已经完成,你的项目也顺利的跑起来了。我们需要重点理解run()方法执行的九个步骤以及事件、初始化器、监听器等组件的执行时间点。

四、Spring Boot启动过程流程图

更全面的一个流程图:


 相关文章:【Spring】Java SPI机制及Spring Boot使用实例_springboot spi-CSDN博客
                  【Spring】面试官,请别再问Spring Bean的生命周期了!_give any instantiationawarebeanpostprocessors the -CSDN博客

http://www.dtcms.com/a/301888.html

相关文章:

  • 鸿蒙打包签名
  • HarmonyOS 6 云开发-用户头像上传云存储
  • 前端工程化常见问题总结
  • Windows|CUDA和cuDNN下载和安装,默认安装在C盘和不安装在C盘的两种方法
  • AI技术革命:产业重塑与未来工作范式转型。
  • 深入解析MIPI C-PHY (四)C-PHY物理层对应的上层协议的深度解析
  • 齐护Ebook科技与艺术Steam教育套件 可图形化micropython Arduino编程ESP32纸电路手工
  • 湖南(源点咨询)市场调研 如何在行业研究中快速有效介入 起头篇
  • Triton编译
  • 【n8n教程笔记——工作流Workflow】文本课程(第一阶段)——5.5 计算预订订单数量和总金额 (Calculating booked orders)
  • Rouge:面向摘要自动评估的召回导向型指标——原理、演进与应用全景
  • 分表分库与分区表
  • Android启动时间优化大全
  • 蛋白质反向折叠模型-ProteinMPNN安装教程
  • 学习日志20 python
  • 【unitrix】 6.18 二进制小数特质(t_decimal.rs)
  • EPOLLET 边缘触发模式深度解析
  • 抗辐照芯片在低轨卫星星座CAN总线通讯及供电系统的应用探讨
  • vue3的一些浅显用法
  • Day06–哈希表–242. 有效的字母异位词,349. 两个数组的交集,202. 快乐数,1. 两数之和
  • 浙大公开课—基于深度学习的特征匹配与姿态估计
  • (补题)拼图游戏
  • EPOLLIN事件的详细解析
  • 【时时三省】(C语言基础)指针数组和多重指针
  • MySQL 8.4 Windows 版安装记录与步骤参考
  • 【C语言网络编程基础】DNS 协议与请求详解
  • Context Engineering Notes
  • 持续优化Cypress自动化测试
  • FunctionCall 如何使用以及如何训练
  • 从MySQL的information_schema系统数据库中获取表的元数据信息