再见 bootstrap.yml,spring.config.import 时代的配置管理
再见 bootstrap.yml,spring.config.import 时代的配置管理
dong4jbootstrap.yml 为什么不在了
我在第五篇文章里写了 @Value 到 @ConfigurationProperties 的迁移。那是配置管理的”内容”层面——配置怎么写、怎么校验、怎么分类。这篇文章说的是配置管理的”机制”层面——配置文件本身是怎么被加载的,以及从一个机制迁移到另一个机制时要面对的坑。
先说背景:Spring Cloud 2020+(对应 Spring Boot 2.4+)废弃了 bootstrap.yml。
很多人可能都没注意到这件事。因为如果你的项目是那之前创建的,你的 bootstrap.yml 还在正常工作——Spring Cloud 还保留着向后兼容的能力。但如果你从 Spring Boot 2.3 跳到 3.x,或者像我一样在做架构迁移,就会发现 bootstrap.yml 被官方标记为”不推荐使用”。
为什么要废弃?官方的理由有三条:
额外的上下文。 bootstrap.yml 和 application.yml 是两个独立的 ApplicationContext。bootstrap.yml 先加载,形成一个”引导上下文”(Bootstrap Context),然后 application.yml 再加载,形成”主上下文”(Application Context)。这种双上下文的机制让排查问题变得困难——你不知道一个属性是在哪个上下文里被解析的。
维护成本高。 多了一套配置加载逻辑,Spring Cloud 团队的维护负担就多了一倍。而且 bootstrap.yml 的行为和 application.yml 并不完全一致——有些属性在 bootstrap 上下文里不会触发自动配置,因为自动配置只在主上下文中生效。这些差异常常是 bug 的来源。
违背 Spring Boot 的统一配置入口理念。 Spring Boot 的设计哲学是”一套配置入口”,所有外部化配置都从 application.yml 开始。bootstrap.yml 打破了这个约定——它是在 application.yml 之前加载的”配置的配置”。
对我来说,最直观的感受是另一个:在双上下文机制下,Spring 的 Listener 会被执行两次。 因为同一段初始化逻辑在两个上下文里各跑了一次。你能想象一个初始化数据库连接的 Listener 被执行两次是什么效果吗?
所以在新版本的微服务迁移中,我们完全拥抱了 spring.config.import 代替 bootstrap.yml 的方式。
spring.config.import 怎么用
最简洁的写法:
1 | spring: |
两行 import 分别做了什么事:
第一行,导入 share-config.yaml——这是所有微服务共享的公共配置,比如数据库连接池参数、Feign 超时配置、MyBatis 的全局配置。refreshEnabled=false 表示这个配置不需要动态刷新,因为改共享配置影响面太广,应该走正式的变更流程,而不是在 Nacos 后台随手改。
第二行,导入 ${spring.application.name}.yaml——这是当前服务专属的配置。refreshEnabled=true 表示支持动态刷新,因为这个配置只影响当前服务,改动风险可控。
这个设计背后是一个三层配置体系:
1 | 服务专属配置 (xxx-service.yaml) → 优先级最高 |
使用者在自己的服务配置里只需要写自己特有的东西。比如 sctelcp-user-api-service.yaml:
1 | server: |
第一个坑:为什么 application.yml 不能拆分
这是整个配置迁移过程中最大的坑。
看起来合理的做法是把不同环境的 Nacos 连接信息放在不同的 profile 文件中——application-local.yml 放本地 Nacos 地址,application-dev.yml 放开发环境地址。整洁、清晰、符合 Spring Boot 的最佳实践。
但是这样配,Nacos 根本连不上。没有任何报错,它就是静默地不连接。
原因在于 spring.config.import 的执行时机。它的执行顺序是这样的:
- 加载
application.yml的默认部分(非 profile 部分) - 执行
spring.config.import,连接 Nacos 并导入远程配置 - 加载 profile 文件(
application-{profile}.yml)
第 2 步在第 3 步之前。所以如果你把 Nacos 的连接地址放在 application-local.yml 里:
1 | # application.yml |
第 2 步的时候,Spring 不知道 Nacos 在哪,spring.config.import 静默失败。没有任何异常——Spring 认为配置中心连接失败不应该阻止应用启动,所以它默认吞掉了这个错误。然后整个 Nacos 配置都不生效,你的服务用着本地的默认配置跑起来,直到某个需要远程配置的功能被调用时才暴露问题。
排查这种问题极其痛苦,因为日志里没有任何 ERROR。你看着应用正常启动了,但所有的 Nacos 配置都是 null。
正确的做法是:所有 profile 相关的配置放在一个 application.yml 中,用 --- 分隔,通过 spring.config.activate.on-profile 做条件激活。
1 | # application.yml |
这个文件虽然比拆分方案长了点,但它是可预测的。你一眼就知道 local 环境连哪个 Nacos,dev 环境连哪个 Nacos。不会出现”配置明明写了为什么不生效”的情况。
也许有人会问:Spring Boot 不是支持 spring.config.import 与 profile 的配合吗?答案是:支持,但仅限于 application-{profile}.yml 里定义的非 import 属性。import 本身是在 profile 文件加载之前执行的,这是架构设计上的时间差,不是你写配置能绕过去的。
第二个坑:配置优先级变了
以前 bootstrap.yml 时代,配置中心的配置优先级高于本地配置。很多人养成了这样的心智模型:Nacos 上改个值,本地就自动覆盖了。
到了 spring.config.import 时代,规则反过来了——本地配置优先级高于配置中心。
为什么?因为 spring.config.import 导入的配置被视为一种”额外的配置源”,和本地配置在同一个 ConfigData 体系里处理。而 Spring Boot 默认的规则是:本地文件配置优先。
具体来说,加载顺序是:
1 | 1. application.yml 默认部分(非 profile) |
一个具体例子:
1 | # application.yml |
假设 Nacos 上 share.yaml 里配了 aa=222。最终的值是 aa=333。
加载过程:
- 加载默认配置:
aa=111 - import Nacos 配置:
aa=222(此时覆盖了111) - 加载 dev profile:
aa=333(此时覆盖了222)
如果你导入多个 Nacos 配置:
1 | spring: |
share2.yaml 的优先级高于 share1.yaml——后导入的覆盖先导入的。
这个优先级反转对本地开发来说是好事。以前你想在本地覆盖配置中心的某个值,得去 Nacos 后台改,或者在 application.yml 里配 spring.cloud.config.allowOverride=true。现在直接改本地配置就行——本地天然优先。
但这也意味着,你不能像以前那样依赖 Nacos 后台改配置来覆盖本地配置。 如果你希望配置中心优先于本地(比如某些生产环境的治理需求),需要显式声明:
1 | spring: |
这是一个需要在团队规范里写清楚的变化。别让一个开发者在不知道优先级反转的情况下,在 Nacos 上改了个值,以为生效了,结果本地配置一直在覆盖它。
一个调试技巧
如果你想看配置到底是怎么加载的,加一行日志:
1 | logging: |
启动时控制台会打印每个配置源的加载顺序和内容。配错了排查起来一目了然。
share-config.yaml 里该放什么、不该放什么
迁移过程中,我们对 share-config.yaml 做了一次”瘦身手术”。原则是:共享配置只放真正通用的东西,服务特有的配置挪到各自服务的配置中。
具体操作:
- OSS 配置从
share-config.yaml迁移到sctelcp-infrastructure-service.yaml——只有基础设施服务才需要对象存储,不是全局配置 - 删除
spring.cloud.nacos.discovery相关配置——注册中心配置放在本地application.yml,和配置中心放一起,不再分开两套 - 删除 RSA 密钥和 SM2 密钥相关配置——安全敏感配置不应该有默认值,强制业务方自己配
- 新增
spring.datasource默认配置——连接池参数是所有服务通用的,适合放在共享配置中 - 新增
http.client配置模板——HTTP 客户端的超时、重试等配置,服务可按需继承或覆盖
最关键的改动是第三条。在第五篇文章里也提过这个原则——安全敏感信息的默认值是安全的最大敌人。你在共享配置里写了个 SM2 默认密钥,绝大多数业务方不会去改,也不会记得去改,那加密就成了摆设。更糟糕的是,多个项目共用同一个默认密钥,一个项目的密钥泄露就意味着所有项目的加密数据都不安全了。
解决办法很简单——留空,强制业务方在初始化时自己生成:
1 | secret: |
从架构的角度,这不是在使用者制造麻烦,这是在保护他们。但这个措施只有配合培训才有效——你要在文档里解释为什么这一项留空了,以及怎么生成新的密钥对。
附加技能:Maven profiles 切换数据库驱动
迁移过程中还有一个痛点:我们同时适配了 PostgreSQL 和 MySQL,每次切换数据库要手动改 pom.xml 的驱动依赖。而且有时候同时依赖了两个驱动,jar 包体积无谓增大。
用 Maven profiles 解决:
1 | <profiles> |
默认激活 PostgreSQL。需要切换到 MySQL 时,在 IDE 的 Maven 面板里勾选 mysql profile 即可,或者命令行 mvn -Pmysql。
配合一个技巧:不要手动配置 driver-class-name。MyBatis-Plus 能根据 JDBC URL 自动推断驱动类——jdbc:postgresql:// 推断出 org.postgresql.Driver,jdbc:mysql:// 推断出 com.mysql.cj.jdbc.Driver。删掉显式配置之后,切换数据库不需要改任何配置文件。
1 | spring: |
下一篇写什么
这篇是配置管理在架构迁移层面的内容。下一篇是整个迁移过程中最出乎我意料的一个发现——验证码组件的安全漏洞。不是代码写错了,而是设计逻辑本身有安全隐患:重放攻击、用户名枚举、DoS 攻击,以及一个因为 token 获取错误导致缓存机制完全失效的隐蔽 bug。这些漏洞没有一个是通过自动化测试发现的——全是我手动构造请求探测出来的。

















