做后端开发的人几乎都遇到过这类需求同一套代码要同时部署到开发、测试、生产等不同环境每个环境的数据库地址、日志级别、缓存连接、第三方接口密钥可能完全不同。Spring Boot 的标准解法是 profile而在实际项目里最常碰到的两个配置就是spring.profiles.active和spring.profiles.include。这两个属性名字太像了很多人误以为它们是同一个东西的两种写法其实语义完全不同。spring.profiles.active负责“选主环境”spring.profiles.include负责“往当前环境里追加公共配置”。如果分不清这一点非常容易出现配置被覆盖、include 不生效、环境串配置的诡异问题。这篇文章就把这两行属性的实现逻辑、加载顺序、常见误区和实操配置完整讲一遍。我尽量用项目里真实会遇到的例子来说明而不是只贴语法。无论你是在维护一个老项目还是刚开始给新服务做多环境配置这几点都值得花几分钟看明白。1. 先从整体上理解这两行配置是干什么的1.1 多环境带来的配置管理麻烦在没有 profile 机制的年代我们切换环境往往是直接改配置文件或者维护多套 properties 文件打包时手动换一份。这个做法的最大问题是容易漏改、容易改错、还容易在合并代码时把生产配置一起提交上去。后来大家开始用 Maven 的 profile 做资源过滤用-Pdev这类参数在构建时替换配置文件。这套方案也能跑但它把“环境切换”提前到了编译阶段。真正发布到服务器上的 jar 已经是固定环境的产物运维想临时调整环境就非常不方便。Spring Boot 的 profile 不一样。它是在应用启动阶段根据spring.profiles.active决定加载哪些配置片段环境是运行时决定的而不是构建期决定。所以同一个 jar 包开发时用--spring.profiles.activedev启动生产上用--spring.profiles.activeprod启动不需要重新打包。这一点是解决多环境问题的核心思路。1.2 active 是“主开关”include 是“必含项”spring.profiles.active就是我们常说的“激活 profile”。比如你写了spring.profiles.activedev意味着应用会加载application-dev.yml里的配置同时dev这个环境标识会在整个应用上下文里生效。spring.profiles.include的意思则是“无论当前激活了哪个主环境我也要无条件加入这些 profile”。比如在application.yml里写spring: profiles: include: common, security那么启动时不管active指定的是dev还是prodcommon和security这两个 profile 都会被加载。include 里的配置通常放一些所有环境都需要的公共内容比如基础数据源、通用安全规则、全局过滤器等。从语义上看active 是“我这次运行主要用哪个环境”include 是“这些配置无论如何都给我带上”。两者是主从关系不是平级关系。1.3 为什么不能只靠 active 解决有人可能会问既然可以用逗号分隔为什么不直接写成spring.profiles.activedev,common,security我遇到过不少这么干的同事刚开始看着没问题后面维护起来就暴露了问题。主环境是dev、test、prod公共配置是common、security、logging。如果全部写在 active 里每次切换环境都要写一串很长的环境名很容易漏写common。一旦漏掉运行时就会出现一些难以定位的缺失配置。用 include 的好处在于公共配置与主环境解耦。你在application.yml顶层声明一次所有环境自动带上。后续新增一个环境只需要定义自己的主 profile 文件不用惦记 include 里那堆东西。2. 生效顺序与覆盖规则这一块最容易踩坑2.1 配置加载顺序的底层逻辑要理解 active 和 include 的区别核心是理解 Spring Boot 加载配置的顺序。官方文档和实际代码都表明profile 相关的处理实际上分两个阶段先处理spring.profiles.active指定的 profile。再处理spring.profiles.include包含的 profile。这个顺序非常关键。它意味着如果active和include里出现同一个配置项active会覆盖include。官方也明确说过include 的设计初衷就是不让它反过来覆盖 active。用一个场景说明。假设有这两个文件# application-common.yml server: port: 7070# application-dev.yml server: port: 8080启动参数是--spring.profiles.activedev --spring.profiles.includecommon最终生效的端口是8080因为dev属于 activecommon属于 includeactive 的优先级更高。如果你期望的是common覆盖dev那这个设计会直接让你失望。2.2 active 与 include 冲突时的取舍实际项目里我建议尽量不要让 include 和 active 里的配置项产生重叠。如果确实需要覆盖要有意识地区分优先级。一种常见做法是include 里放的是“缺省值”或“兜底配置”active 里放的是“环境专属覆盖值”。比如把通用超时时间放在application-common.ymlrequest: timeout: 3000然后某个专门处理长任务的task主环境放在application-task.ymlrequest: timeout: 15000这样的结果是跑 task 环境时超时时间被覆盖为 15 秒跑其他环境时用 3 秒兜底。这个用法是 include 比较理想的一种场景。2.3 include 放在不同位置效果完全不一样这是新手特别容易忽略的点。spring.profiles.include可以出现在不同层级的配置里但效果差别很大。如果放在application.yml顶层它就是一个“全局必含项”。不管 active 是什么include 都会生效。这点前面已经说过。如果放到某个 profile-specific 文件里比如application-dev.yml那么在Spring Boot 2.4 之前的版本它只会在激活dev时被处理。例如# application-dev.yml spring: profiles: include: common这段配置意味着只有当你激活dev时common才会被额外加载。激活prod时这段 include 不会执行因为prod不加载application-dev.yml。但在Spring Boot 2.4 之后这种写法已经不可靠了。官方在配置加载重构时明确过不再支持在 profile-specific 文件中动态添加 include。如果你用了 2.4 版本还想做条件化的分组加载应该使用spring.profiles.group而不是在application-dev.yml里写 include。这个变化我在后面专门讲。2.4 命令行参数、环境变量与配置文件的优先级spring.profiles.active和spring.profiles.include也可以通过命令行参数和环境变量传入。它们遵循 Spring Boot 常规的配置优先级命令行参数 Java 系统属性 操作系统环境变量 application-{profile}.yml application.yml 默认 profiles。我常用的启动方式是这样java -jar app.jar --spring.profiles.activeprod --spring.profiles.includecommon命令行参数写法简单适合本地联调和测试环境。部署到服务器时我更喜欢用环境变量export SPRING_PROFILES_ACTIVEprod export SPRING_PROFILES_INCLUDEcommon startup.shSpring Boot 的环境变量映射规则是把点号换成下划线、大写SPRING_PROFILES_ACTIVE对应spring.profiles.activeSPRING_PROFILES_INCLUDE对应spring.profiles.include。这里要特别注意环境变量的优先级比application.yml里的 include 更高。也就是说哪怕配置文件里写死了 include只要环境变量里显式设置了SPRING_PROFILES_INCLUDE就会以环境变量为准。这个机制在容器化部署时很有用可以不用改动代码和配置文件直接在部署平台上注入环境变量切换环境。3. 从零搭建一套可用的 profile 切换方案3.1 文件结构怎么规划我建议普通项目按照“基础配置 公共模块 环境配置”三组来组织文件。不要把所有内容都堆在application.yml里也不要让 profile-specific 文件之间互相依赖太多。一套比较清晰的目录结构是这样的src/main/resources/ ├── application.yml ├── application-common.yml ├── application-security.yml ├── application-logging.yml ├── application-dev.yml └── application-prod.ymlapplication.yml只放应用名、基础编码、以及spring.profiles.include这样的全局开关。application-common.yml放所有环境通用的数据源、缓存、MQ 连接等。application-security.yml放通用的安全配置。application-logging.yml放全局限流和日志格式。环境专属文件只放当前环境自定义的部分。这样做的好处是每个文件的职责单一。排查问题时只需要根据启动日志里加载了哪些 profile直接定位到对应文件不用在几个大文件之间来回翻滚。3.2 三种常用启动方式第一种是本地开发直接指定参数这也是最简单的测试方式java -jar order-service.jar --spring.profiles.activedev第二种是服务器上的 shell 脚本用环境变量注入export SPRING_PROFILES_ACTIVEprod exec java -jar order-service.jar第三种是测试环境分支固定比如在 CI 流水线里设置好环境变量让每次构建产物共用一个 jar然后根据部署环境传入不同的 profile。这种方式最符合“一次构建、多处运行”的思路。我在实际项目里还见过一种用法在 test 环境启动时把 include 也通过命令行传入用来临时加载一个只放模拟数据的 mock profile。这样不需要改动正式配置也不需要新增环境文件。3.3 完整示例某订单系统的 dev 与 prod 切换我拿一个虚构的订单服务来演示完整配置。假设这个服务需要数据库、Redis、消息队列三种基础依赖并且不同环境连接地址不一样。application.yml顶层spring: application: name: order-service profiles: include: common, loggingapplication-common.ymlspring: datasource: url: jdbc:mysql://localhost:3306/order_default username: root password: root redis: host: localhost port: 6379application-logging.ymllogging: pattern: console: %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%napplication-dev.ymlspring: datasource: url: jdbc:mysql://192.168.1.10:3306/order_dev username: dev_user password: dev_pass redis: host: 192.168.1.20application-prod.ymlspring: datasource: url: jdbc:mysql://10.20.30.40:3306/order_prod username: prod_user password: prod_pass redis: host: 10.20.30.50当使用--spring.profiles.activedev启动时实际加载的知识点是application.yml声明 include common, logging所以common和logging都会激活同时dev作为 active 激活。这样最终 profile 集合是dev, common, logging。由于 active 优先application-dev.yml中定义的数据库配置会覆盖application-common.yml中的默认数据库配置。当使用--spring.profiles.activeprod启动时加载的 profile 集合是prod, common, logging数据库走生产地址公共模块仍然是同一份。这个结构维护几个月后你会明显感觉到比把所有环境参数都堆在一个 active 列表里要轻松。4. 常见问题速查与排查技巧4.1 include 完全不生效配置没加载如果你启动时发现application-common.yml的内容根本没被加载大概率是 include 的位置写错了。常见错误是把 include 写进了application-{profile}.yml然后期望它全局生效。例如在application-dev.yml里写spring: profiles: include: common然后你用--spring.profiles.activetest启动此时test只加载application-test.yml根本不会去读application-dev.yml中的 include自然就没有common。如果想全局生效应该把 include 放到application.yml顶层。如果确实希望只有某个环境激活时才包含额外 profile建议用 profile group 的方案而不是依赖 profile-specific 文件里的 include。问题排查看启动日志最直接。Spring Boot 启动时会输出The following 3 profiles are active: dev, common, logging如果common不在这个列表里说明 include 没被解析到直接去检查配置文件位置和拼写。4.2 include 和 active 里同一个 key 冲突结果不是我想要的这个场景我在前面说过了。假设common里定义了server.port7070dev里定义了server.port8080启动 activedev、includecommon最终生效的是 8080。如果预期是相反结果那就不是 include 的用法不对而是对优先级理解不一致。判断优先级有一个简单办法把 active 里的 profile 看成“此次运行的主角”include 里的 profile 看成“配角”。配角提供基础内容主角可以覆盖配角。如果你希望某个公共配置无论如何都不能被覆盖必须在 active 环境里避开同 key 的重定义。另外多个 include profile 之间如果出现同名配置顺序也会影响结果。比如 include 是common, base而两个文件都定义server.port后解析的那个文件会覆盖先解析的。这种隐式顺序是最难排查的所以我强烈建议 include 的不同 profile 各自负责独立的配置域不要交叉定义同一个属性。4.3 Spring Boot 2.4 前后的行为差异如果你最近在做版本升级务必留意 include 的差异。Spring Boot 2.4 重构了配置加载机制有些以前能跑的写法在新版本里会失效。核心变化是在 Spring Boot 2.4 之前你可以在application-dev.yml中使用spring.profiles.include来追加其他 profile2.4 之后这种做法不再被支持如果仍然那么写大概率不会按预期加载。替代方案是使用spring.profiles.groupspring: profiles: group: dev: common, logging prod: common, logging然后在启动时只要指定--spring.profiles.activedev就会按组展开成dev, common, logging。这个语义更清晰也避免了很多 include 放在 profile-specific 文件里造成的隐性问题。还在维护老版本项目的读者建议尽快评估升级否则这种旧写法会阻碍你后续用上 2.4 之后的配置新特性。4.4 排查手段与速查表遇到 profile 相关问题时我一般按以下顺序排查看启动日志确认实际激活的 profile 列表。用actuator/env接口查看当前生效配置来源确认配置值来自哪个文件。检查 include 是否放在正确层级。检查版本差异确认是否有 2.4 后行为变更。最后再看同一个 key 在多少文件里被重复定义。场景推荐处理方式全局公共配置所有环境都要加载放在application.yml顶层 include或使用 profile group仅某环境要加载额外配置使用 2.4 的 profile group 更合理需通过运维调整 include用环境变量SPRING_PROFILES_INCLUDE注入active 和 include 同名配置以 active 为准不要期望 include 覆盖老版本项目升级后 include 失效改用 group 并把 include 从 profile-specific 文件移到主文件我个人在实际操作中的体会是配置文件的维护难点往往不是语法而是“看起来每个文件都对但整体行为不是预期”。把 active 和 include 的职责边界划清楚比记住各种参数写法重要得多。最后再分享一个小技巧每次启动新服务时在日志里确认一遍 “The following profiles are active” 这一行几秒钟就能避免大量环境错配问题。