1. 项目启动时的环境切换难题:从一份配置拆成多套说起
先从一个我再熟悉不过的场景讲起。刚接触 Spring 项目时,所有人的配置都堆在同一个application.properties里:数据库地址写的是本机的localhost:3306,缓存连接指向开发服务器,第三方接口的密钥也是开发环境那一套。等到要部署测试环境或者生产环境时,就把配置里的地址和口令逐行替换,改完再启动,运气好一遍过,运气不好漏掉某一个配置项,项目能跑但功能全错,排查起来相当浪费时间。
后来接触到spring.profiles.active,才意识到这种"改配置再重启"的做法从一开始就是错的。Spring 的 profile 机制,本质上就是为同一套代码维护多份运行环境配置的手段:开发、测试、预发、生产各自维护独立的配置文件,启动时通过激活对应的 profile 来决定加载哪份配置。它的设计目标,是让"环境切换"这件事变成一行参数的事,而不是十几处手改的事。
不过,真正上手之后你会发现,spring.profiles.active只是解决了"选哪组配置"的问题,而spring.profiles.include解决的却是"除了当前选的这组,还要把另外几组配置一并带上"的问题。两者在配置文件中只差一个单词,但行为差异很大,用错的情况我也见过不少。尤其在一些多模块、多环境的项目里,如果搞不清楚这两个属性的加载顺序和组合逻辑,配置越写越乱,最后谁也不敢动配置文件。
所以这篇内容我不想只讲 API 层面的用法,更想把这几年实际项目中踩过的坑、总结出的配置组织方式一起聊清楚,让有同样困扰的人少走点弯路。
1.1 配置文件的分层直觉
先建立一个直观认知。可以把 profile 配置理解成"基础材料 + 场景配料"的关系。基础的application.yml放的是所有环境里都不会变的公共配置,比如服务名、端口通用的部分、框架版本相关的参数。而每个独立的 profile 配置文件,比如application-dev.yml、application-prod.yml,放的是某个环境特有的内容。
此时spring.profiles.active就是"今天我要做哪个版本的菜",它负责决定最终激活哪一份配料。spring.profiles.include则是"除了主配料,我还要额外加哪些辅料",它在不改变主激活环境的前提下,让其它 profile 的配置也一起生效。
理解到这一层,后面看两个属性的具体行为就容易多了。
2.spring.profiles.active的使用姿势与生效逻辑
spring.profiles.active的作用一句话可以讲清楚:设置当前激活的 profile 名称,Spring Boot 启动时会额外加载对应的application-{profile}.yml文件。多个 profile 用逗号分隔,例如spring.profiles.active=dev,local。
但实际使用中,它的细节比表面上一行配置要多得多。
2.1 最基础的用法:逗号分隔多个环境
假设项目里有这样的文件结构:
src/main/resources/ ├── application.yml ├── application-dev.yml ├── application-test.yml └── application-prod.yml在application.yml中写:
spring: profiles: active: dev启动后 Spring Boot 加载application.yml作为主配置,再加载application-dev.yml作为 profile 专项配置。两者同时生效,且专项配置中的属性值会覆盖主配置中同名属性值。
如果你想同时激活多个 profile,比如既要开发环境,又要本地专用的一些 mock 配置,可以写:
spring: profiles: active: dev,local注意逗号分隔的多个 profile 之间是平级关系,它们在配置加载阶段都会被激活,没有"主次"之分。但如果两个 profile 专项文件里定义了同一个属性,后加载的会覆盖先加载的。从这个角度说,多个 profile 同时激活时,文件加载顺序值得单独确认,后面我会专门讲这一块。
2.2 外部覆盖的优先级:命令行、环境变量、Java 系统属性
这是spring.profiles.active最有价值的特点之一:写在application.yml里的激活值,并不是最终值,它是可以被外部更高优先级的配置来源覆盖的。
命令行参数优先级最高,启动时这样指定:
java -jar app.jar --spring.profiles.active=prod也可以通过环境变量指定:
export SPRING_PROFILES_ACTIVE=prod java -jar app.jar还可以用 Java 系统属性:
java -Dspring.profiles.active=prod -jar app.jar这个能力的实际意义在于:项目构建产物是同一个 jar 包,开发、测试、生产环境不需要重新打包,只需要在启动命令里指定不同的 profile 就行。这也是很多部署脚本最常用的一行参数。
我把几个常见配置来源按优先级排一下,方便参考:
| 配置来源 | 优先级 | 常见写法 |
|---|---|---|
| 命令行参数 | 最高 | --spring.profiles.active=prod |
| Java 系统属性 | 高 | -Dspring.profiles.active=prod |
| 环境变量 | 中 | SPRING_PROFILES_ACTIVE=prod |
application.yml内部 | 低 | spring.profiles.active: dev |
一个容易被忽略的点是:正因为 active 可以被外部覆盖,很多部署平台(无论是容器编排还是云平台)的教程都会强调用环境变量来动态切换环境,但这同时也意味着,如果有人在服务器上设置了SPRING_PROFILES_ACTIVE环境变量而没有及时清理,你写在application.yml里的 active 就会一律失效。这种环境变量"残留"问题,我在团队里排查过不止一次。
2.3 active 写在配置文件中要注意的两个关键点
第一,spring.profiles.active不应该出现在 profile 专项配置文件里。什么意思?就是不要写在application-dev.yml里再写spring.profiles.active: test,这种写法在新的 Spring Boot 版本里要么无效,要么报错。因为 profile 专项文件本身就是由 active 或 include 触发而加载的,你在这个文件里又想去激活别的 profile,就变成无限套娃式的逻辑。正确做法是:激活动作只放在主配置文件或外部参数中。
第二,spring.profiles.active写多个值时的覆盖行为要小心。如果命令行传的是--spring.profiles.active=prod,配置文件里写的是dev,最终生效的是命令行传的prod,两者不会合并。也就是说,外部传参采用的是"整体覆盖"而不是"追加"。如果你本来期望外部传参只是在原有基础上多激活一个环境,这种思路行不通,需要配合 include 来实现。
3.spring.profiles.include:主动"拉入"其它配置组的叠加能力
如果说 active 是"选主菜",include 就是"加辅料"。spring.profiles.include的作用,是在当前配置生效的同时,把其它指定的 profile 一并加载进来,而且不影响当前已激活的 profile 集合。
它最典型的应用场景是:某些配置是多个环境共用的基础能力,比如监控上报参数、统一的日志采集配置、Redis 缓存底座的通用连接参数。这些内容单独抽成一个 profile 文件,比如application-common.yml,然后通过 include 引入。
3.1 include 的语法与典型场景
在application.yml中:
spring: profiles: include: common,monitor profiles: active: dev启动时,如果 active 指定的是 dev,那么最终激活的 profile 集合是:dev, common, monitor。集合并集里都生效了,但外部通过--spring.profiles.active=prod覆盖时,dev会被替换成prod,而 include 引入的common, monitor仍然保留。
这个"外部覆盖 active 不影响 include"的特点非常实用。比如同一个应用在生产环境跑的时候,不管环境怎么切换,监控上报和统一日志这两块基础配置始终都要带着,你就不需要每次启动都手动加参数了,把它们写死在 include 里更稳妥。
实际项目中我见过两种组织方式。一种是把公共能力拆成独立 profile 文件,用 include 引入作为默认组合:
spring: profiles: include: common,logging,monitor另一种是在 profile 专项文件里动态 include。比如application-cloud.yml里写:
spring: profiles: include: registry,mysql-cloud这样当别人主动激活cloud这个 profile 时,registry和mysql-cloud会被连带激活,而这两个 profile 本身不用出现在主配置的 activate 里。这种写法在区分部署形态(本地、服务器、云环境)时尤其方便。
3.2 include 在 YAML 多文档中的行为
如果你用过 Spring Boot 2.4 之后版本的 YAML 多文档功能,应该见过这种写法:
spring: config: activate: on-profile: dev --- spring: config: activate: on-profile: prod在这种结构下,spring.profiles.include放在哪个文档里,行为很不一样。它如果放在默认的第一个文档(没有 on-profile 条件)中,就是全局的,任何环境都会引入它;如果放在某个on-profile: xxx的文档里,只有当对应的 profile 被激活时,这个 include 才会生效。
我建议在 YAML 多文档结构下,把 include 尽量放在默认文档的顶部,让它保持全局语义。否则后续维护的人看到某个环境莫名其妙多加载了配置,要翻到文档中间才能发现原因,排查成本很高。
另一点值得说的是,include 虽然可以写在命令行参数里(--spring.profiles.include=xxx),但它和配置文件里的 include 是覆盖关系还是叠加关系,取决于属性来源的优先级规则。由于这个行为在不同版本之间并不稳定,我在实际团队里定过一个规矩:include 只放在配置文件里维护,命令行不传 include 参数,避免不确定的行为影响环境一致性。
3.3 include 与 group:什么时候该用哪个
Spring Boot 2.4 之后还引入了spring.profiles.group,它在一定程度上比 include 更贴近"组合激活"的场景。group 的语义是:激活某个 profile 时,自动一次性激活一整组其它 profile。典型写法:
spring: profiles: group: dev: dev-db,dev-mq,dev-cache prod: prod-db,prod-mq,prod-cache这样做的好处是,启动时只需要--spring.profiles.active=dev,group 会自动帮你把dev-db、dev-mq、dev-cache全部激活。相比 include,group 更强调"按主环境分组",而 include 更强调"跨环境共用的基础能力"。
我的实践体会是:group 适合把同一个环境下的子模块配置归组,include 适合把多个环境都需要的公共配置统一引入。两个配合使用时,尽量让职责清晰:include 管基础能力,group 管环境分组,不要混着用。如果都往里面堆,最后 profiles 集合完全不可预期。
4. active 和 include 的核心区别:激活 vs 叠加
很多文章会直接把两者的区别归结为"active 设置当前激活的环境,include 追加额外的环境",这句话对,但不够深。真正理解它们,要从两个维度看:一个是"它影响的是什么集合",另一个是"它在什么时候生效、可不可以被外部覆盖"。
4.1 两行配置,行为差异很大
先看一张对比表:
| 对比维度 | spring.profiles.active | spring.profiles.include |
|---|---|---|
| 核心语义 | 设置当前主激活 profile | 追加加载其它 profile |
| 是否影响最终激活集合的主标识 | 是,它就是主标识 | 否,只是在集合中加入其它项 |
| 典型位置 | 主配置文件 或 外部参数 | 主配置文件 或 profile 专项文件 |
| 外部覆盖行为 | 高优先级参数会整体覆盖配置值 | 通常由配置文件维护,外部传参有覆盖风险 |
| 加载顺序 | 先被读取,决定后续加载哪些 profile-specific 文件 | 与 active 一起参与拼接,确定最终加载集合 |
| 典型使用场景 | 环境切换:dev/test/prod | 公共能力引入:logging/monitor/common |
| 能否在 profile 专项文件中声明 | 不推荐,新版本基本无效 | 可以,但仅在该 profile 激活时生效 |
这张表里最值得反复琢磨的是第一行和第五行。active 是"因",include 是"果"的一部分。Spring Boot 启动时,先扫描所有配置来源,确定 active 的最终值(外部参数优先),然后才是进入配置加载阶段,把 active 指定的 profile 专项文件和 include 指定的 profile 专项文件一起纳入加载列表。
4.2 叠加顺序与覆盖优先级
当多个 profile 同时激活时,一个绕不开的问题是:同名属性谁覆盖谁。
官方文档给出的加载优先级里,profile 专项配置文件的优先级高于无 profile 的主配置文件。也就是说,无论多少个 profile 被激活,它们的优先级都高于application.yml。但多个 profile 文件之间的相对顺序,则取决于它们进入加载列表的顺序。
这个顺序规则大致是:spring.profiles.active中先列出的 profile 先加载,之后是spring.profiles.include引入的 profile。后加载的 profile 文件覆盖先加载的。举个实际例子:
spring: profiles: active: dev include: common启动后加载顺序是:application.yml->application-dev.yml->application-common.yml。最终属性值以common中的为准。所以如果dev和common里都定义了同一个第三方接口地址,common里会赢。
这个顺序经常被搞反。很多人在dev里精心调好的参数,测试时发现不生效,排查到最后才发现是common里的同名配置在后面覆盖了。我的建议是:尽量不要让不同的 profile 文件定义同名属性,如果实在避免不了,必须清楚加载顺序,把"最终生效值"的可预期性放在第一位。
4.3 容易混淆的加载过程拆解
用一个场景完整走一遍加载过程。假设配置如下:
application.yml中:
server: port: 8080 spring: profiles: active: dev include: commonapplication-dev.yml中:
server: port: 8081 logging: level: root: debugapplication-common.yml中:
app: timezone: UTC+8启动命令为:
java -jar app.jar --spring.profiles.active=prod最终行为是这样的:
- bootstrap 阶段,Spring Boot 收集配置来源。命令行中指定的
--spring.profiles.active=prod优先级高于application.yml中的dev,所以最终主激活变成prod。 - 由于 include 没有外部参数覆盖,
common依旧保留,最终的激活集合是prod, common。 - 加载配置时,除了
application.yml,还会找application-prod.yml和application-common.yml。application-dev.yml不会加载。 - 如果
application-prod.yml不存在,不会报错,只是没有对应专项配置。这一点经常让新手困惑——active 指定了一个不存在的 profile,启动照样能成功,除非该 profile 里有必填配置,否则只能到运行时才发现问题。
这个拆解里,最值得记住的是第二步:active 可以被外部整体覆盖,include 是配置内部声明的追加项,两者不是同一个维度的东西。理解了这个,很多配置异常都能快速定位。
5. 实战组合:一套可靠的 profile 组织方案与踩坑记录
理论讲完,说点落地经验。我所在的团队以前维护的配置是这样组织的,用了之后环境切换和公共配置管理都清爽了很多,分享出来供参考。
5.1 推荐的分层结构
结合 active 和 include 的特点,我把配置拆成三层:
第一层是主配置application.yml,只放所有环境都一致的项,外加两个关键声明:include 引入公共能力 profiles,group 定义环境分组。
spring: application: name: demo-service profiles: include: common,logging group: dev: dev-db,dev-cache prod: prod-db,prod-cache第二层是公共能力 profile 文件,按功能拆分:application-common.yml放公共业务参数,application-logging.yml放日志级别和格式。这样可复用的配置不会散落在每个环境里重复维护。
第三层是环境相关 profile 文件:application-dev-db.yml、application-prod-db.yml分别管理各自的数据库地址;application-dev.yml、application-prod.yml则作为环境入口,可以再声明一些环境级的覆盖参数。
运行开发环境时启动命令只需要:
java -jar app.jar --spring.profiles.active=dev因为 group 的定义,dev-db、dev-cache会跟着激活;因为 include 的定义,common、logging也会跟着激活。最终配置来源清晰,每个文件职责单一,不会再出现"一个配置改动影响所有环境"的尴尬局面。
5.2 我踩过的几个坑
这个组织方式不是一开始就这样的,中间踩过不少坑,挑几个典型说说。
第一个坑是 include 引入的 profile 文件里又写了spring.profiles.include。在项目快速迭代期,有人在application-common.yml里又 include 了common-ext,看起来逻辑没问题,但一旦某个文件的 include 链路过深,排查配置来源就变成一场灾难。更麻烦的是,如果两个 profile 互相 include(A include B,B include A),Spring 启动会直接报循环引用错误,排查起来非常绕。后来我们定了一条铁律:include 只在主配置文件中写,profile 专项文件里一律不写 include。
第二个坑是 group 与 include 同时使用时的加载顺序问题。之前有个同事把 group 中的子 profile 又写进了 include,结果同一个 profile 在同一份配置里被加载了两次,因为顺序不同,同名属性覆盖结果变得不确定。从那以后,我们约束:公共能力进 include,环境子项进 group,两者集合不允许有交集。
第三个坑是环境变量覆盖。有一次测试环境一直加载到了生产配置,查了半天才发现是环境里残留了SPRING_PROFILES_ACTIVE=prod的环境变量,导致application.yml里的 dev 全部失效。部署脚本里如果曾经设置过环境变量,后面项目切换了环境或调整了命名,旧变量特别容易残留下来。现在的处理方式是:部署脚本启动前显式打印当前生效的 profile 值,每次发布时对比确认。
5.3 小团队落地建议
最后给一些执行层面的建议。如果你所在的团队不大,配置的管理完全不需要复杂,记住三条原则就够了。
第一,spring.profiles.active的最终值应该尽量只由启动命令或环境变量控制,不要依赖写在配置文件里的那一份。因为只有这样才能保证同一个包在不同环境的表现是完全可控的。配置文件里的 active 就当默认值对待,别当成"唯一指定值"。
第二,spring.profiles.include一定要控制使用范围。它最适合的是"凡是这个服务启动都必须带上的基础能力",而不是"某个环境特有的配置"。凡是环境特异的,交给 active 或 group。
第三,每个 profile 的名字要有明确约定。我看到过dev、dev1、dev-new、local-dev这类名字在同一个项目里共存的情况,最后配置文件数量爆炸,谁都不敢删。profile 名称应该是一份配置仓库的公共契约,改名要慎重,新增要评审。
配置管理这件事,看着只是几行 YAML,但真正复杂的从来不是语法,而是多人协作下每个属性值的来源能不能被快速追溯。合理利用 active 和 include 各自的特性,让每个配置文件的职责边界清晰起来,后续维护的幸福感会提升很多。
这两年在不少项目里看到过因为 profiles 使用混乱导致的线上配置错乱问题,大多不是技术上的疑难杂症,而是对两个属性边界理解不透,又在配置文件里层层叠加。回到开头的问题:spring.profiles.active和spring.profiles.include的区别,本质上就是一句话——前者决定"用哪组环境配置",后者决定"在任何环境下都要把哪些配置带上"。把这句话理解到位了,具体环境怎么组织,遇到已知的坑怎么规避,就会有一个非常清晰的判断基准,不需要再去查文档记各种边界情况了。