《SpringBoot》第05章 配置文件解析

news2024/7/6 18:47:27

前言

SpringBoot中的application.properties(或application.yaml)文件都是再熟悉不过的了。它是应用的配置文件,我们可以把需要的一些配置信息都写在这个文件里面,需要的时候,我们可以通过@Value注解来直接获取即可,那这个文件是什么时候以及如何被应该加载的呢?那么这里就分析一下

其实作者是在使用Nacos配置中心的时候,感到很困惑,所里这里先研究一下单纯SpringBoot的配置文件,然后再看Nacos

一、原理概述

整个配置文件的加载过程其实就是一个事件监听的机制来实现的,整个过程的主要流程如下:
在这里插入图片描述

  1. 触发监听器,来启动配置文件的加载。SpringBoot只是采用的监听器的方式,如果直接代码调用也可以。
  2. ConfigFileApplicationListener监听器中,遍历解析各个配置文件,将解析到的配置存入Environment中。

二、源码分析

1.触发监听器

SpringBoot启动的时候,会读取META-INF/spring.factories配置文件中的监听器,其中的ConfigFileApplicationListener监听器,这个就是和读取配置文件相关的一个监听器:
在这里插入图片描述

这个监听器负责监听两个事件了:ApplicationEnvironmentPreparedEventApplicationPreparedEvent

在这里插入图片描述

既然监听器有了,那么何时触发监听器呢?在SpringBoot的启动类SpringApplication中:
在这里插入图片描述

在这里插入图片描述

发布事件以后,事件发布器就会遍历全部的监听器,找到匹配的监听器,自然而然就找到了ConfigFileApplicationListener,这些都是监听器的基础知识。

2.监听器执行

读取配置文件的代码主要在ConfigFileApplicationListener的内部类Loader中,这里只解释部分重要代码:

1) 哪些路径下的配置文件会被加载?

在使用过程中,一般通过IEDA创建SpringBoot以后,配置文件会默认在resources下面,那么只能放在这下面吗?肯定不是的

  1. 首先我们可以通过配置spring.config.additional-location属性值来指定我们需要去加载的路径,而且这个配置的路径是优先去加载的
  2. 同样可以通过配置spring.config.location这个属性值来指定需要加载的路径,只不过这个配置的优先级是低于spring.config.additional-location的优先级的
  3. 如果我们没有配置spring.config.location,Spring则会加入默认的4个加载路径:classpath:/,classpath:/config/,file:./,file:./config/,这也是为什么我们开发的时候将配置文件写在类路径下就可以被加载到的原因
    在这里插入图片描述

然后思考一下,既然路径有多个,接下来就是遍历解析每个路径:

在这里插入图片描述

2) 配置文件的名称只能 application 开头吗?

默认的配置文件为application.propertiesapplicaiton.yml,后缀有两个,那么前缀其前缀application可以通过spring.config.name来修改,但一般就是默认的

// ConfigFileApplicationListener -> Loader.java

public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

private static final String DEFAULT_NAMES = "application";

private Set<String> getSearchNames() {
   // 1.获取配置 spring.config.name
   if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
      String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
      return asResolvedSet(property, null);
   }
   // 2.没有配置就使用默认的 application
   return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

3) 配置文件可以支持哪些后缀?

SpringBoot通过不同的加载器来进行不同的后缀进行加载的。在底层构建加载的Loader类的时候,SpringBoot就从spring.factories的配置文件中读取到了两种属性资源加载器并进行了实例化(如下图),即:PropertiesPropertySourceLoaderYamlPropertySourceLoader

在这里插入图片描述

在这里配置了允许加载的配置文件后缀类型:

在这里插入图片描述

4) 遍历解析配置文件

上面介绍了:允许配置的路径共计4个,文件名称默认为application,支持的后缀由解析器返回。余下的解析方式就是很简单的遍历。

在这里插入图片描述

接下来看一下具体如何解析:

在这里插入图片描述

具体的解析代码:

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
      DocumentConsumer consumer) {
   try {
      Resource resource = this.resourceLoader.getResource(location);
      // 1.判断配置文件资源是否存在
      if (resource == null || !resource.exists()) {
         if (this.logger.isTraceEnabled()) {
            StringBuilder description = getDescription("Skipped missing config ", location, resource,
                  profile);
            this.logger.trace(description);
         }
         return;
      }
      // 2.判断文件后缀名不存在
      if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
         if (this.logger.isTraceEnabled()) {
            StringBuilder description = getDescription("Skipped empty config extension ", location,
                  resource, profile);
            this.logger.trace(description);
         }
         return;
      }
      // 3.将资源配置文件路径拼接成为PropertySourceLoader加载程序name需要的格式
      String name = "applicationConfig: [" + location + "]";
      // 4.调用解析器
      // 返回的结果包含:解析完毕的PropertySource对象、配置profiles、激活配置activeProfiles
      List<Document> documents = loadDocuments(loader, name, resource);
       
      // 5.判定返回资源配置文件是否为空
      if (CollectionUtils.isEmpty(documents)) {
         if (this.logger.isTraceEnabled()) {
            StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
                  profile);
            this.logger.trace(description);
         }
         return;
      }
      List<Document> loaded = new ArrayList<>();
      // 6.加载配置
      for (Document document : documents) {
         if (filter.match(document)) {
            addActiveProfiles(document.getActiveProfiles());
            addIncludedProfiles(document.getIncludeProfiles());
            loaded.add(document);
         }
      }
      Collections.reverse(loaded);
      // 7.调用消费者回调函数,将加载到的文档存放入loaded属性
      if (!loaded.isEmpty()) {
         loaded.forEach((document) -> consumer.accept(profile, document));
         if (this.logger.isDebugEnabled()) {
            StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
            this.logger.debug(description);
         }
      }
   }
   catch (Exception ex) {
      throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
   }
}

5) 哪里负责解析配置文件?

第1步:方法入口】
在这里插入图片描述

第2步:解析步骤】

// 解析配置文件
private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
      throws IOException {
   // 1.创建用于保存多次加载同一文档的缓存键,  避免重复加载
   DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
   // 2.缓存获取
   List<Document> documents = this.loadDocumentsCache.get(cacheKey);
   if (documents == null) {
      // 3.解析配置
      List<PropertySource<?>> loaded = loader.load(name, resource);
      // 4.封装结果
      documents = asDocuments(loaded);
      // 5.存入缓存
      this.loadDocumentsCache.put(cacheKey, documents);
   }
   return documents;
}

第3步:解析配置文件】

在这里插入图片描述

第4步:封装返回结果】

// 封装返回结果
private List<Document> asDocuments(List<PropertySource<?>> loaded) {
   if (loaded == null) {
      return Collections.emptyList();
   }
   return loaded.stream().map((propertySource) -> {
      Binder binder = new Binder(ConfigurationPropertySources.from(propertySource),
            this.placeholdersResolver);
      return new Document(propertySource, binder.bind("spring.profiles", STRING_ARRAY).orElse(null),
            getProfiles(binder, ACTIVE_PROFILES_PROPERTY), getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
   }).collect(Collectors.toList());
}

解析后的结果,包含配置、spring.profile.activesprng.profile.include

在这里插入图片描述

6) 如何实现不同环境配置文件的切换的呢?

第1点:知识回顾】

使用过SpringBoot的都知道,可以通过spring.profiles.activespring.profiles.include切换不同的环境,首先回忆一下使用:

spring.profiles.active:切换不同环境的配置文件,这个最常用,就不详细介绍了

spring.profiles.include:包含,可以将1个配置文件拆成几部分存储,也是在原来的配置上扩展

那么大体看来,两种好像功能一样,都是扩展原来的配置文件。区别在于:优先级不同,spring.profiles.active更高

配置示例一

application.properties中,同时配置了spring.profiles.active=devspring.profiles.include=dev1,dev2

那么加载的顺序为dev1,dev2,dev,因为优先级不同

配置示例二

application.properties中,配置spring.profiles.active=devapplication-dev.properties中,配置spring.profiles.include=dev1,dev2。使用application-dev.properties时自动就激活了dev1、dev2两个文件,不用再次指定。

那么加载的顺序为dev,dev1,dev2

第2点:具体实现】

那么如何实现的呢? 首先来到其总入口:

在这里插入图片描述

首先看一下initializeProfiles(),方法逻辑如下:

  1. 首先存入一个null,代表application.properties要首先被加载
  2. 获取其它的profiles。(这里有人可能疑问了?配置文件还没加载呢,咋能有其它profiles呢?我们可以在启动jar包的时候配置上)
  3. 如果没有配置profiles,那么会存入一个默认的default,代表默认会加载application-default.properties
private void initializeProfiles() {
   // 1.首先存入null,代表application.properties要首先被加载的
   //   这也印证了application.properties是优先级最低的,其它的配置可以覆盖我的
   this.profiles.add(null);
   Set<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
   Set<Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
   List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
   // 2.存入配置
   this.profiles.addAll(otherActiveProfiles);
   this.profiles.addAll(includedViaProperty);
   addActiveProfiles(activatedViaProperty);
   if (this.profiles.size() == 1) {
      // 3.如果没有激活的配置,则加入一个default,代表没有使用active,则加载application-default.properties
      for (String defaultProfileName : this.environment.getDefaultProfiles()) {
         Profile defaultProfile = new Profile(defaultProfileName, true);
         this.profiles.add(defaultProfile);
      }
   }
}

看完了initializeProfiles(),那么在继续回到上面的方法,就可以解释一切了:

在这里插入图片描述

第3点:profile存入】

解析完配置文件以后,会把最新读取到的profile存入profiles

在这里插入图片描述

解析到的spring.profile.active会存到最后,代表优先级最高

在这里插入图片描述

解析到的spring.profile.include会存到最前面,代表其优先级最低

在这里插入图片描述

7) 我们的配置文件被加载后最终是放到了哪里呢?

其实在我们调用load()的时候,一直传入了一个函数式接口DocumentConsumer,其中这个addToLoaded(MutablePropertySources::addLast, false)就是函数式接口的实现,这就是函数式变成,不得不说Spring的开发人员还是腻害的。

在这里插入图片描述

在我们加载完配置文件并封装成Document后,调用了上面DocumentConsumer的accept方法,将Document对象的PropertySources封装到了MutablePropertySourcespropertySourceList中,并将MutablePropertySources对象放入了loaded集合中:
在这里插入图片描述

然后等到所有的配置文件都加载封装完成后,会统一将这些封装后的MutablePropertySources对象放入到Environment中。也就是说,最终我们所有的配置文件的信息都是加载到了Environment中的,以后我们要是拿都是在Environment中拿的
在这里插入图片描述

在这里插入图片描述

3.@Value是如何解析的

当把配置文件解析到Environment中,那么@Value是如何读取的?那么这里简单的介绍几个关键点

第1点:解析@Value的入口】

AbstractAutowireCapableBeanFactory#doCreateBean(),其中会调用populateBean(),该方法负责Bean填充,包括解析@Autowired@Resource@Value

在该方法中,会通过后置处理器AutowiredAnnotationBeanPostProcessor来解析

在这里插入图片描述

在该后置处理器中,有2个内部类:

在这里插入图片描述

具体解析属性的方法inject()

在这里插入图片描述

至此后续的逻辑就很清晰了,解析掉${},然后去Environment找即可,那么这里再贴几张图:

在这里插入图片描述

在这里插入图片描述

三、参考文章

https://blog.csdn.net/ITlikeyou/article/details/124652978 《SpringBoot加载配置文件原理分析》

https://blog.csdn.net/yaomingyang/article/details/109259884 《死磕源码系列【ConfigFileApplicationListener监听器源码解析】》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/725799.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

deepstream指北——python接口的使用

目录 一、机器配置二、环境配置三、运行实例 一、机器配置 电脑&#xff1a;台式机系统&#xff1a;Ubuntu 20.04.5显卡&#xff1a;GTX 1070&#xff0c;8G显存软件版本&#xff1a;deepstream&#xff1a;6.1.1显卡驱动版本&#xff1a;515.76CUDA版本&#xff1a;11.7.1cud…

基于PyQt5的桌面图像调试仿真平台开发(10)色彩矩阵

系列文章目录 基于PyQt5的桌面图像调试仿真平台开发(1)环境搭建 基于PyQt5的桌面图像调试仿真平台开发(2)UI设计和控件绑定 基于PyQt5的桌面图像调试仿真平台开发(3)黑电平处理 基于PyQt5的桌面图像调试仿真平台开发(4)白平衡处理 基于PyQt5的桌面图像调试仿真平台开发(5)…

当赛博朋克碰上小鸡舞 segment anything

Segment Anything能给我们做什么 前言内容具体实现成果 前言 最近&#xff0c;大模型的热度确实是非常非常的高&#xff0c;从chatgpt到segment anything&#xff0c;这些东西整的我这刚入门的小白确实有点懵逼。最近实在是不知道干啥&#xff0c; 想想能不能用大模型整点花活…

Spring后置处理器BeanFactoryPostProcessor与BeanPostProcessor源码解析

文章目录 一、简介1、BeanFactoryPostProcessor2、BeanPostProcessor 二、BeanFactoryPostProcessor 源码解析1、BeanDefinitionRegistryPostProcessor 接口实现类的处理流程2、BeanFactoryPostProcessor 接口实现类的处理流程3、总结 三、BeanPostProcessor 源码解析 一、简介…

uniapp 之 多端实现图片压缩(含H5实现)

compressImage 说明 文档平台差异说明已标出&#xff1a;官网提供的api uni.compressImage除了H5平台&#xff0c;其余平台都支持&#xff0c;所以我们利用条件编译&#xff0c;然后单独处理一下H5的图片压缩即可。 utils.js 里面封装一下该方法&#xff0c;方便调用 /*** 图…

-Xloggc:d:/gc.log

-Xloggc:d:/gc.log把信息记录成 log文件参数-Xmx20m -Xms20m -XX:NewRatio4 -XX:SurvivorRatio2 -Xss1m -XX:PrintGCDetails -XX:UseSerialGC -XX:PrintCommandLineFlags -Xloggc:d:/gc.log结果[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y3PpljS…

【PyTorch API】 nn.RNN 和 nn.LSTM 介绍和代码详解

文章目录 1. nn.RNN 构建单向 RNN2. nn.LSTM 构建单向 LSTM3. 推荐参考资料 1. nn.RNN 构建单向 RNN torch.nn.RNN 的 PyTorch 链接&#xff1a;torch.nn.RNN(*args, **kwargs) nn.RNN 的用法和输入输出参数的介绍直接看代码&#xff1a; import torch import torch.nn as n…

商业模式画布

商业模式画布给了创业者一个思考的框架&#xff0c;在行动之前充分思考和演练。 文章目录 认识商业模式画布九个组成部分&#xff08;以Zoom为例拆解&#xff09;收入成本 九个组成部分的关系总结 认识商业模式画布 九个组成部分&#xff08;以Zoom为例拆解&#xff09; 收入 成…

七月创作之星挑战赛开始咯~

活动火热进行中&#xff01; 欢迎各位大佬积极参与~ 大家请加入卡奥斯开源社区官方社群哦&#xff0c;最新活动实时更新&#xff01; 还有专属群内福利&#xff08;蛋糕券、购物卡、周边礼品&#xff09;等你来拿~ 礼品详情

高效管理工作任务,推荐优秀任务管理软件助力工作效率提升

任务管理软件是一种用于组织任务、将任务分配给个人并监控其进展的软件。该软件可以帮助确保任务在预算内按时完成。它在协同工作环境中特别有用&#xff0c;在这种环境中&#xff0c;多人在处理需要跟踪和监视的任务。 任务管理软件可以帮助简化分配任务和监控任务进度的过程。…

ModaHub魔搭社区:基于阿里云 ACK 搭建开源向量数据库 Milvus

目录 一、准备资源 二、集群创建&#xff1a; 本集群基于Terway网络构建 二、连接刚刚创建的ACK集群 三、部署Milvus数据库 四、优化Milvus配置 简介&#xff1a; 生成式 AI&#xff08;Generative AI&#xff09;引爆了向量数据库&#xff08;Vector Database&#xff0…

STM8低门槛快速入门,类似Arduino封装库模式开发介绍

STM8低门槛快速入门&#xff0c;类似Arduino封装库模式开发介绍 &#x1f4cc;STM8外设封装库原项目开源地址&#xff1a;https://github.com/gicking/STM8_templates&#x1f4cd;个人整理过的项目地址&#xff1a;https://github.com/perseverance51/STM8-Templates &#x1…

前端开发常用Nginx设置说明

前端部署常用到Nginx&#xff0c;作为前端开发常用的配置不多&#xff0c;担也需要掌握 常见配置说明&#xff0c;这里只列表server模块的核心代码 server {listen 9015; # 端口号server_name 172.16.101.191; # 浏览器访问域名&#xff0c;不配置默认为本服务器地址index in…

redhat6安装mysql8.0.33

1、下载mysql 官网地址&#xff1a;https://downloads.mysql.com/archives/community/ 下载步骤&#xff1a; 过滤操作系统版本 下载后&#xff0c;上传到服务器Downloads目录 2、安装mysql8 解压压缩包 tar -xvf mysql-8.0.31-1.el9.x86_64.rpm-bundle.tar [rootrhel64 …

node搭建一个简单的脚手架

一、什么是脚手架 脚手架&#xff08;Scaffold&#xff09;是指在软件开发过程中为提高开发效率而提供的一套基础代码结构、组织规范、开发工具和工程化配置的工具。脚手架可以帮助开发团队快速搭建项目的基础框架&#xff0c;规范项目的开发流程&#xff0c;并提供一些常用的…

指针函数与函数指针

指针函数 指针函数&#xff1a;指针函数是一个函数&#xff0c;返回值是一个指针。 int *fun; //fun是指针变量 int *fun(x,y); //fun是指针函数; #include<iostream> using namespace std;char* day_name() {return("Monday"); //返回地址 }int main() {char…

堆排序选择排序

选择排序 选择排序&#xff08;Selection sort&#xff09;是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小&#xff08;大&#xff09;元素&#xff0c;存放到排序序列的起始位置&#xff0c;然后&#xff0c;再从剩余未排序元素中继续寻找最小&…

Linux系统下 - [linux命令]查找包含指定内容的文件

格式1&#xff1a;grep -r “指定内容” 目录 eg:输出包含"指定内容"的文件列表以及简要信息 查找当前目录下的 CONFIG_ESP_SMARTCONFIG_TYPE grep -r "CONFIG_ESP_SMARTCONFIG_TYPE" .格式2&#xff1a;grep -r -l “指定内容” 目录 eg:仅输出包含&q…

模拟Toast 自定义提示框

模拟Toast 自定义提示框 前言 为满足产品需求&#xff0c;发现现在的ToastUtils不是太重就是不太满足需求&#xff0c;这边写个简单易用的工具&#xff0c;几十行代码解决的问题,还要啥轮子。 功能如下&#xff1a; 自动消失相对锚点位置 可配置&#xff0c;正中间&#x…

刷题日记06《回溯算法》

问题描述 力扣https://leetcode.cn/problems/Ygoe9J/ 给定一个无重复元素的正整数数组 candidates 和一个正整数 target &#xff0c;找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。 candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同…