1. 为什么同时需要两个属性:一次生产事故引出的问题
先说个真事。之前团队里有个支付回调服务,application.yml里本来写的是:
spring: profiles: active: dev include: common开发阶段一切正常,成员各自在自己的分支上跑,dev这个 profile 负责把本地 H2 数据库、调试日志开关、宽松的 CORS 配置都拉起来。后来服务要上线,运维同事按流程在启动脚本里加了--spring.profiles.active=prod,想让它直接切到生产配置。结果启动倒是很顺利,但日志里出现了dev环境独有的打印格式,消息网关却连到了测试集群,一时之间谁都不敢让这个实例接真实流量。
当时大家围着日志吵了半天,最后把问题定位在spring.profiles.include上:它把common这个固定 profile 带进来了,而common里又引了一整套开发环境的公共依赖。spring.profiles.active被外部命令覆盖成了prod,但include的生效逻辑根本不受这个影响,照样把自己声明的那份配置塞进了激活列表。
这起事故可以浓缩成一个非常实际的问题:active 和 include 都能让某个 profile 生效,但它们之间到底是什么关系,什么时候该用哪个?这篇文章我就把这两个属性的工作机制、优先级、适用场景和常见坑一次性说清楚。内容主要面向用 Spring Boot 2.x/3.x 搭建微服务或单体应用的研发同学,不管你现在用 YAML 多文档、还是老式的application-{profile}.properties,都能直接对照着改配置。
1.1 Profile 体系的基本背景
先补一段背景知识,方便后面所有例子有共同语言。Spring 里的 profile,本质上是给BeanDefinition和配置属性文件打的一层逻辑分组。ApplicationContext在刷新容器之前,会从Environment里读取当前“活动 profile 集合”,然后:
- 带有
@Profile("dev")的配置类或 Bean,只有dev在这个集合里时才被注册; - 文件名为
application-dev.yml的配置资源,只有dev在集合里时才被加载; - YAML 多文档中带有
spring.config.activate.on-profile: dev的配置段,逻辑相同。
所以,不管是spring.profiles.active还是spring.profiles.include,它们最终的作用都是往这个“活动 profile 集合”里加成员。区别不在最终效果,而在谁去声明、从哪个配置源声明、以及触发条件是什么。
2. spring.profiles.active:谁决定服务以哪套配置起床
2.1 active 激活之后的连锁反应
spring.profiles.active是 Spring Boot 最常用的配置项之一,含义直白:启动时告诉我,当前应用要激活哪些 profile。
举个例子,下面这段配置声明了两个活动 profile:
spring: profiles: active: dev,eureka-local启动日志里会出现:
The following 2 profiles are active: "dev", "eureka-local"这句话很多人扫一眼就忘了,但它实际上暴露了 active 的一个关键特性:它接受的是一个列表,不是单一值。逗号分隔的每个名字都会进入活动集合,而不是“二选一”。很多初学者以为active: dev和active: prod是互斥开关,其实完全可以同时写dev,prod,只是要小心两个 profile 里的同名配置到底谁覆盖谁。属性覆盖的顺序不取决于 active 列表的书写顺序,而取决于配置源加载顺序,这一点后面讲坑的时候会细说。
当某个 profile 进入集合后,除了 Bean 过滤,application-{profile}.yml文件也会开始参与配置加载。比如当前激活集合里有prod,那么除了application.yml本身,application-prod.yml也会被读进来,其中定义的属性会覆盖主配置里的同名项。这就是很多项目里“一个主配置文件 + 一堆环境覆盖文件”的底层原理。
2.2 外部设置方式和优先级排序
spring.profiles.active最值得注意的一点是:它几乎总是被设计成“可以由外部环境接管”的配置项。Spring Boot 外部化配置的优先级从高到低大致如下:
| 设置方式 | 示例写法 | 说明 |
|---|---|---|
| 命令行参数 | --spring.profiles.active=prod | 优先级最高,适合容器启动脚本传递 |
| Java 系统属性 | -Dspring.profiles.active=prod | 常写在 JVM 参数里 |
| 操作系统环境变量 | SPRING_PROFILES_ACTIVE=prod | CI/CD 平台最常用的注入方式 |
| 应用内配置 | spring.profiles.active: prod | 写在application.yml里,作为开发默认值 |
也就是说,如果你想强制某个环境使用指定 profile,直接在部署平台上配一个SPRING_PROFILES_ACTIVE环境变量,就能覆盖应用配置文件里的 active 设置。这个设计的初衷是好的:同一个 jar 包,部署到哪套环境由调度系统决定,而不是靠手工改包内配置。
但这也带来一个副作用:应用内的 active 更像一个“默认值”。如果团队里有个人在本地启动时图省事,把application.yml里的 active 改成了自己需要的环境,忘记改回来就提交了,那么 CI 环境只要设置了环境变量,覆盖关系也会让这个提交内容不生效。换句话说,active 是一个可以被外部夺走控制权的开关,你很难用它来“强制固定”某些 profile。
2.3 单独使用 active 的短板
正因为 active 很容易被外部覆盖,所以它不适合承载“无论什么环境都必须加载”的配置。举两个具体例子:
- 你在
application.yml里写active: dev,monitor,希望监控端点永远打开。结果生产启动时命令行传了--spring.profiles.active=prod,monitor 就被挤掉了,监控数据直接缺失。 - 你在
active里放了一个公共安全配置common-security,希望所有环境统一生效。但测试环境和生产环境的启动脚本都各自传了不同的 active 值,这个公共配置很容易被遗漏。
active 的定位就是“运行时主开关”:由外部或主配置决定本次启动以哪套环境为主,但不应该承担“固定附属项”的职责。那固定附属项交给谁?就是接下来要讲的spring.profiles.include。
3. spring.profiles.include:不被外部覆盖的固定配置
3.1 加载机制:无条件的“附加激活”
spring.profiles.include的意思是:把指定的 profile 无条件加入活动集合。它写在应用配置里,和外部环境变量是否传入、传了什么,没有关系。
看一个典型配置:
spring: profiles: active: dev include: common,monitor当启动命令没有额外传参时,活动集合是dev, common, monitor。当启动命令传了--spring.profiles.active=prod时,活动集合就变成prod, common, monitor。active被外部覆盖了,但include声明的内容依然保留。
这里的本质区别在于声明主体和触发条件:
- active 的行为是“选择本次环境的主题”,它的值可以被运行时环境重新指定;
- include 的行为是“主题确定后,必须附带这些公共模块”,它是项目配置的一部分,不受运行参数影响。
在 Spring Boot 2.4 之后的实现里,配置文件数据加载时会通过Profiles组件统一处理spring.profiles.active、spring.profiles.include和spring.profiles.group。外部配置优先确定了初始激活集,再从这个初始集出发解析 include 和 group,最终写入Environment的活动 profile 集合。所以 include 在语义上确实就是“附加项”,它的存在是防御性的,防止任何环境下缺少某些公共能力。
3.2 include 的经典使用场景
我在实际项目里见到最多、也最推荐用 include 来承载的,是这几类东西:
- 监控和运维端点:比如 actuator 的暴露配置、健康检查公共配置,任何环境都不能漏。
- 日志框架的公共格式:JSON 日志输出、traceId 透传、日志级别覆盖策略,这些无论你在 dev 还是 prod 都应该一致。
- 跨环境的中间件地址封装:有些公司会把注册中心、配置中心、网关地址抽象成公共 profile,各个环境都 include 一份公共值,再在环境自己的 profile 里覆盖必要差异。
- 默认安全配置:比如统一的 CORS 策略、请求体大小限制、公共过滤器。
举个具体配置示例。假设你有一个application.yml:
spring: profiles: active: local include: common一个application-local.yml:
server: port: 8081 spring: datasource: url: jdbc:h2:mem:localdb以及一个application-common.yml:
management: endpoints: web: exposure: include: health,info,metrics logging: pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"这时你本地启动,活动集合是local, common,既有本地数据库,又有监管端点。哪怕你某天上线时用--spring.profiles.active=prod切换,common依然会跟着进入活动集合,因为 include 在配置层面做了兜底。
3.3 一处容易理解错的细节
有一个观点需要纠正:include 不是“active 的子集”,它和 active 一样会让对应配置文件和 Bean 生效。也就是说,spring.profiles.include: common之后,application-common.yml会被加载,带@Profile("common")的 Bean 也会被创建。
我见过有人把 include 理解成“只是引入额外属性文件,不影响 Bean”,然后调试半天发现某些 Bean 突然多注册了,最后才反应过来:include 引入的 profile 也是活动 profile,行为上跟 active 引入的 profile 没有任何区别。
4. active 与 include 的核心区别:一张表加三个执行视角
4.1 对比表:从使用角度看清楚差异
| 维度 | spring.profiles.active | spring.profiles.include |
|---|---|---|
| 声明位置 | 命令行、环境变量、系统属性、应用配置文件 | 主要在应用配置文件里声明 |
| 是否易被外部覆盖 | 是,高优先级外部配置会覆盖应用内设置 | 否,应用内声明后作为附加项持续生效 |
| 核心语义 | 选择“本次环境主主题” | 固定“主主题之外必须带上的公共模块” |
| 典型应用 | dev / test / staging / prod 环境切换 | 监控、日志、公共安全、基础中间件 |
| 配置值 | 支持逗号分隔的多个 profile | 支持逗号分隔的多个 profile |
| 加载结果 | 进入活动 profile 集合 | 同样进入活动 profile 集合 |
是否参与application-{profile}加载 | 是 | 是 |
| 能否引用 profile group | 能,active 的组名会被解析为成员 | 主要用于添加 profile,组名的展开行为在 2.4+ 有额外规则 |
4.2 从执行顺序看二者的归宿
如果你打开 Spring Boot 的配置加载日志,或者通过 actuator 的/env端点观察spring.profiles.active这个属性,会发现一个有意思的现象:active 和 include 最终都会归入同一个“激活 profile 集合”,而且在大多数版本的实现里,Profiles组件会先收集外部传入的 active 值,然后读取应用配置中的 include、group 信息,再通过迭代将所有附加项合并进去。
所以从源码层面看,两个属性的“归宿”是一样的,真正不同的是合并过程的输入来源。active 可以被更高优先级的 PropertySource 覆盖,而 include 不在外部命令行、环境变量那一条覆盖链上,这就是它“稳定”的根本原因。
4.3 边界行为:重复出现怎么办
假设你同时写了:
spring: profiles: active: dev,common include: common,monitorcommon既出现在 active 里,又出现在 include 里,会发生什么?答案是不会重复计数,也不会报错。Spring Boot 会把活动集合按名字去重,最终集合只是dev, common, monitor。这个行为在多数场景下是安全的,但它也提醒你:include 和 active 在配置上是“平等”的,不会因为你先写 active 就先加载,后写 include 就后覆盖。
5. 多环境切换中常见的坑与完整排查思路
这部分是文章里含金量最高的内容,因为这些坑几乎每个 Spring Boot 项目都会遇到,而且很多坑从报错信息上根本看不出原因。
5.1 坑一:include 写进了 profile-specific 文档,结果不生效
Spring Boot 2.4 之后,YAML 多文档配置的写法有了变化。很多人会随手把 include 写进---分隔之后的某个 profile 文档里,比如:
spring: config: activate: on-profile: dev profiles: include: common表面看起来逻辑挺顺:“在 dev 环境时额外带上 common”。但 Spring Boot 2.4+ 对这种写法是明确不支持的,spring.profiles.include和spring.profiles.active都不应该放在带有spring.config.activate.on-profile的文档里。结果就是 include 被忽略,common没有进入活动集合。
正确做法是把 include 放到全局配置段,或者放在---之前的第一段 YAML 文档里:
spring: profiles: include: common --- spring: config: activate: on-profile: dev # dev 的专属配置所以排查 include 是否生效时,第一件事就是打开 YAML 文件看看它写在哪一层。对于老项目还在用 Spring Boot 2.3 或更早版本的,这个问题会好一些,但也建议按新规范写,方便以后升级。
5.2 坑二:active 被高优先级外部配置覆盖,但团队不知道
这是文章开头事故的核心原因。application.yml里写active: dev,只对本地开发有效。一旦部署平台配了SPRING_PROFILES_ACTIVE=prod,这个文件里的 active 就会被覆盖。但如果你接着在配置文件里写了include: common,include 不会被覆盖。
由此产生的最典型误判是:团队以为切换 active 就能彻底换一套配置环境,结果发现某些开发期配置还活着,就开始怀疑 Spring Boot 的缓存、怀疑配置中心没刷新、甚至怀疑同事改了没提交。实际上就是 include 在起作用。
排查方法很简单:启动日志里看激活 profile 列表,或者请求 actuator 的/env端点,查spring.profiles.active的值以及activeProfiles数组。只要发现“我以为 prod 是唯一环境,结果列表里还有个公共 profile”,那基本就是 include 或 group 在起作用。
5.3 坑三:include 引用了不存在的 profile
include: nonexistent不会导致启动失败,它跟 active 一样,允许引用一个根本没定义任何配置的 profile。这里面藏着两个问题:
- 如果
nonexistent的意图是想加载application-nonexistent.yml,但文件名拼错了,Spring Boot 不会报错,你很难发现。 - 如果
nonexistent是要激活某个带@Profile("nonexistent")的 Bean,而类路径下根本没有这个类,同样不会报错。
所以 include 里的名字一定要做成集中管理,最好通过启动日志确认到底哪些 profile 被激活了,而不是靠“我觉得它应该生效”。
5.4 完整的排查链路
我把排查步骤整理成一个清单,照着做基本能定位 90% 的 profile 问题:
- 先看启动日志里的激活 profile 列表。Spring Boot 会明确输出
The following X profiles are active。 - 看 include 在 YAML 里的位置,确保它位于全局配置段,而不是
spring.config.activate.on-profile所属的文档里。 - 看是否有环境变量、命令行参数、系统属性覆盖了 active。
- 通过 actuator 的
/env端点查看spring.profiles.active原始值和最终值之间的差异。 - 检查
application-{profile}.yml文件名是否和 profile 名完全一致,大小写、中划线、下划线都要仔细看。 - 如果用了配置中心,检查远端配置是否也定义了一份 active/include,它的优先级在本地文件之上。
6. Spring Boot 2.4+ 的 profile group:include 的进化替代方案
6.1 从 include 到 group
Spring Boot 2.4 引入了spring.profiles.group来管理 profile 之间的组合关系。它比 include 更清晰的地方在于:你可以把一组 profile 定义一个名字,然后在 active 里只引用这个组名。
举个例子:
spring: profiles: group: local: - dev - common production: - prod-core - common - monitor active: local启动时 active 的值是local,Spring Boot 会把它解析成dev, common两个 profile。如果你在生产环境用--spring.profiles.active=production,那么实际激活的就是prod-core, common, monitor。
group 的引入让 include 不再是唯一的选择方案。两者的分工也逐渐清晰:
include适合“全局固定、无条件附加”的少量 profile。group适合“每个主环境需要不同组合”的场景,能够让外部只需要传一个组名,而不必知道组内到底有哪些 profile 名。
6.2 group 与 include 的选择逻辑
在实际项目里,我的建议是这样分:
| 期望行为 | 推荐方式 |
|---|---|
| 所有环境都必须加载的公共配置 | spring.profiles.include |
| 同一部署环境需要一组固定组合 | spring.profiles.group |
| 启动时需要外部决定环境 | active 用命令行或环境变量传入组名/ profile 名 |
| 环境切换时不想把内部 profile 名暴露给部署层 | 用 group 对外暴露一个友好名字 |
group 还有一个好处:它把组合关系集中放在一个地方,团队成员看配置文件就能明白“local 环境由哪些 profile 拼成”,而不是在启动命令里手工拼一堆名字。排错也更容易。
6.3 一套可以落地的分层配置模板
我目前在新项目里使用的模板大概长这样。application.yml只做三件事:定 group、选默认组、include 全局公共项。
spring: profiles: group: local: - common - local test: - common - test prod: - common - prod active: local include: base --- spring: config: activate: on-profile: base app: trace: enabled: true logging: level: root: INFO --- spring: config: activate: on-profile: common management: endpoints: web: exposure: include: health,info,metrics --- spring: config: activate: on-profile: local server: port: 8081 spring: datasource: url: jdbc:h2:mem:local --- spring: config: activate: on-profile: test server: port: 8082 --- spring: config: activate: on-profile: prod server: port: 8083外部部署时,只需要传一个组名,比如--spring.profiles.active=test,内部自动展开为test, common, base。public 的基础配置由base兜底,环境差异由 group 决定。
这样设计之后,active 的角色被限定为“接收外部指定的组名”,include 的角色被限定为“全局无条件附加项”,两者不再混淆。
结合我自己的经验,最值得记住的一条心得是:不要把 include 当成隐藏的 active 来用。它不能帮你做环境切换,也不适合承载需要条件判断的逻辑。它唯一擅长的就是“锁定公共配置”。当你在配置里看到某个 profile 总是在激活列表里出现,先想想它到底应该由 include 固定,还是应该由 group 组合;想清楚这一点,多环境配置的维护成本会直线下降。