整体流程分析

SpringBoot的配置文件有两种 ,一种是 properties文件,一种是yml文件。在SpringBoot启动过程中会对这些文件进行解析加载。在SpringBoot启动的过程中,配置文件查找和解析的逻辑在listeners.environmentPrepared(environment)方法中。

void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.environmentPrepared(environment);
}
}

依次遍历监听器管理器的environmentPrepared方法,默认只有一个 EventPublishingRunListener 监听器管理器,代码如下,

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

监听管理器的多播器有中有11个,其中针对配置文件的监听器类为 ConfigFileApplicationListener,会执行该类的 onApplicationEvent方法。代码如下,

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
// 执行 ApplicationEnvironmentPreparedEvent 事件
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}

onApplicationEnvironmentPreparedEvent的逻辑为:先从/META-INF/spring.factories文件中获取实现了EnvironmentPostProcessor接口的环境变量后置处理器集合,再把当前的 ConfigFileApplicationListener 监听器添加到 环境变量后置处理器集合中(ConfigFileApplicationListener实现了EnvironmentPostProcessor接口),然后循环遍历 postProcessEnvironment 方法,并传入 事件的SpringApplication 对象和 环境变量。

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}

在ConfigFileApplicationListener 的postProcessEnvironment方法中(其他几个环境变量后置处理器与读取配置文件无关),核心是创建了一个Load对象,并且调用了load()方法。

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}
  1. Loader是ConfigFileApplicationListener 的一个内部类,在Loader的构造方法中,会生成具体属性文件的资源加载类并赋值给this.propertySourceLoaders。代码如下,
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
//从 /META-INF/spring.factories 中加载 PropertySourceLoader 的实现类
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}

从 /META-INF/spring.factories 中加载 PropertySourceLoader 的实现类,在具体解析资源文件的时候用到。具体的实现类如下,

org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
  1. Loader类的load方法是加载配置文件的入口方法,代码如下,
void load() {
FilteredPropertySource.apply(...)
}

FilteredPropertySource.apply()方法先判断是否存在以 defaultProperties 为名的 PropertySource 属性对象,如果不存在则执行operation.accept,如果存在则先替换,再执行operation.accept方法。代码如下:

static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
Consumer<PropertySource<?>> operation) {
// 在环境变量中获取 属性资源管理对象
MutablePropertySources propertySources = environment.getPropertySources();
// 根据 资源名称 获取属性资源对象
PropertySource<?> original = propertySources.get(propertySourceName);
// 如果为null,则执行 operation.accept
if (original == null) {
operation.accept(null);
return;
}
//根据propertySourceName名称进行替换
propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
try {
operation.accept(original);
}
finally {
propertySources.replace(propertySourceName, original);
}
}
  1. operation.accept是一个函数接口,配置文件的解析和处理都在该方法中。具体的逻辑为:先初始化待处理的属性文件,再遍历解析待处理的属性文件并解析结果放在this.loaded中,然后添加this.loaded的数据至环境变量 this.environment.getPropertySources() 中,最后设置环境变量的ActiveProfiles属性。代码如下,
// 待处理的属性文件
this.profiles = new LinkedList<>();
// 已处理的属性文件
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
// 已经加载的 属性文件和属性
this.loaded = new LinkedHashMap<>();
// 添加 this.profiles 的 null 和 默认的属性文件
initializeProfiles();
// 循环 this.profiles 加载
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
// 如果是主属性文件则先添加到环境变量中的 addActiveProfile
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
// 真正加载逻辑
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
// 加载 profile 为null 的
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 添加已经加载的 属性文件和属性至 环境变量 this.environment.getPropertySources() 中
addLoadedPropertySources();
//根据已处理的属性文件设置环境变量的ActiveProfiles
applyActiveProfiles(defaultProperties);

配置文件解析过程

  1. 如上的load()的逻辑为:先获取所有的查找路径,再遍历查找路径并且获取属性配置文件的名称,最后根据名称和路径进行加载。这里会在 file:./config/,file:./,classpath:/config/,classpath:/ 四个不同的目录进行查找。优先级从左至右。
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// 获取 默认的 classpath:/,classpath:/config/,file:./,file:./config/ 文件路径
// 根据路径查找具体的属性配置文件
getSearchLocations().forEach((location) -> {
// 判断是否为文件夹
boolean isFolder = location.endsWith("/");
// 获取 属性配置文件的名称
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
// 根据名称遍历进行加载具体路径下的具体的属性文件名
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
  1. getSearchLocations()获取属性文件搜索路径,如果环境变量中包括了 spring.config.location 则使用环境变量中配置的值,如果没有则使用默认的 file:./config/,file:./,classpath:/config/,classpath:/文件路径。代码如下,
// 获取搜索路径
private Set<String> getSearchLocations() {
// 如果环境变量中包括了 spring.config.location 则使用 环境变量配置的值。
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
return getSearchLocations(CONFIG_LOCATION_PROPERTY);
}
// 获取 环境变量 spring.config.additional-location 的值
Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
// 添加默认的 classpath:/,classpath:/config/,file:./,file:./config/ 搜索文件
// 倒叙排列后 为 file:./config/,file:./,classpath:/config/,classpath:/
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
return locations;
}
  1. getSearchNames()获取属性文件搜索名称,如果环境变量中有设置 spring.config.name 属性,则获取设置的名称,如果没有设置配置文件名称的环境变量则返回名称为 application。代码如下,
private Set<String> getSearchNames() {
// 如果 环境变量中有设置 spring.config.name 属性,则获取设置的 名称
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
return asResolvedSet(property, null);
}
// 如果没有设置环境变量 则返回名称为 application
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}
  1. names.forEach((name) -> load(location, name, profile, filterFactory, consumer))中的load() 的逻辑为:先判断文件名name是否为null,如果为null则通过遍历属性资源加载器并且根据location进行加载属性资源文件;如果不为null ,则通过遍历属性资源加载器和遍历属性资源加载器的扩展名,根据location和 name 来加载属性资源文件,从配置文件可知,先会遍历执行 PropertiesPropertySourceLoader 的扩展名 ,然后遍历执行YamlPropertySourceLoader的扩展名 。代码如下,
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
// 如果文件名称为null
if (!StringUtils.hasText(name)) {
// 遍历属性资源加载器
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// 根据属性资源加载的扩展名称进行过滤
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
throw new IllegalStateException("File extension of config file location '" + location
+ "' is not known to any PropertySourceLoader. If the location is meant to reference "
+ "a directory, it must end in '/'");
}
Set<String> processed = new HashSet<>();
// 遍历属性资源加载器
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// 遍历属性资源加载器的扩展名
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
// 传入具体的属性文件路径和后缀名,进行加载属性资源文件
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
  • PropertiesPropertySourceLoader的扩展名包括:"properties", "xml" ;
  • YamlPropertySourceLoader的扩展名包括:"yml", "yaml" 。
  1. loadForFileExtension()关键代码是load()方法,代码如下。
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// Try profile-specific file & profile section in profile file (gh-340)
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// Try profile specific sections in files we've already processed
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// Also try the profile-specific section (if any) of the normal file
// 拼接文件路径和后缀名后,进行加载属性资源文件
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
  1. 如上代码的load()方法主要逻辑为:先根据传入的文件路径生成 Resource 对象,如果该Resource 对象存在则解析成具体的documents对象,然后根据DocumentFilter 过滤器进行匹配,匹配成功则添加到 loaded 中,再进行倒叙排列。最后遍历 loaded 对象,调用consumer.accept ,将 profile 和 document 添加至 this.loaded 对象。
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
// 根据文件路径 获取资源
Resource resource = this.resourceLoader.getResource(location);
// 如果为 null 则返回
if (resource == null || !resource.exists()) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped missing config ", location, resource,
profile);
this.logger.trace(description);
}
return;
}
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;
}
String name = "applicationConfig: [" + location + "]";
// 根据资源 和 属性资源解析器加载 List<Document> ,并进行缓存
List<Document> documents = loadDocuments(loader, name, resource);
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<>();
// 遍历 documents
for (Document document : documents) {
// 如果匹配则添加
if (filter.match(document)) {
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
// 倒叙排列
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
//遍历 loaded 对象,调用consumer.accept ,将 profile 和 document 添加至 this.loaded 对象
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);
}
}
  1. 至此配置文件解析全部处理完成,最终会把解析出来的配置文件和配置属性值添加到了 this.loaded 对象中。
  2. 总结一下,默认情况下,属性配置文件的搜索路径为 file:./config/,file:./,classpath:/config/,classpath:/ ,优先级从左往右;配置文件名称为 application,扩展名为"properties", "xml","yml", "yaml",优先级从左往右。如果同一个配置属性配置在多个配置文件中,则取优先级最高的那个配置值。

环境变量设置配置属性

  1. addLoadedPropertySources()方法,主要逻辑为:添加已经加载的属性文件添加至环境变量 this.environment 中 。代码如下,
private void addLoadedPropertySources() {
// 获取环境变量的 PropertySources对象
MutablePropertySources destination = this.environment.getPropertySources();
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
// 倒序排列
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (added.add(source.getName())) {
// 添加 PropertySource 至 destination
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
  1. applyActiveProfiles()主要逻辑为:根据已处理的属性文件设置环境变量environment的ActiveProfiles属性。
// 设置环境变量的ActiveProfiles
private void applyActiveProfiles(PropertySource<?> defaultProperties) {
List<String> activeProfiles = new ArrayList<>();
if (defaultProperties != null) {
Binder binder = new Binder(ConfigurationPropertySources.from(defaultProperties),
new PropertySourcesPlaceholdersResolver(this.environment));
activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.include"));
if (!this.activatedProfiles) {
activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.active"));
}
}
this.processedProfiles.stream().filter(this::isDefaultProfile).map(Profile::getName)
.forEach(activeProfiles::add);
this.environment.setActiveProfiles(activeProfiles.toArray(new String[0]));
}

SpringBoot配置文件读取过程分析的更多相关文章

  1. 【日常错误】spring-boot配置文件读取不到

    最近在用spring-boot做项目时,遇到自定义的配置文件无法读取到的问题,通过在appcation.java类上定义@PropertySource(value = {"classpath ...

  2. springboot配置文件读取pom文件信息

    解决的问题 springboot(当然别的也可以)多环境切换需要该配置文件,打包时不够方便. 解决: 配置文件能读取pom文件中的配置,根据命令选择不同配置注入springboot的配置文件中 pom ...

  3. 【Java】SpringBoot配置文件读取中文乱码

    [问题]在配置文件application.properties中配置一个值含有中文的变量.spring加载配置之后,读取的变量中文部分出现乱码.根据CSDN说的一堆办法,改encoding为UTF-8 ...

  4. SpringBoot 配置文件存放位置及读取顺序

    SpringBoot配置文件可以使用yml格式和properties格式 分别的默认命名为:application.yml.application.properties 存放目录 SpringBoot ...

  5. 关于springboot配置文件的另类读取方法

    一.背景故事   前阵子我接手了公司另外一个同事手里的项目,项目是用的springboot 写的,但是比较坑的就是这个项目写的有点不伦不类.虽然是用的springboot,但由于他是拿了一堆代码拼凑起 ...

  6. SpringBoot配置文件 application.properties详解

    SpringBoot配置文件 application.properties详解   本文转载:https://www.cnblogs.com/louby/p/8565027.html 阅读过程中若发现 ...

  7. SpringBoot学习笔记(八):SpringBoot启动端口+访问路、SpringBoot配置文件yml、SpringBoot多环境区分、SpringBoot打包发布

    SpringBoot启动端口+访问路径 配置文件: server.port=9090 server.context-path=/springboot 现在只能用http://127.0.0.1:909 ...

  8. Springboot配置文件参数使用docker-compose实现动态配置

    文章总结; Springboot配置文件中的一些参数可以写成变量的形式,具体变量的值可以从docker-compose.yml文件中设置来获取 在yml文件中,通过${Envirment_variab ...

  9. [spring源码学习]二、IOC源码——配置文件读取

    一.环境准备 对于学习源码来讲,拿到一大堆的代码,脑袋里肯定是嗡嗡的,所以从代码实例进行跟踪调试未尝不是一种好的办法,此处,我们准备了一个小例子: package com.zjl; public cl ...

随机推荐

  1. 编写引入svg

    SVG是一种XML语言,类似XHTML,可以用来绘制矢量图形,例如右面展示的图形.SVG可以通过定义必要的线和形状来创建一个图形,也可以修改已有的位图,或者将这两种方式结合起来创建图形.图形和其组成部 ...

  2. 关于Mysql索引的数据结构

    索引的数据结构 1.为什么使用索引 概念: 索引是存储索引用于快速找到数据记录的一种数据结构,就好比一本书的目录部分,通过目录中对应的文章的页码,便可以快速定位到需要的文章,Mysql 中也是一样的道 ...

  3. 2003031121——浦娟——Python数据分析第七周作业——MySQL的安装及使用

    项目 要求 课程班级博客链接 20级数据班(本) 作业要求链接 Python第七周作业 博客名称 2003031121--浦娟--Python数据分析第七周作业--MySQL的安装及使用 要求 每道题 ...

  4. tuandui last

    组长博客链接### 组长博客 参考邹欣老师的问题模板进行总结思考### 设想和目标(2分)#### 1.我们的软件要解决什么问题?是否定义得很清楚?是否对典型用户和典型场景有清晰的描述? 解决的问题 ...

  5. 超全!华为交换机端口vlan详解~

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 华为交换机和其他品牌的交换机在端口的vlan划分上有一些区别,今天就和大家详细说说华为交换机的 ...

  6. 【原创】记一次对X呼APP的渗透测试

    获取CMS并本地安装 X呼是一款开源的客服CMS系统,访问官网,下载安卓版本的app和源码本地搭建: 发现这cms预留admin表中的用户就不少.... 直接用预留的密码解密,然后就能登录手机APP了 ...

  7. 解读先电2.4版 iaas-install-mysql.sh 脚本

    #!/bin/bash #声明解释器路径 source /etc/xiandian/openrc.sh #生效环境变量 ping $HOST_IP -c 4 >> /dev/null 2& ...

  8. 【Rust】使用HashMap解决官方文档中的闭包限制

    问题概述 值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们.然而,目前 Cacher 的实现存在两个小问题,这使得在不同上下文中复用变得很困难. 第一个问题是 Cacher  ...

  9. DirectX11--CPU与GPU计时器

    前言 GAMES104的王希说过: 游戏引擎的世界里,它的核心是靠Tick()函数把这个世界驱动起来. 本来单是一个CPU的计时器是不至于为其写一篇博客的,但把GPU计时器功能加上后就不一样了.在这一 ...

  10. 数仓选型必列入考虑的OLAP列式数据库ClickHouse(上)

    概述 定义 ClickHouse官网地址 https://clickhouse.com/ 最新版本22.4.5.9 ClickHouse官网文档地址 https://clickhouse.com/do ...