Spring Boot 启动(二) Environment 加载

Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html)

上一节中讲解了 SpringApplication 启动的整个流程,本节关注第二步 prepareEnvironment,尤其是配置文件的加载。

  1. Spring Boot 配置使用
  2. Spring Boot 配置文件加载流程分析 - ConfigFileApplicationListener

一、prepareEnvironment 加载流程分析

  1. public ConfigurableApplicationContext run(String... args) {
  2. // 1. listeners 用户监听容器的运行,默认实现为 EventPublishingRunListener
  3. SpringApplicationRunListeners listeners = getRunListeners(args);
  4. ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
  5. // 2. 初始化环境变量 environment
  6. ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
  7. }
  8. private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
  9. ApplicationArguments applicationArguments) {
  10. // 1. 根据 webApplicationType 创建相应的 Environment
  11. ConfigurableEnvironment environment = getOrCreateEnvironment();
  12. // 2. 配置 Environment,主要有三点:一是 ConversionService;二是数据源,包括命令行参数;三是 Profiles
  13. configureEnvironment(environment, applicationArguments.getSourceArgs());
  14. // 3. 激活 environmentPrepared 事件,主要是加载 application.yml 等配置文件
  15. // ConfigFileApplicationListener#ApplicationEnvironmentPreparedEvent
  16. listeners.environmentPrepared(environment);
  17. bindToSpringApplication(environment);
  18. if (!this.isCustomEnvironment) {
  19. environment = new EnvironmentConverter(getClassLoader())
  20. .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
  21. }
  22. // ??? 以后再研究
  23. ConfigurationPropertySources.attach(environment);
  24. return environment;
  25. }
  1. 根据 webApplicationType 类型创建相应的 Environment,分为 StandardEnvironment、StandardServletEnvironment、StandardReactiveWebEnvironment。

  2. configureEnvironment 主要有三点:一是 ConversionService;二是数据源,包括命令行参数;三是 Profiles

  3. 激活 environmentPrepared 事件,主要是加载 application.yml 等配置文件

2.1 getOrCreateEnvironment

对于 StandardServletEnvironment 的 servletContextInitParams 和 servletConfigInitParams 两个 web 的数据源,会先用 StubPropertySource 占位,等初始化 web 容器时再替换。详见:https://www.cnblogs.com/binarylei/p/10291323.html

2.2 configureEnvironment

  1. protected void configureEnvironment(ConfigurableEnvironment environment,
  2. String[] args) {
  3. // 1. 设置 ConversionService
  4. if (this.addConversionService) {
  5. ConversionService conversionService = ApplicationConversionService.getSharedInstance();
  6. environment.setConversionService((ConfigurableConversionService) conversionService);
  7. }
  8. // 2. 加载 defaultProperties 和 CommandLinePropertySource(main 参数) 信息
  9. configurePropertySources(environment, args);
  10. // 3. 设置 environment 的 Profiles(additionalProfiles + spring.profile.active/default)
  11. configureProfiles(environment, args);
  12. }
  1. configurePropertySources 添加在 Spring Framework 基础上添加了两个新的据源,一是自定义的 defaultProperties;二是 CommandLinePropertySource(main 参数)

  2. configureProfiles 在原有的剖面上添加自定义的剖面 additionalProfiles,注意 additionalProfiles 在前,Spring Framework 默认的剖面在后。

2.3 environmentPrepared

listeners.environmentPrepared(environment) 主要是加载配置文件,其中 listeners 是通过 spring.factories 配置的 SpringApplicationRunListener,默认实现是 EventPublishingRunListener。

  1. @Override
  2. public void environmentPrepared(ConfigurableEnvironment environment) {
  3. this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
  4. this.application, this.args, environment));
  5. }

environmentPrepared 触发了 ApplicationEnvironmentPreparedEvent 事件,这个事件是在 spring.factories 配置的监听器 ConfigFileApplicationListener 处理的。

二、ConfigFileApplicationListener

2.1 ConfigFileApplicationListener 处理流程

  1. public class ConfigFileApplicationListener
  2. implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
  3. @Override
  4. public void onApplicationEvent(ApplicationEvent event) {
  5. // 1. Environment 加载完成触发 ApplicationEnvironmentPreparedEvent
  6. if (event instanceof ApplicationEnvironmentPreparedEvent) {
  7. onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
  8. }
  9. // 2. ApplicationContext 加载完成触发 ApplicationPreparedEvent
  10. if (event instanceof ApplicationPreparedEvent) {
  11. onApplicationPreparedEvent(event);
  12. }
  13. }
  14. }

本例中触发了 ApplicationEnvironmentPreparedEvent 事件。

  1. private void onApplicationEnvironmentPreparedEvent(
  2. ApplicationEnvironmentPreparedEvent event) {
  3. // 1. 委托给 EnvironmentPostProcessor 处理,也是通过 spring.factories 配置
  4. List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
  5. // 2. ConfigFileApplicationListener 本身也实现了 EnvironmentPostProcessor 接口
  6. postProcessors.add(this);
  7. // 3. spring 都都通过 AnnotationAwareOrderComparator 控制执行顺序
  8. AnnotationAwareOrderComparator.sort(postProcessors);
  9. // 4. 执行 EnvironmentPostProcessor
  10. for (EnvironmentPostProcessor postProcessor : postProcessors) {
  11. postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
  12. }
  13. }

在 spring.factories 配置文件中默认定义了三个 EnvironmentPostProcessor 的实现类:

  1. # Environment Post Processors
  2. org.springframework.boot.env.EnvironmentPostProcessor=\
  3. org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
  4. org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
  5. org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor

优先级 SystemEnvironmentPropertySourceEnvironmentPostProcessor > SpringApplicationJsonEnvironmentPostProcessor

  • SystemEnvironmentPropertySourceEnvironmentPostProcessor 对 systemEnvironment 属性进行了包装。
  • SpringApplicationJsonEnvironmentPostProcessor 解析 spring.application.json 或 SPRING_APPLICATION_JSON 配置的 json 字符串。

同时 ConfigFileApplicationListener 也实现了 EnvironmentPostProcessor 接口。我们重点关注的是 ConfigFileApplicationListener 是如何加载配置文件的,其它的 EnvironmentPostProcessor 暂时忽略。跟踪 ConfigFileApplicationListener#postProcessEnvironment 方法,最终加载配置文件委托给了其内部类 Loader 完成。

  1. protected void addPropertySources(ConfigurableEnvironment environment,
  2. ResourceLoader resourceLoader) {
  3. // 1. 加载随机数据源 ${random.int} ${random.long} ${random.uuid}
  4. RandomValuePropertySource.addToEnvironment(environment);
  5. // 2. 加载配置文件
  6. new Loader(environment, resourceLoader).load();
  7. }

三、Loader 加载配置文件

3.1 Spring Boot 默认目录及配置文件名

Spring Boot 默认的配置文件的目录及配置文件名称如下:

  1. // 1. 配置文件默认的目录,解析时会倒置,所以 Spring Boot 默认 jar 包的配置文件会覆盖 jar 中的配置文件
  2. private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
  3. public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
  4. // 2. 配置文件默认的文件名
  5. private static final String DEFAULT_NAMES = "application";
  6. public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

3.2 profiles 解析配置文件的顺序

先了解一起 Spring FrameWork 和 Spring Boot 的 profiles 的概念。

配置 Spring 说明
spring.profiles.active Spring FrameWork AbstractEnvironment 激活的剖面
spring.profiles.default Spring FrameWork AbstractEnvironment 默认剖面
additionalProfiles Spring Boot SpringApplication 自定义激活的剖面
spring.profiles.include Spring Boot ConfigFileApplicationListener 自定义激活的剖面

在启动 SpringApplication#prepareEnvironment 时已经激活了 additionalProfiles + Spring FrameWork 剖面,注意剖面的顺序。 ConfigFileApplicationListener 引入 spring.profiles.include

  1. private void initializeProfiles() {
  2. // 1. null
  3. this.profiles.add(null);
  4. // spring.profiles.include + spring.profiles.active 配置的剖面
  5. Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
  6. // 2. environment.getActiveProfiles() 过滤 activatedViaProperty 之后的剖面
  7. // 目前看只有 SpringApplication 配置的 additionalProfiles
  8. this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
  9. // 3. spring.profiles.include + spring.profiles.active
  10. // addActiveProfiles 方法只能调用一次,前提是 activatedViaProperty 不为空
  11. addActiveProfiles(activatedViaProperty);
  12. // 4. spring.profiles.default
  13. if (this.profiles.size() == 1) {
  14. for (String defaultProfileName : this.environment.getDefaultProfiles()) {
  15. Profile defaultProfile = new Profile(defaultProfileName, true);
  16. this.profiles.add(defaultProfile);
  17. }
  18. }
  19. }

Spring Boot 配置文件 Profiles(application-dev.properties) 的解析顺序如下:

  1. 首先解析 null,也就是 application.properties 或 application.yml 文件
  2. spring.profiles.include/active 属性配置之外的剖面先解析,一般是 activatedViaProperty 或其它编程式配置的 Profiles
  3. spring.profiles.include 定义的剖面,第三和第四的顺序在 getProfilesActivatedViaProperty 中定义
  4. spring.profiles.active 定义的剖面
  5. spring.profiles.default 如果没有激活的剖面,默认 default,即没有 2、3、4 项

注意:实际读取配置文件的顺序和解析的相反,下面会详细说明。

还有一种情况是在配置文件 application.properties 中定义了 spring.profiles.include/active 属性的情况。加载到配置文件后需要判断是否定义了以上两个属性,如果定义了,也需要加载该剖面对应的配置文件。

  1. private void load(PropertySourceLoader loader, String location, Profile profile,
  2. DocumentFilter filter, DocumentConsumer consumer) {
  3. // 省略...
  4. List<Document> loaded = new ArrayList<>();
  5. for (Document document : documents) {
  6. if (filter.match(document)) {
  7. // 1. spring.profiles.active,如果已经定义了该方法就不会再执行了
  8. addActiveProfiles(document.getActiveProfiles());
  9. // 2. spring.profiles.include
  10. addIncludedProfiles(document.getIncludeProfiles());
  11. loaded.add(document);
  12. }
  13. }
  14. }
  15. // 毫无疑问,如果配置文件中定义了 spring.profiles.include 则需要先解析这些剖面,再解析其余的剖面
  16. private void addIncludedProfiles(Set<Profile> includeProfiles) {
  17. LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
  18. this.profiles.clear();
  19. // 1. 先解析配置文件中定义的 spring.profiles.include,当然如果已经解析了则需要排除
  20. this.profiles.addAll(includeProfiles);
  21. this.profiles.removeAll(this.processedProfiles);
  22. // 2. 再解析剩余的剖面
  23. this.profiles.addAll(existingProfiles);
  24. }

总结,(1) 剖面最终的读取顺序如下:

  1. spring.profiles.active 配置的剖面
  2. spring.profiles.include 配置的剖面
  3. 编程式配置的剖面,如 SpringApplicaiton#etAdditionalProfiles 或 environment#addActiveProfile
  4. 如果未定义激活的剖面,则 spring.profiles.default
  5. 默认的配置文件,如 application.properties
  6. 如果 1-5 项定义了多个,则后面定义的剖面覆盖前面的剖面,如 spring.profiles.active=dev,test 则 test 覆盖 dev

(2) 文件名定义的读取顺序如下:

  1. spring.config.name 定义了配置文件名,默认为 applicaiton,可以定义多个,如果有多个则后面的覆盖前面的

(3) 目录定义的读取顺序如下:

  1. spring.config.location 定义配置文件所在目录,默认为 classpath:/,classpath:/config/,file:./,file:./config/也就是后面的覆盖前面的配置,也就是 jar 包外的配置覆盖 jar 包内的配置。注意 spring.config.location 如果指定了文件名则 spring.config.name 不会生效。

  2. spring.config.additional-location 上面的配置会覆盖 Spring Boot 的默认配置目录,而本配置项则是在默认配置项上追加,先读取 spring.config.additional-location 再读取默认的目录。当然如果显示的定义了 spring.config.location 就只会读取这一项。

3.3 配置文件解析

  1. public void load() {
  2. // 所有的要解析的 profiles,注意读取配置文件的时候可以会增加
  3. // 因为配置文件中可能又定义了 spring.profiles.include 属性
  4. this.profiles = new LinkedList<>();
  5. // 已经解析过的 profiles,可以避免循环解析
  6. this.processedProfiles = new LinkedList<>();
  7. this.activatedProfiles = false;
  8. this.loaded = new LinkedHashMap<>();
  9. // 1. this.profiles 集合定义了 profile 解析顺序
  10. initializeProfiles();
  11. while (!this.profiles.isEmpty()) {
  12. Profile profile = this.profiles.poll();
  13. if (profile != null && !profile.isDefaultProfile()) {
  14. addProfileToEnvironment(profile.getName());
  15. }
  16. // 2. 具体解析配置文件到 this.loaded 中
  17. load(profile, this::getPositiveProfileFilter,
  18. addToLoaded(MutablePropertySources::addLast, false));
  19. this.processedProfiles.add(profile);
  20. }
  21. // 3. 解析后 environment#getActiveProfles 可能和配置文件的顺序 processedProfiles 不一致
  22. resetEnvironmentProfiles(this.processedProfiles);
  23. // 4. 默认的配置文件中定义了剖面,则要看这个配置文件定义的剖面是否激活
  24. // 即 application.properties 定义了 spring.profile=dev,dev 如果被激活则加载
  25. load(null, this::getNegativeProfileFilter,
  26. addToLoaded(MutablePropertySources::addFirst, true));
  27. // 5. 加载配置文件到 environment 中,注意读取配置文件的顺序和解析的相反
  28. addLoadedPropertySources();
  29. }
  1. initializeProfiles 加载所有的剖面,解析时会按上面提到的顺序进行解析
  2. load 具体解析配置文件到 this.loaded 中
  3. addLoadedPropertySources 加载配置文件到 environment 中,注意读取配置文件的顺序和解析的相反

配置文件属性那一种剖面有三种定义方式:

  1. 文件名指定:application-dev.properties 属于 dev 剖面
  2. 文件名为 application.properties 但文件配置了 spring.profile=dev 属性也属于 dev 剖面
  3. 以上两种都指定了,即文件名为 application-dev.properties 的同时文件配置属性 spring.profile=dev

Spring Boot 针对以上三种情况均有支持。load 方法加载配置文件,最终调用 loadForFileExtension 方法。

  1. private void loadForFileExtension(PropertySourceLoader loader, String prefix,
  2. String fileExtension, Profile profile,
  3. DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
  4. DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
  5. DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
  6. // 1. application-dev.properties
  7. if (profile != null) {
  8. String profileSpecificFile = prefix + "-" + profile + fileExtension;
  9. // application-dev.properties 这二个 load 最多只可能有一个生效 (gh-340)
  10. load(loader, profileSpecificFile, profile, defaultFilter, consumer);
  11. // application-dev.properties && spring.profile=dev
  12. load(loader, profileSpecificFile, profile, profileFilter, consumer);
  13. // Try profile specific sections in files we've already processed
  14. for (Profile processedProfile : this.processedProfiles) {
  15. if (processedProfile != null) {
  16. String previouslyLoaded = prefix + "-" + processedProfile
  17. + fileExtension;
  18. load(loader, previouslyLoaded, profile, profileFilter, consumer);
  19. }
  20. }
  21. }
  22. // 2. application.properties && spring.profile=dev
  23. load(loader, prefix + fileExtension, profile, profileFilter, consumer);
  24. }

DocumentFilter 判断文件中是否定义了 spring.profile 的剖面

  1. private DocumentFilter getPositiveProfileFilter(Profile profile) {
  2. return (Document document) -> {
  3. // profile==null 则文件中不能定义 spring.profile
  4. if (profile == null) {
  5. return ObjectUtils.isEmpty(document.getProfiles());
  6. }
  7. // profile!=null 则配置文件中定义的 spring.profile 包含该 profile
  8. // 且该配置文件定义的 spring.profile 被激活了
  9. return ObjectUtils.containsElement(document.getProfiles(), profile.getName())
  10. && this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
  11. };
  12. }

另外这里的 PropertySourceLoader 也是通过 spring.factories 定义的,默认为 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 两种。


每天用心记录一点点。内容也许不重要,但习惯很重要!

Spring Boot 启动(二) Environment 加载的更多相关文章

  1. spring boot 启动 开启注解 加载 bean

    业务描述:创建一个cache类然后交给spring 管理. @Component @Scope("singleton") public class Cache { public C ...

  2. Spring Boot自定义配置与加载

    Spring Boot自定义配置与加载 application.properties主要用来配置数据库连接.日志相关配置等.除了这些配置内容之外,还可以自定义一些配置项,如: my.config.ms ...

  3. Spring:启动项目时加载数据库数据(总结)

    在项目中需要启动程序时,要将数据库的用户信息表加载到内存中,找到一下几种方式. 1.实现ApplicationListener接口,重写onApplicationEvent方法,可以在项目启动的时候执 ...

  4. Spring Boot 2程序不能加载 com.mysql.jdbc.Driver 问题

    用Spring Boot Starter 向导生成了一个很简单SpringBoot程序, 用到了 MySQL, 总是下面不能加载 Mysql driver class 错误. Cannot load ...

  5. Spring Boot 静态资源能加载css 不能加载js

    Spring Boot 配置拦截器的时候默认 是放行 静态资源 , 也就是说不需要进行配置 registry.addResourceHandler("/**") .addResou ...

  6. 1.Spring项目启动时,加载相关初始化配置

    Spring项目启动时,会加载一些常用的配置: 1.加载spring上下文 SpringApplicationContextUtils.initApplicationContext(event.get ...

  7. spring容器启动完成后加载自定义逻辑

    业务需求中,可能会有一些逻辑需要在应用启动完成后,例如字典缓存,资源池初始化等等,代码如下 public class InitApplication implements ApplicationCon ...

  8. spring boot的静态资源加载

    1.spring boot默认资源处理 Spring Boot 默认为我们提供了静态资源处理,使用 WebMvcAutoConfiguration 中的配置各种属性. spring boot默认加载文 ...

  9. spring boot注解 --@spring-boot-devtools 自动加载修改的文件和类

    spriing boot中有一个注解,是自动加载修改后的类或者文件. 使用方法为: spring-boot-devtools=true 需要引入devtools包依赖: <dependency& ...

  10. spring boot开发 静态资源加载不出来

    spring boot 1.5 版本之前 不拦截静态资源 springboot 2.x版本 拦截静态资源 private static final String[] CLASSPATH_RESOURC ...

随机推荐

  1. xterm下字体设置

    code ~/.Xdefaults xterm*locale: true xterm.utf8: true xterm*utf8Title: true ! 滚动条 !XTerm*scrollBar: ...

  2. 关于rtsp的时间戳问题

    这里主要关注的rtp包的时间戳,在rtsp中,播放器的1S钟的定义是和媒体的采样率有关的. 例如视频的采样率是90K,那么最小时间粒度(单位)是1/90000秒,再转换成ms就是 1/90毫秒,这个就 ...

  3. python导入requests库一直报错原因总结 (文件名与库名冲突)

    花了好长时间一直在搞这个 源代码: 一直报如下错误: 分析原因: 总以为没有导入requests库,一直在网上搜索各种的导入库方法(下载第三方的requests库,用各种命令工具安装),还是报错 后来 ...

  4. Oracle + Mybatis批量插入数据,xml.mapper种的写法

    1,把表中去年所有的信息全部复制作为今年的数据,即查询出去年所有的数据然后复制插入 <insert id="cover" parameterType="java.l ...

  5. MySQL 之 MHA + ProxySQL + keepalived 实现读写分离,高可用(一)

    准备服务器: docker network create --subnet=192.168.0.0/16 staticnetdocker run -d --privileged -v `pwd`/my ...

  6. 关于PHP代码复用‘traits’的一段代码

    附:代码摘自菜鸟教程 <?php// 定义一个类名Base对象,并带有公共函数sayHello class Base { public function sayHello() { echo 'H ...

  7. 【Linux】【Chrome】安装Chrome浏览器的攻略

    https://blog.csdn.net/chenlix/article/details/72526205 1.切换到root: su - 或者 sudo -i 2.下载新的软件源定义: cd /e ...

  8. Oracle 关联查询

    select count(1),a.policy_id from gp_pol_prod a where a.product_id=8401 group by a.policy_id having c ...

  9. Python字符串格式化 (%操作符)

    在许多编程语言中都包含有格式化字符串的功能,比如C和Fortran语言中的格式化输入输出.Python中内置有对字符串进行格式化的操作%. 模板 格式化字符串时,Python使用一个字符串作为模板.模 ...

  10. [FE] 有效开展一个前端项目1

    今天的前端如果没有用到 npm,效率是比较低的:所以要从使用的工具来讲. 1. 一切都依赖于 nodejs: 下载一个 linux 的源码包就可以开始安装了. $ wget https://nodej ...