本文已参与「新人创作礼」活动,一起开启掘金创作之路。
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 变量中. 这一步运行结束,配置文件的信息已经添加进来了.
进入ConfigFileApplicationListener的onApplicationEvent()方法如下:
执行 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 ,它支持
properties和xml两种格式。
- 对于 YamlPropertySourceLoader,它支持
yml和yaml两种格式。
虽然加载了 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.active和spring.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。
\