【源码】SpringBoot 启动流程 (三)

96 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

5.准备环境变量 prepareEnvironment

应用上下文环境包括计算机的环境, Java环境, SpringBoot 的运行环境, SpringBoot 项目的配置等等.

进入prepareEnvironment()方法如下:

1.获取或创建环境对象

进入getOrCreateEnvironment()方法, 内部就是根据当前应用的类型, 来创建对应的环境对象.

Servelt 项目创建StandardServletEnvironment对象, webfux 项目创建 StandardReactiveWebEnvironment 对象, 普通项目则创建 StandardEnvironment 对象.

StandardServletEnvironment 和 StandardReactiveWebEnvironment 是 StandardEnvironment 的子类. StandardServletEnvironment 类的结构图如下:

通过 F5 一路 DEBUG 可知, StandardServletEnvironment 该对象添加了如下的参数:

  • servletConfigINitParams参数
  • servletContextInitParams参数
  • 系统属性集参数
  • 系统环境变量参数

然后将这些参数设置到了环境对象的 propertySources 属性中.

2.设置类型转换器, 设置自定义参数

进入configureEnvironment()方法如下:

2.1 设置类型转换器

源码中ApplicationConversionService.getSharedInstance()方法创建了类型转换服务对象 ApplicationConversionService. 提供类型转换服务, 可以将 A 类型数据转换为 B 类型数据.

该方法内部使用双重检测机制的单例模式. 初始化之后 conversionService 被成功注入了 132 个类型转换器.

然后把类型转换服务对象设置到当前 应用 environment 中.

2.2 设置自定义参数

源码中configurePropertySources()方法则是 将 args 参数加入到环境中。

首先是如果存在 defaultProperties,则将其加入到环境中。

然后是判断了 args 参数是否有值:

如果有值,且在配置源中存在 commandLineArgs 的配置,则将其封装为对象,保存。

如果有值,但是在配置源中不存在 commandLineArgs 的配置,则将其封装成SimpleCommandLinePropertySource加入环境中.

上述的代码执行完毕之后,MutablePropertySources 类中 propertySourceList 已经存在的属性为:

  • commandLineArgs
  • servletConfigInitParams
  • servletContextInitParams
  • jndiProperties(如果存在)
  • systemProperties
  • systemEnvironment
  • defaultProperties(如果存在)

这些配置源的同名属性优先级从前到后依次降低,在最前面的使用优先级最高。即命令行的优先级最高、其次是程序中的、然后是系统的环境变量以及属性、最后是默认的。

2.3 设置 Profile 参数

源码中configureProfiles()方法则是 激活相应的配置文件.

不过经过多次 DEBUG 这块,因为没有设置如下参数,所以实际上并没有添加任何 Profile 进来。

3.启动相应的监听器

其中一个重要的监听器ConfigFileApplicationListener就是加载项目配置文件的监听器. 该监听器会从默认的位置加载配置文件, 并将其加入 上下文的 environment 变量中. 这一步运行结束,配置文件的信息已经添加进来了.

进入ConfigFileApplicationListeneronApplicationEvent()方法如下:

执行 onApplicationEnvironmentPreparedEvent()方法如下:

3.1 加载 EnvironmentPostProcessor 类

进入loadPostProcessors()方法如下,很熟悉的逻辑了,从 spring.factories 中实例化 EnvironmentPostProcessor 的类。

DEBUG 可以看到,一共有5个:

3.2 把自己加入进去

添加监听器 ConfigFileApplicationListener 到 postProcessors 中。

3.3 排序

排序后的顺序如下:

3.4 执行 EnvironmentPostProcessor 和监听器事件逻辑

这里会分别执行这六个 Processor 的 postProcessEnvironment() 方法,这里还是使用 ConfigFileApplicationListener 为例:

可以看到创建了一个Loader对象,并调用了其load()方法。Loader 是 ConfigFileApplicationListener 的内部类,也是加载配置文件的核心类。

进入 load() 方法,逻辑如下:

initializeProfiles()初始化方法源码如下:

主要完成两件事:

  • 向 profiles 中添加一个 null 的 profile,这个得作用主要用来加载没有指定 profile 的配置文件,比如:application.properties。第1步
  • 获取当前环境中激活的配置文件,根据 spring.profiles.active 参数或者 spring.profiles.include 参数指定的 application.yml 文件. 第2步,这里返回的是空.

获取其他激活的配置文件,这里也没有获取到。第3步.

添加激活的配置文件到 profiles,由于这里没获取到,所以没添加. 第4步

如果没有,这里 profiles 中只有 null 一个元素,第5步.

调用this.environment.getDefaultProfiles()方法,获取默认的 profile,看这里的意思是可以获取多个默认的. (但我这里 DEBUG 只有一个)

则创建一个默认的配置文件,形如 application-default.yml,application-default.properties. 第5步,第6步.

因为 profiles 采用了 LIFO 队列,后进先出。所以会先加载 profile 为 null 的配置文件,也就是先匹配application.properties、application.yml 配置。

初始化方法运行完毕之后,profiles 队列中存在如下两个元素:

然后就是循环解析文件:

其中load()方法会从指定位置,去加载配置文件,进入 load() 方法如下:

其中getSearchLocations()方法,是解析配置文件的位置,进入该方法如下:

这几个常量的值如下:

可以看到,首先如果配置了 spring.config.location属性,则从该属性指定的位置去找,并且立刻就返回了,不再进行后续的查找。如果没有该属性,则从spring.config.additional-location属性指定的位置去找。而且不论是否找到,都会再从 DEFAULT_SEARCH_LOCATIONS的位置去找,因为该参数是追加配置文件的意思,所以还会去找。也就是默认的配置文件的位置,可以看到是classpath下,或者classpath/config目录下,或者当前项目目录下,或者当前项目目录下的 config 目录下。

看到这里你会有疑问,DEFAULT_SEARCH_LOCATIONS中定义的默认配置文件的位置的优先级好像是反了?怎么回事呢?

其实在 asResolvedSet()方法内部,对其顺序 ( 优先级 ) 进行了调整。

方法执行完毕,由于这里未设置 属性,所以最后配置文件的位置只有默认的4个:

然后 foreach 循环对这些位置分别进行处理,这几个位置都是以/结尾的,所以会进入getSearchNames()方法如下:

首先还是判断了是否存在spring.config.name属性,如果存在,则使用该文件作为配置文件名称,否则使用默认的DEFAULT_NAMES作为配置文件名。

可以看到getSearchNames()方法的返回值是 Set 集合,而默认的文件名只有一个,所以说明spring.config.name属性支持配置多个文件名。

然后就是进入 laod() 方法,加载文件:

方法分为两部分逻辑,一部分是 文件名 name 没值的,一部分是文件名 name 有值的。

为什么有文件名 name 没值的情况呢?想了一下,可能是 spring.config.location 或者 spring.config.additional-location 支持配置到具体的文件上。也就是这里的 location 参数中包含文件名了,所以 name 可以为空。

从这两部分逻辑中加载文件的方法入参上,也能证明这一点。

由于这里使用默认配置文件名,所以 name 有值,直接看第二部分的逻辑:

首先是 for 循环遍历 this.propertySourceLoaders,从名字上看,是属性来源加载类,该属性是在前面实例化 Loader 的时候设置的。类型是 PropertySourceLoader类型的,是个接口,该接口位于 org.springframework.boot.env 包中。

查看该接口的实现类如下:

可以看到有两个实现类,一个用来加载.properties文件,一个用来加载.yml文件。

通过 DEBUG 看到,确实是这样:

内部 for 循环则遍历了 PropertySourceLoader 类 的getFileExtensions()方法,该方法返回当前属性资源加载类支持的文件格式。

  • 对于 PropertiesPropertySourceLoader ,它支持 propertiesxml两种格式。

  • 对于 YamlPropertySourceLoader,它支持ymlyaml两种格式。

虽然加载了 4 中格式的文件,但是其处理流程基本一致,这里以 properties 为例:

进入 loadForFileExtension()方法:

还记得 profiles 中有哪些值吗?由于 null 是排在最前面的,所以该方法的实际逻辑如下:

第1,2步获取了过滤器,不过这里 profile 是 null,所以啥都没有,然后进入 load() 方法,该方法主要做了三件事儿,判断是否存在,解析配置,应用配置。源码如下:

首先是判断了要加载的配置文件是否存在,不存在,则跳过。

接着是解析配置:项目配置存在 application.yml 文件,所以接着对配置进行解析:

进入loadDocuments()方法如下:

这里返回的是 List 格式,所有猜测 SpringBoot 支持激活多个配置文件.

通过调用 loader.load() 方法,这里是读取的 yml 文件,所以实际执行的是YamlPropertySourceLoader.loa()方法,该方法中首先读取了配置文件中的每一项内容,存放在List<Map<String, Object>>中,然后又转化为PropertySource对象。

PropertySource 对象的 name 属性是配置文件名,source 属性则是所有的配置项:

然后又调用asDocuments()方法将 PropertySource对象转为了List<Document>对象。

asDocuments()方法内部,对所有的配置项进行了查找,查找是否存在spring.profiles.activespring.profiles.include配置,如果存在,分别设置到Document对象的对应属性中去,Document 对象结构如下:

解析配置完成之后,就是对配置进行应用。

首先是遍历List<Document>,并设置需要激活的配置:

进入addActiveProfiles()方法如下,该方法中,主要是设置激活状态activatedProfiles=true,以及移除默认的 profile.

之前我们 DEBUG 的时候知道,initializeProfiles()方法执行之后,有两个 profile, 分别是 null 和 default,现在执行到这里后,就变为了 null 和 qa.

这里 profile == null 的就运行完毕了,还记得吗,profile == default 的被移除了,现在还需要循环 profile == qa 的。

这里会把 qa 设置到 environment 中去。

\

4.绑定环境参数到 SpringApplication

进入bindToSpringApplication(environment);方法内部:

先是调用了 Binder 类的 get 方法, 然后又调用了 bind 方法.

org.springframework.boot.context.properties.bind.Binder类是一个容器对象, 它绑定来自一个或多个 ConfigurationPropertySources 的对象.

该方法是把spring.main配置加入到 application 中。具体作用暂时没搞懂。

5.判断是否需要对容器类型进行转换

属性this.isCustomEnvironment默认值为 false,而 DEBUG 到这里,还没有任何地方对其进行修改,所以会执行 if 里的代码:

创建了EnvironmentConverter实例,并调用了其convertEnvironmentIfNecessary()方法,该方法将给定的 environment 转换为给定的 StandardEnvironment 类型。 如果环境已经是同一类型,则不进行转换,并且原样返回。

根据spring.main.web-application-type配置,判断是否需要转换环境.

6.将 ConfigurationPropertySource 添加到环境中

进入attach()方法如下:

首先是从enviorment中获取propertySources属性,此时是有 8 个,如下:

然后判断是否存在configurationProperties名字的元素,不存在,则实例化ConfigurationPropertySourcesPropertySource,并加入 propertySourcesList 中的头部。

所以,该方法运行结束后,会有 9 个:

\

运行到这里,prepareEnvironment() 方法就运行结束了,此时返回回来的 enviorment 对象如下:

StandardServletEnvironment类型的容器,激活的配置文件时 qa。

\