
1. 为什么同时需要两个属性一次生产事故引出的问题先说个真事。之前团队里有个支付回调服务application.yml里本来写的是spring: profiles: active: dev include: common开发阶段一切正常成员各自在自己的分支上跑dev这个 profile 负责把本地 H2 数据库、调试日志开关、宽松的 CORS 配置都拉起来。后来服务要上线运维同事按流程在启动脚本里加了--spring.profiles.activeprod想让它直接切到生产配置。结果启动倒是很顺利但日志里出现了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。举个例子下面这段配置声明了两个活动 profilespring: 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.activeprod优先级最高适合容器启动脚本传递Java 系统属性-Dspring.profiles.activeprod常写在 JVM 参数里操作系统环境变量SPRING_PROFILES_ACTIVEprodCI/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.activeprodmonitor 就被挤掉了监控数据直接缺失。你在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.activeprod时活动集合就变成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.ymlspring: profiles: active: local include: common一个application-local.ymlserver: port: 8081 spring: datasource: url: jdbc:h2:mem:localdb以及一个application-common.ymlmanagement: 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.activeprod切换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.activespring.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_ACTIVEprod这个文件里的 active 就会被覆盖。但如果你接着在配置文件里写了include: commoninclude 不会被覆盖。由此产生的最典型误判是团队以为切换 active 就能彻底换一套配置环境结果发现某些开发期配置还活着就开始怀疑 Spring Boot 的缓存、怀疑配置中心没刷新、甚至怀疑同事改了没提交。实际上就是 include 在起作用。排查方法很简单启动日志里看激活 profile 列表或者请求 actuator 的/env端点查spring.profiles.active的值以及activeProfiles数组。只要发现“我以为 prod 是唯一环境结果列表里还有个公共 profile”那基本就是 include 或 group 在起作用。5.3 坑三include 引用了不存在的 profileinclude: 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 groupinclude 的进化替代方案6.1 从 include 到 groupSpring Boot 2.4 引入了spring.profiles.group来管理 profile 之间的组合关系。它比 include 更清晰的地方在于你可以把一组 profile 定义一个名字然后在 active 里只引用这个组名。举个例子spring: profiles: group: local: - dev - common production: - prod-core - common - monitor active: local启动时 active 的值是localSpring Boot 会把它解析成dev, common两个 profile。如果你在生产环境用--spring.profiles.activeproduction那么实际激活的就是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.activetest内部自动展开为test, common, base。public 的基础配置由base兜底环境差异由 group 决定。这样设计之后active 的角色被限定为“接收外部指定的组名”include 的角色被限定为“全局无条件附加项”两者不再混淆。结合我自己的经验最值得记住的一条心得是不要把 include 当成隐藏的 active 来用。它不能帮你做环境切换也不适合承载需要条件判断的逻辑。它唯一擅长的就是“锁定公共配置”。当你在配置里看到某个 profile 总是在激活列表里出现先想想它到底应该由 include 固定还是应该由 group 组合想清楚这一点多环境配置的维护成本会直线下降。