SpringBoot 系列:(一)SpringBoot 启动过程

本贴最后更新于 1992 天前,其中的信息可能已经时移俗易

在我当前的工作中,基本上所有的 Java 工程都是基于 SpringBoot 构建的。而在日常开发过程中,我们更多的关注于如何使用 SpringBoot,并没有太多的去了解 SpringBoot 的底层实现细节,或者对 SpringBoot 底层实现细节的了解处于一种零碎、分散的状态。因此,我想通过撰写系列文章来加深自己对于 SpringBoot 的理解,与大家一起加油进步。当然,由于自己也还是程序员届的一个小学生,如果文中有哪些错误之处,也请大家留言指出。

简介

这篇文章旨在通过分析 SpringBoot 的源码,了解 SpringBoot 的启动过程。读者在阅读该文时,最好能同时翻阅 SpringBoot 源码。

当然,在正式开始 SpringBoot 启动过程源码走读前,我们在脑海里需要有一个认知,即:SpringBoot 赋予我们的,是在基于约定的前提下,快速构建一个 Spring 容器的能力。

打个比方,在 SpringBoot 之前,如果要在 Spring 工程中通过 Druid 中间件连接数据库的话,需要通过 XML 文件的形式生成 DruidDataSource;在 XML 文件中,需要人工介入 DruidDataSource 的生成过程,比如指定数据库连接的 URL、用户名、密码等。某些情况下,这些配置显得十分繁琐,整个 XML 文件也变得十分复杂。那么,是否能以一种简单的方式来生成 DruidDataSource 呢?有,即 Druid 中间件的维护方提供一个自动配置类,Spring 在启动过程中通过某种方式感知到该自动配置类,并调用该自动配置类的某些特定方法。这些方法会读取工程的配置文件中某些配置值,利用这些配置值来初始化一个 DruidDataSource,并注册到 Spring 容器。这样,对于一个使用 Druid 中间件的开发者来说,他需要做的仅仅是通过 maven 或者 gradle 引入包含自动配置类的依赖包,并在配置文件中指定配置值而已,从而省去了编写 XML 文件的工作。

这就是 SpringBoot 帮我们做的事情,即在约定的基础上,帮助我们更快的构建 Spring 容器,简化开发过程。

SpringBoot 工程启动

SpringBoot 工程的启动非常简单,只需要运行 main 方法即可,main 方法中核心在于执行 SpringApplication 的 run 静态方法。代码如下:

WX20191008162847.png

生成 SpringApplication 对象

确定 webApplicationType

这一步的目的是确定当前的工程是否是 web 工程;如果是 web 工程的话,到底是普通的基于 servlet 的 web 工程还是 reactive web 工程。

其判断逻辑也比较简单,逻辑如下:
(1)如果类路径中存在 DispatcherHandler,但是不存在 DispatcherServlet,也不存在 ServletContainer,则为一个 reactive web 工程

(2)如果当前类路径下不存在 javax.servlet.Servlet 或者不存在 org.springframework.web.context.ConfigurableWebApplicationContext,则当前工程不是 web 工程

(3)否则,为一个普通的基于 servlet 的 web 工程
WX20191008163706.png

从 spring.factories 文件中读取并实例化 ApplicationContextInitializer 对象

这里代码很简单,核心在于调用 SpringFactoriesLoader 类的静态方法 loadFactoryNames,获取到 spring.factories 文件中的 ApplicationContextInitializer 配置,并初始化 ApplicationContextInitializer 对象。

ApplicationContextInitializer 是一个接口,内部只有一个 initialize 的方法声明。在 SpringBoot 启动的后续过程中(创建并配置 ConfigurableApplicationContext 对象时),会调用该方法,用来对进一步配置 ConfigurableApplicationContext。

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> { void initialize(C applicationContext); }

SpringFactoriesLoader 的 loadFactoryNames 方法会读取 META-INF 目录下 spring.factories。META-INF/spring.factories 可以有多个,分布于工程自身的资源目录下(运行时能从类路径根目录下获取到),或者在引入的依赖包根目录下。SpringBoot 包依赖中的一个 spring.factories 内容如以下截图所示。以该截图为例,本节标题“从 spring.factories 文件中读取并实例化 ApplicationContextInitializer 对象”,即指从 spring.factories 读取并实例化 ConfigurationWarningsApplicationContextInitializer/ContextIdApplicationContextInitializer/DelegatingApplicationContextInitializer/ServerPortInfoApplicationContextInitializer

image.png

从 spring.factories 文件中读取并实例化 ApplicationListener 对象

逻辑与“从 spring.factories 文件中读取并实例化 ApplicationContextInitializer 对象”类似,通过 SpringFactoriesLoader 读取 META-INF/spring.factories,并生成 ClearCachesApplicationListener/ParentContextCloserApplicationListener/FileEncodingApplicationListener/ConfigFileApplicationListener 等对象。这些对象会在 SpringBoot 启动的后续步骤中使用到(“从 spring.factories 文件中读取并实例化 SpringApplicationRunListener 对象” 步骤,会生成 EventPublishingRunListener 对象,该对象内部会持有这些 ApplicationListener 的引用)

ApplicationListener 接口定义如下。

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { void onApplicationEvent(E event); }

在众多 ApplicationListener 中,需要特别注意 ConfigFileApplicationListener。ConfigFileApplicationListener 会被用来加载配置文件 application.properties、application.yml。

确定 main 方法所在的类

逻辑比较有意思,是通过异常的形式,从异常栈里面解析出 main 方法所属类的。
image.png

调用 SpringApplication 对象的 run 非静态方法

从 spring.factories 文件中读取并实例化 SpringApplicationRunListener 对象

过程与“从 spring.factories 文件中读取并实例化 ApplicationContextInitializer 对象”和“从 spring.factories 文件中读取并实例化 ApplicationListener 对象”类似,通过 SpringFactoriesLoader 读取 META-INF/spring.factories,并生成 SpringApplicationRunListener 接口实现类的实例。

SpringApplicationRunListener 接口定义如下,主要用于在 SpringApplication 启动过程各个阶段中,触发相应的事件。

public interface SpringApplicationRunListener { void starting(); void environmentPrepared(ConfigurableEnvironment environment); void contextPrepared(ConfigurableApplicationContext context); void contextLoaded(ConfigurableApplicationContext context); void started(ConfigurableApplicationContext context); void running(ConfigurableApplicationContext context); void failed(ConfigurableApplicationContext context, Throwable exception); }

从前面的 spring.factories 截图可知,这一步主要是生成 EventPublishingRunListener 对象。生成 EventPublishingRunListener 对象时,会将已读取 ApplicationListener 对象传入 EventPublishingRunListener 的构造函数,构造函数内部再将 ApplicationListener 的引用传入一个 SimpleApplicationEventMulticaster 对象。总而言之,EventPublishingRunListener 对象会间接的持有 ApplicationListener 对象的引用。

调用 SpringApplicationRunListener 对象的 starting 方法

这里,调用 SpringApplicationRunListener 对象的 starting 方法,主要是调用 EventPublishingRunListener 对象的 starting 方法。EventPublishingRunListener 对象的 starting 方法内部会通过 SimpleApplicationEventMulticaster 广播一个 ApplicationStartingEvent 事件,该事件会被相应的 ApplicationListener 处理。

生成并配置一个 ConfigurableEnvironment 对象

根据前面确定的 webApplicationType,生成一个 ConfigurableEnvironment 对象

webApplicationType 类型 ConfigurableEnvironment 对象类型
SERVLET StandardServletEnvironment
REACTIVE StandardReactiveWebEnvironment
其他 StandardEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) { // ⑴. 得到环境对象ConfigurableEnvironment,没有则创建一个StandardServletEnvironment ConfigurableEnvironment environment = getOrCreateEnvironment(); // ⑵. 配置环境信息(激活环境,通过从系统环境变量里取) configureEnvironment(environment, applicationArguments.getSourceArgs()); // ⑶. 发布ApplicationEnvironmentPreparedEvent事件,加载配置文件 listeners.environmentPrepared(environment); if (isWebEnvironment(environment) && !this.webEnvironment) { environment = convertToStandardEnvironment(environment); } return environment; } protected void configureEnvironment(ConfigurableEnvironment environment,String[] args) { configurePropertySources(environment, args); // 配置ConfigurableEnvironment中的激活属性 configureProfiles(environment, args); } protected void configureProfiles(ConfigurableEnvironment environment, String[] args) { environment.getActiveProfiles(); // ensure they are initialized // additionalProfiles是项目启动时在main中SpringApplication.setAdditionalProfiles("")配置的 Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles); // 获取环境变量中设置的spring.profiles.active属性 profiles.addAll(Arrays.asList(environment.getActiveProfiles())); // 赋值 activeProfiles environment.setActiveProfiles(StringUtils.toStringArray(profiles)); }

在这段代码里,会触发 ApplicationEnvironmentPreparedEvent。该事件会被 ConfigFileApplicationListener 处理(“从 spring.factories 文件中读取并实例化 ApplicationListener 对象”这一节有提及)。

ConfigFileApplicationListener 会加载不同优先级目录下的 application.properties、application.yml 文件。其处理 ApplicationEnvironmentPreparedEvent 事件的逻辑为:

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered { @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ApplicationEnvironmentPreparedEvent) { onApplicationEnvironmentPreparedEvent( (ApplicationEnvironmentPreparedEvent) event); } //... } private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { //从spring.factories文件中读取并实例化EnvironmentPostProcessor List<EnvironmentPostProcessor> postProcessors = loadPostProcessors(); //ConfigFileApplicationListener 自身也是一个EnvironmentPostProcessor,添加到列表 postProcessors.add(this); AnnotationAwareOrderComparator.sort(postProcessors); //依次执行EnvironmentPostProcessor的postProcessEnvironment方法 for (EnvironmentPostProcessor postProcessor : postProcessors) { postProcessor.postProcessEnvironment( event.getEnvironment(), event.getSpringApplication()); } } @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { addPropertySources(environment, application.getResourceLoader()); } //按照一定的优先级来加载不同位置的配置文件application.properties、application.yml //优先级高的配置覆盖优先级底的配置 //优先级从高到低为 file:./config/ > file:./ > classpath:config/ > classpath: protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { RandomValuePropertySource.addToEnvironment(environment); new Loader(environment, resourceLoader).load(); } }

生成并配置一个 ConfigurableApplicationContext 对象

根据前面确定的 webApplicationType,生成一个 ConfigurableApplicationContext 对象

webApplicationType 类型 ConfigurableApplicationContext 对象类型
SERVLET AnnotationConfigServletWebServerApplicationContext
REACTIVE AnnotationConfigReactiveWebServerApplicationContext
其他 AnnotationConfigApplicationContext

创建完毕 ConfigurableApplicationContext 后,对 ConfigurableApplicationContext 做一些初始化配置,包括调用 ApplicationContextInitializer 的 initialize 方法以及注册配置源。该过程中 hai 会广播 ApplicationContextInitializedEvent 和 ApplicationPreparedEvent 事件。

private void prepareContext( ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { //... 省略一些代码 //这里会调用前面从spring.factories文件中读取并实例化 //ApplicationContextInitializer对象的initialize方法, //用于对ConfigurableApplicationContext进一步处理 applyInitializers(context); //广播ApplicationContextInitializedEvent事件,被相应的ApplicationListener处理 listeners.contextPrepared(context); //... 省略一些代码 // Load the sources Set<Object> sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty"); // 根据 main方法所在的类(一般为配置类),注册为一个配置源 // 配置源用来配置一个spring容器,典型的配置源有xml文件和configuration类 load(context, sources.toArray(new Object[0])); //广播ApplicationPreparedEvent事件,被相应的ApplicationListener处理 listeners.contextLoaded(context); }

执行 ConfigurableApplicationContext 对象的 refresh 方法

ConfigurableApplicationContext 可能是 AnnotationConfigServletWebServerApplicationContext、AnnotationConfigReactiveWebServerApplicationContext 或者 AnnotationConfigApplicationContext。这里调用其 refresh 方法。

注意,这里是 spring 容器构建的重点,包含了 bean 的生成过程。后续会单开章节分析,本文暂不涉及。

执行 ConfigurableApplicationContext 对象的 refresh 方法完毕后,一个 spring 容器即构建完毕,可以被使用了。这一步的作用,相当于是在传统 spring 应用中执行 ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml")

public class App { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml"); User user = context.getBean(User.class); System.out.println(user.getName()); } }

调用 SpringApplicationRunListener 对象的 started 方法

调用 SpringApplicationRunListener 对象的 started 方法,其内部调用 ConfigurableApplicationContext 的 publishEvent 方法,发布一个 ApplicationStartedEvent 事件

SpringApplicationRunListeners 对象代码 public void started(ConfigurableApplicationContext context) { for (SpringApplicationRunListener listener : this.listeners) { listener.started(context); } } EventPublishingRunListener代码 @Override public void started(ConfigurableApplicationContext context) { context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context)); }

调用 SpringApplicationRunListener 对象的 running 方法

调用 SpringApplicationRunListener 对象的 running 方法,其内部调用 ConfigurableApplicationContext 的 publishEvent 方法,发布一个 ApplicationReadyEvent 事件

SpringApplicationRunListeners 对象代码 public void running(ConfigurableApplicationContext context) { for (SpringApplicationRunListener listener : this.listeners) { listener.running(context); } } EventPublishingRunListener代码 @Override public void running(ConfigurableApplicationContext context) { context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context)); }

SpringBoot 启动过程中触发的事件

从上诉代码分析可知,SpringBoot 启动过程各步骤中会调用 SpringApplicationRunListener 的相应方法,触发相应事件。总结如下:

动作 事件
生成 SpringApplication 对象 确定 webApplicationType
从 spring.factories 文件中读取并实例化 ApplicationContextInitializer 对象
从 spring.factories 文件中读取并实例化 ApplicationListener 对象
确定 main 方法所在的类
调用 SpringApplication 对象的 run 非静态方法 从 spring.factories 文件中读取并实例化 SpringApplicationRunListener 对象
调用 SpringApplicationRunListener 对象的 starting 方法 SpringApplicationRunListener.starting 方法,广播 ApplicationStartingEvent 事件
生成并配置一个 ConfigurableEnvironment 对象 SpringApplicationRunListener.environmentPrepared 方法,广播 ApplicationEnvironmentPreparedEvent 事件,通过 ConfigFileApplicationListener 加载配置文件 application.properties、application.yml
生成并配置一个 ConfigurableApplicationContext 对象 SpringApplicationRunListener.contextPrepared 和 contextLoaded 方法,广播 ApplicationContextInitializedEvent 和 ApplicationPreparedEvent 事件
执行 ConfigurableApplicationContext 对象的 refresh 方法 以上事件,都是通过 EventPublishingRunListener 内部的 SimpleApplicationEventMulticaster 广播出去的。在执行完 refresh 之后,spring 容器构建成功,各个 bean 都实例化了,后面的事件则是通过 spring 容器发布的。
调用 SpringApplicationRunListener 对象的 started 方法 SpringApplicationRunListener.started 方法,调用 ConfigurableApplicationContext 发布 ApplicationStartedEvent 事件
调用 SpringApplicationRunListener 对象的 running 方法 SpringApplicationRunListener.running 方法,调用 ConfigurableApplicationContext 发布 ApplicationReadyEvent 事件

从表可知,将 ApplicationListener 注册到 spring.factories 文件中,则该 ApplicationListener 可以监听到任何它想监听的事件;而在 ApplicationListener 类加 Component 注解,则该 ApplicationListener 只能监听到 ApplicationStartedEvent、ApplicationReadyEvent 事件。

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    946 引用 • 1460 回帖 • 1 关注
  • 启动过程
    1 引用
  • 代码
    467 引用 • 586 回帖 • 9 关注
  • 事件
    6 引用 • 43 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 6 关注
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    6 引用 • 15 回帖 • 44 关注
  • ZeroNet

    ZeroNet 是一个基于比特币加密技术和 BT 网络技术的去中心化的、开放开源的网络和交流系统。

    1 引用 • 21 回帖 • 639 关注
  • 生活

    生活是指人类生存过程中的各项活动的总和,范畴较广,一般指为幸福的意义而存在。生活实际上是对人生的一种诠释。生活包括人类在社会中与自己息息相关的日常活动和心理影射。

    230 引用 • 1454 回帖
  • GraphQL

    GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

    4 引用 • 3 回帖
  • Linux

    Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。它能运行主要的 Unix 工具软件、应用程序和网络协议,并支持 32 位和 64 位硬件。Linux 继承了 Unix 以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

    950 引用 • 943 回帖 • 1 关注
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖
  • danl
    163 关注
  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3455 回帖 • 166 关注
  • RemNote
    2 引用 • 16 回帖 • 9 关注
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    267 引用 • 666 回帖 • 1 关注
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有计划重制上线。

    14 引用 • 257 回帖 • 3 关注
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 54 关注
  • Swagger

    Swagger 是一款非常流行的 API 开发工具,它遵循 OpenAPI Specification(这是一种通用的、和编程语言无关的 API 描述规范)。Swagger 贯穿整个 API 生命周期,如 API 的设计、编写文档、测试和部署。

    26 引用 • 35 回帖 • 3 关注
  • 导航

    各种网址链接、内容导航。

    43 引用 • 177 回帖
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖
  • 996
    13 引用 • 200 回帖
  • 前端

    前端技术一般分为前端设计和前端开发,前端设计可以理解为网站的视觉设计,前端开发则是网站的前台代码实现,包括 HTML、CSS 以及 JavaScript 等。

    245 引用 • 1338 回帖 • 1 关注
  • IPFS

    IPFS(InterPlanetary File System,星际文件系统)是永久的、去中心化保存和共享文件的方法,这是一种内容可寻址、版本化、点对点超媒体的分布式协议。请浏览 IPFS 入门笔记了解更多细节。

    21 引用 • 245 回帖 • 235 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 718 关注
  • Q&A

    提问之前请先看《提问的智慧》,好的问题比好的答案更有价值。

    9345 引用 • 42566 回帖 • 114 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    143 引用 • 442 回帖
  • Postman

    Postman 是一款简单好用的 HTTP API 调试工具。

    4 引用 • 3 回帖 • 1 关注
  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    58 引用 • 22 回帖 • 2 关注
  • JRebel

    JRebel 是一款 Java 虚拟机插件,它使得 Java 程序员能在不进行重部署的情况下,即时看到代码的改变对一个应用程序带来的影响。

    26 引用 • 78 回帖 • 678 关注
  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    179 引用 • 408 回帖 • 486 关注
  • 新人

    让我们欢迎这对新人。哦,不好意思说错了,让我们欢迎这位新人!
    新手上路,请谨慎驾驶!

    52 引用 • 228 回帖