type
status
date
slug
summary
tags
category
icon
password
AI summary

背景

我们的配置中心一直是自研的,并且也用了很久,一直也没出现过什么问题。直到最近这段时间,出了两个同类型的问题,于是乎准备来研究并解决一下。
问题原因都是因为引入了某个新包,并且这个新包的classpath根路径下包含了1个application.properties文件,这个时候Spring会加载这个application.properties配置文件。如果该配置文件里有配置中心里没有配置的key-value,那么该配置就会生效(如果配置中心有,那么还是配置中心的优先级更高)。

问题分析

问题的原因很简单。我们知道如果SpringBoot不接入外部配置中心,那么默认就是加载classpath下的application.properties文件。而我们自研的配置中心的client包的实现其实和默认的ConfigFileApplicationListener一样,都是利用了EnvironmentPostProcessor这个扩展点,不过我们的Bean加载的优先级会比默认的更高一些。
所以产生上述问题的原因就是:
  1. 首先我们自定义的实现类优先加载了配置中心的配置文件
  1. 我们的项目的classpath根路径下没有放置application.properties
  1. SpringBoot默认的 ConfigFileApplicationListener加载了classpath下的application.properties,这个时候就可能会加载到其他jar包里的位于classpath下的的application.properties。当然由于jar包加载顺序的不确定性,也没有办法确定具体是加载到哪个application.properties文件
大概原因已经了解了。下面我们再来看看源代码,看看如何解决这个问题。比如看看ConfigFileApplicationListener能不能禁用掉?

SpringBoot配置文件的加载流程——ConfigFileApplicationListener源码分析

核心都在这个Loader.load()方法里,这个类比较长,所以相关的解释我就以注释的形式直接加在对应代码附近,这样阅读起来体验会比较好一些。这里对load方法做一个简单的描述,整个load方法其实只有一行代码:FilteredPropertySource.apply,这个方法接受4个参数:
  1. enviroment
  1. propertySourceName(固定为"default.properties")
  1. filteredProperties(固定为Set["spring.profiles.active", "spring.profiles.include"]),
  1. Consumer回调
这个apply方法的大致逻辑为:
  1. 首先会从enviroment里去找名为default.properties的PropertySource
    1. 如果没找到,会直接调用回调函数,且入参为null。
    2. 如果找到了,也会调用回调函数,但是入参为找到的PropertySource。但是回调前后会替换enviroment里对应的PropertySource,调用前会过滤掉Set,调用后又会复原(TODO 这一步暂时没有理解具体是什么用意,有了解的朋友可以指教一下)
这里看得不是太明白也没关系,核心还是在Loader类里,结合上面的Consumer回调函数一起看下面的这些代码:
上面这段的逻辑确实有些复杂,主要是因为支持了在文件里配置spring.profiles这种骚操作,不然的话代码逻辑会简单清晰很多。我们再把上面的流程提炼一下:
  1. 构建profiles列表,来源有几个:
    1. 固定的一个null元素,是为了加载不带profile的配置文件,如application.properties
    2. spring.profiles.active
    3. spring.profiles.include
    4. Spring容器里通过其他方式配置的Profile,比如通过SpringApplicationBuilder来配置profiles
  1. 根据1中构建的profiles里的每一个profile,都会应用以下的加载逻辑
    1. 去searchLocations为目录去加载配置文件,可以通过spring.config.additional-location或者spring.config.location来指定
    2. 可以通过spring.config.name来指定基础文件名
    3. 会根据propertySourceLoaders结合基础文件名来加载不同扩展名的配置文件(如xml、properties)
    4. 每一个组合都包含了4次加载动作
      1. 加载文件名带profile但是配置文件里没有配置spring.profiles的配置文件
      2. 加载文件名带profile,配置文件里有配置spring.profiles并且配置的spring.profiles里包含了本profile的配置文件
      3. 处理之前已经加载过的profile对应的配置文件(加载过profile不代表对应的配置文件被加载进来了,因为还有过滤规则),这个时候如果配置文件里配置的spring.profiles包含了本次的profile,那也会加载该配置文件
      4. 加载文件名不带profile,这里分两种情况
        1. profile为null时,此时配置文件里没有配置spring.profiles才会加载
        2. profile不为null时,此时配置文件里必须配置了spring.profiles并且spring.profiles包含profile时才会加载 上述4种情况都还有一个共同的前提是该profile被active了
  1. 再一次去searchLocations下加载配置文件,和第2步差不多,但是此时的profile恒为null
    1. 去searchLocations为目录去加载配置文件,可以通过spring.config.additional-location或者spring.config.location来指定
    2. 可以通过spring.config.name来指定基础文件名
    3. 会根据propertySourceLoaders结合基础文件名来加载不同扩展名的配置文件(如xml、properties)
    4. 由于profile恒为null,所以只会加载文件名不带profile,但是配置文件里配置了spring.profiles,且配置的spring.profiles不为空的配置文件
  1. 再一次性地把整个加载到的PropertySources添加到环境变量里
  1. 根据processedProfiles(过滤掉profile为null的情况),重新设置一下activeProfiles
可以看到虽然我已经尽量用精炼的语言总结了,但是整个流程还是相对比较复杂,并且逻辑也不是非常清晰,包括对这部分代码的设计和想法感觉还是不能完全理解到位。希望对这块有更深入理解的同学可以在评论区提点一二。
另外,关于spring.profiles这种配置的用法我着实没有见过,于是也去请教了一下chatgpt
notion image

自研配置中心client包的代码分析

下面这一段是我们自研配置中心client包加载完远程配置文件之后注入到环境变量的源代码
可以看到这里的实现也有一些问题,因为这里把配置中心的配置放在了第一位(First)。这样一来,配置中心的配置就是第一优先级了,甚至超过了CommandLine参数。所以这里也需要调整一下,也算是这次review代码的时候看出来的一个问题。这里参考ConfigFileApplicationListener的实现调整一下即可:
notion image

优化方案

其实上面讲了半天,主要是为了全面的了解SpringBoot加载配置文件的整个流程。如果你对前文的内容已经消化得差不多了,比较容易能想到一些优化方案:
  1. 修改spring.config.name的配置值,替换成一个不可能用到的名字,如never_exist_999_xxx
  1. 修改spring.config.location的配置值,替换成一个不可能用到的目录,如never_exist_999_xxx
并且这个还不能写死,因为我们既有使用配置中心的项目,也有使用SpringBoot原生配置文件的项目。所以我们把这一段逻辑放到配置中心client包里:
notion image

附录

ClassLoader.getResource

这一段是javadoc告诉我们,通过ClassLoader.getResource
  1. 如果指定name的资源在多个jar包中存在,只能返回其中一个
  1. 具体返回哪一个是根据jar包加载的顺序决定的,但是这个顺序是不可预测的
ConfigFileApplicationListener加载配置文件的时候底层调用的就是这个方法
notion image

defaultProperties

找了半天我说defaultProperties在哪呢
org.springframework.boot.SpringApplication#configurePropertySources
notion image
Mac使用docker,volume默认挂载路径/var/lib/docker/volumes不存在问题——多种解决方案刷一张亿级表带来的思考
Loading...
黑微狗
黑微狗
一只普通的干饭汪🍚
Latest posts
RocketMQ 4.6.0 Message Trace 功能异常排查
2025-4-8
browser-use 项目核心原理
2025-3-28
关于怎么搭建一个这样的blog
2025-3-28
关于怎么给blog搞一个自定义的域名
2025-3-28
Excel导入需求升级——支持内嵌图片导入
2025-3-28
mysql流式查询中的一个坑
2025-3-28