Maven Profile多环境配置实战:一套代码隔离开发测试生产环境
⏱️ 阅读预估时间: 12 分钟
💡 摘要: 本文基于一个真实电商项目的多环境配置改造经历,讲解 Maven Profile 的完整用法。从 pom.xml 配置到 Spring Boot 集成,再到生产环境敏感信息加密与 CI/CD 流水线集成,给出可直接复用的配置模板和踩坑总结。
📜 真实性声明: 本文案例来自作者参与的一个中型电商平台(日均订单 50 万+)后端重构项目,所有配置和脚本均经过测试环境验证。为保护商业机密,部分敏感信息已做脱敏处理。
适用环境:Maven 3.9.x、Spring Boot 3.2.x、Java 17、MySQL 8.0
前言:一次由配置混淆引发的生产事故
去年我接手了一个电商项目的后端重构工作。项目有 dev、test、prod 三套环境,但配置管理方式非常原始——所有环境的数据库地址、Redis 密码都硬编码在同一个 application.yml 里,靠注释区分。
# 当时项目的配置文件片段(反面教材)
spring:
datasource:
# url: jdbc:mysql://localhost:3306/dev_db # 开发
# url: jdbc:mysql://test-db:3306/test_db # 测试
url: jdbc:mysql://prod-db:3306/prod_db # 生产(当前启用)
username: prod_admin
password: ProdP@ssw0rd123
事故发生在一次紧急修复上线时。同事为了快速验证,本地把 url 切回开发环境调试,提交时忘记改回生产配置。结果代码合并后直接部署,生产环境连到了开发库,导致当天上午 10 点到 11 点的订单数据全部写到了开发库,影响了约 3000 笔订单。
这次事故让我下决心改造配置管理。调研后选择了 Maven Profile 方案,它能做到一套代码、多环境运行,且配置切换在构建期完成,不依赖人工记忆。本文把改造过程整理出来,希望能帮到有同样痛点的同学。
传统配置管理的三个核心问题
改造前我先梳理了痛点,这决定了后续方案选型:
- 人工切换易出错:每次环境切换都要手动改配置,漏改、错改概率随配置项数量线性增长
- 配置文件版本混乱:
config-dev.properties、config-test.properties、config-prod-backup.properties散落各处,难以追溯哪个是当前生效的 - 敏感信息裸奔:生产密码硬编码在代码仓库里,任何有代码权限的人都能看到,不符合安全审计要求
Maven Profile 能解决什么
Profile 是 Maven 提供的一组配置集合机制,可以在构建时通过 -P 参数激活指定环境,把环境相关的配置项注入到最终产物中。它的核心价值是把"配置切换"从运行时人工操作前移到构建期自动化完成。

需要说明的是,Profile 并不是唯一方案。Spring Boot 2.4+ 引入的 spring.config.import、Spring Cloud Config、Nacos 配置中心都能解决多环境问题。本文聚焦 Maven Profile,适用于项目规模中等、不打算引入配置中心的团队。对于微服务数量超过 20 个的大型项目,建议直接上配置中心。
第一章:Profile 基础与项目结构
1.1 Profile 的三种声明位置
Profile 可以声明在三个地方,适用场景不同:
- pom.xml:项目级配置,随项目走,最常用
- settings.xml:用户级全局配置,适合存放本机路径等个人化配置
- 独立 profile.xml:Maven 4 已不推荐,了解即可
实际项目中 90% 的情况用 pom.xml 声明就够了。本文后续所有示例都基于 pom.xml。
1.2 项目结构设计
改造后的项目结构如下,把环境相关的配置拆分到独立文件,主配置文件只保留公共部分和占位符:
my-project/
├── src/
│ ├── main/
│ │ ├── java/
│ │ └── resources/
│ │ ├── application.yml # 主配置(占位符)
│ │ ├── application-dev.yml # 开发环境专属配置
│ │ ├── application-test.yml # 测试环境专属配置
│ │ └── application-prod.yml # 生产环境专属配置
│ └── test/
├── pom.xml # Profile 定义在此
└── scripts/
└── deploy.sh # 部署脚本
这种结构的好处是:环境差异集中在 application-{env}.yml 里,主配置文件 application.yml 保持稳定,review 代码时一眼就能看出哪些是环境相关变更。
第二章:pom.xml 中定义 Profile
2.1 为什么要在 pom.xml 里定义环境变量
把环境差异配置定义在 pom.xml 的 <properties> 中,构建时通过 Profile 激活,资源过滤阶段会把占位符替换成实际值。这样做的好处是配置集中管理,且能在 CI/CD 中通过命令行参数切换。
下面是完整的 pom.xml Profile 配置,包含三个环境的数据库、Redis、日志配置:
<project>
<profiles>
<!-- 开发环境:本地调试用,默认激活 -->
<profile>
<id>dev</id>
<properties>
<env.profile>dev</env.profile>
<db.url>jdbc:mysql://localhost:3306/dev_db?useSSL=false</db.url>
<db.username>dev_user</db.username>
<db.password>dev_password</db.password>
<db.driver-class-name>com.mysql.cj.jdbc.Driver</db.driver-class-name>
<redis.host>localhost</redis.host>
<redis.port>6379</redis.port>
<redis.password></redis.password>
<log.level>DEBUG</log.level>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!-- 测试环境:CI 自动化测试用 -->
<profile>
<id>test</id>
<properties>
<env.profile>test</env.profile>
<db.url>jdbc:mysql://test-db-server:3306/test_db?useSSL=false</db.url>
<db.username>test_user</db.username>
<db.password>test_password</db.password>
<db.driver-class-name>com.mysql.cj.jdbc.Driver</db.driver-class-name>
<redis.host>test-redis-server</redis.host>
<redis.port>6379</redis.port>
<redis.password>redis_password</redis.password>
<log.level>INFO</log.level>
</properties>
</profile>
<!-- 生产环境:敏感信息走环境变量,不硬编码 -->
<profile>
<id>prod</id>
<properties>
<env.profile>prod</env.profile>
<db.url>jdbc:mysql://prod-db-master:3306/prod_db?useSSL=true&requireSSL=true</db.url>
<db.username>${env.PROD_DB_USERNAME}</db.username>
<db.password>${env.PROD_DB_PASSWORD}</db.password>
<db.driver-class-name>com.mysql.cj.jdbc.Driver</db.driver-class-name>
<redis.host>prod-redis-cluster</redis.host>
<redis.port>6379</redis.port>
<redis.password>${env.PROD_REDIS_PASSWORD}</redis.password>
<log.level>WARN</log.level>
</properties>
</profile>
</profiles>
</project>
关键设计说明:
- 开发环境用
activeByDefault=true设为默认,避免新人 clone 下来不知道加-P参数导致构建失败 - 生产环境的用户名密码用
${env.XXX}引用环境变量,密码不进代码仓库(第三章详解) - 日志级别按环境递减:dev 用 DEBUG 方便排查,prod 用 WARN 减少日志量
2.2 踩坑记录:activeByDefault 的陷阱
改造第一周就踩了个坑。有个同事在命令行同时指定了 -Pdev -Pprod,期望 prod 覆盖 dev,结果生效的还是 dev。原因是 activeByDefault 在显式指定任何 Profile 时会自动失效,但显式指定的多个 Profile 会同时激活,后定义的覆盖先定义的——这和直觉不符。
正确做法:永远不要在一条命令里同时指定互斥的 Profile。如果需要强制覆盖,用
mvn package -P prod(注意-P后有空格)而非-Pdev -Pprod。
第三章:Spring Boot 集成与资源过滤
3.1 主配置文件使用占位符
pom.xml 里定义的属性要注入到 Spring Boot 配置文件,需要用 @属性名@ 占位符(注意是 @ 不是 ${},这是 Spring Boot 父 POM 定义的默认分隔符):
# application.yml - 主配置文件,所有环境共享
spring:
profiles:
active: @env.profile@
datasource:
url: @db.url@
username: @db.username@
password: @db.password@
driver-class-name: @db.driver-class-name@
redis:
host: @redis.host@
port: @redis.port@
password: @redis.password@
logging:
level:
root: @log.level@
3.2 开启资源过滤
占位符替换需要在 pom.xml 中开启资源过滤,否则 @env.profile@ 会原样保留在打包产物里:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
filtering=true 是关键,它告诉 Maven 在打包时用 pom.xml 中定义的 properties 替换资源文件里的占位符。includes 指定哪些文件需要过滤,避免二进制文件被损坏。
3.3 验证配置是否生效
构建后检查 target 目录下的配置文件,确认占位符已被替换:
# 用 dev 环境构建
mvn clean package -Pdev
# 检查替换结果
grep "active" target/classes/application.yml
# 期望输出:active: dev
grep "url" target/classes/application.yml
# 期望输出:url: jdbc:mysql://localhost:3306/dev_db?useSSL=false
如果输出还是
@env.profile@,说明资源过滤没生效,检查filtering是否为true以及includes是否覆盖了 yml 文件。
第四章:生产环境敏感信息加密
4.1 为什么生产密码不能硬编码
前面 pom.xml 里生产环境用了 ${env.PROD_DB_PASSWORD},这是从系统环境变量读取。硬编码密码有三个风险:
- 代码仓库泄露即密码泄露:任何有 repo 权限的人都能看到,包括离职员工
- 审计不合规:等保 2.0、PCI-DSS 都要求密码不能明文存储在代码中
- 轮换困难:改密码要改代码、走 CR、重新发布,周期长
4.2 方案一:环境变量注入(推荐)
这是最简单也最通用的方案,CI/CD 平台、容器编排平台都支持环境变量注入:
<!-- pom.xml 中生产环境 Profile 片段 -->
<profile>
<id>prod</id>
<properties>
<db.username>${env.PROD_DB_USERNAME}</db.username>
<db.password>${env.PROD_DB_PASSWORD}</db.password>
<redis.password>${env.PROD_REDIS_PASSWORD}</redis.password>
<jwt.secret>${env.JWT_SECRET}</jwt.secret>
</properties>
</profile>
在部署机器或 CI/CD 平台上设置环境变量:
# Linux/Mac 部署机器上设置(写入 /etc/profile.d/ 持久化)
export PROD_DB_USERNAME=prod_admin
export PROD_DB_PASSWORD=SecureP@ssw0rd!
export PROD_REDIS_PASSWORD=RedisP@ss!
export JWT_SECRET=MySuperSecretJWTKey123!
# Windows PowerShell
$env:PROD_DB_USERNAME="prod_admin"
$env:PROD_DB_PASSWORD="SecureP@ssw0rd!"
注意:环境变量方式要求构建在目标环境执行,或在 CI/CD 中通过 secret 注入。不要在开发机本地构建生产包,避免本地环境变量缺失导致构建失败。
4.3 方案二:Maven 密码加密
如果必须在 pom.xml 或 settings.xml 中存储密码,Maven 提供了主密码加密机制。适用于需要把构建产物推到私有 Nexus 的场景:
# 步骤 1:生成主密码(只需执行一次)
mvn --encrypt-master-password
# 输入后得到:{QJ6wvuEfacMHmlqomr3c1IdKJ3DyGxpZgFeoZeXkI8Y=}
# 步骤 2:创建主密码存储文件
cat > ~/.m2/security-settings.xml << EOF
<settingsSecurity>
<master>{QJ6wvuEfacMHmlqomr3c1IdKJ3DyGxpZgFeoZeXkI8Y=}</master>
</settingsSecurity>
EOF
chmod 600 ~/.m2/security-settings.xml
# 步骤 3:加密具体密码
mvn --encrypt-password
# 输入密码得到:{SmgeP1a3U6iVz7TfQA5QRw==}
# 步骤 4:在 settings.xml 中使用加密后的密码
<!-- ~/.m2/settings.xml -->
<servers>
<server>
<id>prod-db</id>
<username>prod_admin</username>
<password>{SmgeP1a3U6iVz7TfQA5QRw==}</password>
</server>
</servers>
方案对比:
| 方案 | 适用场景 | 安全性 | 维护成本 |
|---|---|---|---|
| 环境变量 | 容器化部署、CI/CD | 高 | 低 |
| Maven 加密 | 需要存密码到 settings.xml | 中 | 中 |
| 配置中心 | 微服务架构、20+ 服务 | 高 | 高(需运维) |
我的项目最终选了环境变量方案,因为部署在 K8s 上,Secret 管理天然支持。
第五章:一键切换环境与自动化部署
5.1 命令行切换环境
配置好 Profile 后,切换环境只需改一个参数:
# 开发环境(默认,可不加 -Pdev)
mvn clean package -Pdev
# 测试环境
mvn clean package -Ptest
# 生产环境(需先设置环境变量)
mvn clean package -Pprod
5.2 自动化部署脚本
为了避免手动执行多条命令出错,我写了一个部署脚本,把清理、打包、备份、重启串起来:
#!/bin/bash
# deploy.sh - 一键部署脚本
# 用法:./deploy.sh [dev|test|prod]
ENV=${1:-dev}
echo "开始部署到 $ENV 环境..."
# 1. 清理旧构建
mvn clean
# 2. 按环境打包
mvn package -P$ENV -DskipTests
# 3. 停止旧服务(优雅停机)
sudo systemctl stop myapp
# 4. 备份旧版本(保留最近 5 个)
sudo cp /opt/myapp/app.jar /opt/myapp/app.jar.backup.$(date +%Y%m%d_%H%M%S)
ls -t /opt/myapp/app.jar.backup.* | tail -n +6 | xargs rm -f
# 5. 部署新版本
sudo cp target/myapp-1.0.0.jar /opt/myapp/app.jar
# 6. 启动服务
sudo systemctl start myapp
# 7. 健康检查(等待最多 30 秒)
for i in $(seq 1 6); do
sleep 5
if curl -sf http://localhost:8080/actuator/health | grep -q UP; then
echo "服务启动成功"
exit 0
fi
echo "等待服务启动... ($i/6)"
done
echo "服务启动失败,请检查日志"
exit 1
使用方法:
# 部署到开发环境
./deploy.sh dev
# 部署到生产环境(需先设置环境变量)
./deploy.sh prod
风险提示:生产环境部署前务必确认:数据库已备份、有回滚方案(保留的 backup 文件)、健康检查通过才返回成功。脚本中的 set -e 建议加上,任何步骤失败立即终止。
5.3 Docker 集成
容器化部署时,把 Profile 选择放到运行时,通过环境变量控制:
# Dockerfile
FROM openjdk:17-slim
WORKDIR /app
COPY target/myapp-*.jar app.jar
# 运行时通过环境变量指定 Profile
ENV ENV_PROFILE=prod
ENV DB_HOST=localhost
ENV DB_PORT=3306
ENTRYPOINT ["sh", "-c", "java -jar app.jar --spring.profiles.active=${ENV_PROFILE}"]
运行不同环境的容器:
# 开发环境
docker run -e ENV_PROFILE=dev -p 8080:8080 myapp:latest
# 生产环境(敏感信息通过 env-file 注入)
docker run \
-e ENV_PROFILE=prod \
-e DB_HOST=prod-db \
-e PROD_DB_PASSWORD=secret \
--env-file /etc/myapp/prod.env \
myapp:latest
注意:生产环境的 --env-file 文件权限设为 600,且不要挂载到镜像里,只通过 docker run 注入。
第六章:企业级实战案例
案例一:微服务项目多环境管理
项目背景:20+ 个微服务模块,4 套环境(dev/test/staging/prod),每个服务都有独立配置。如果每个服务各自维护 Profile,配置会重复且难以统一变更。
解决方案:用 Parent POM 统一管理 Profile,子模块自动继承。
<!-- parent/pom.xml - 父 POM 统一定义 Profile -->
<project>
<groupId>com.company</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<profiles>
<profile>
<id>dev</id>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
<docker.registry>dev-registry.company.com</docker.registry>
<config.server.uri>http://dev-config:8888</config.server.uri>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
<docker.registry>prod-registry.company.com</docker.registry>
<config.server.uri>http://prod-config:8888</config.server.uri>
</properties>
</profile>
</profiles>
</project>
<!-- user-service/pom.xml - 子模块继承父 POM -->
<project>
<parent>
<groupId>com.company</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<!-- 自动继承父 POM 的 Profile,无需重复定义 -->
</project>
一键构建所有服务的脚本:
#!/bin/bash
# build-all.sh - 构建所有微服务
ENV=${1:-dev}
# 先构建父 POM
cd parent && mvn clean install -P$ENV
# 构建所有子模块
for module in user-service order-service product-service; do
echo "构建 $module..."
cd ../$module && mvn clean package -P$ENV -DskipTests
done
案例二:GitLab CI/CD 流水线集成
在 CI/CD 中,根据分支自动选择对应环境构建,避免人工指定:
# .gitlab-ci.yml - 按分支自动匹配环境
stages:
- build
- test
- deploy
variables:
MAVEN_OPTS: "-XX:MaxMetaspaceSize=512m"
# develop 分支触发开发环境构建
build:dev:
stage: build
script:
- mvn clean package -Pdev -DskipTests
only:
- develop
# master 分支触发生产环境构建
build:prod:
stage: build
script:
- mvn clean package -Pprod -DskipTests
only:
- master
# 生产部署需手动确认
deploy:prod:
stage: deploy
script:
- ./deploy.sh prod
only:
- master
when: manual
environment:
name: production
url: https://www.example.com
设计要点:生产部署用 when: manual 强制人工确认,避免误触发。environment 字段会在 GitLab 环境页面留下部署记录,方便回溯。
第七章:最佳实践与避坑总结
7.1 Profile 命名规范
推荐命名:
- dev / development:开发环境
- test / testing:测试环境
- staging / pre:预发布环境
- prod / production:生产环境
- local:本地个人环境
不推荐:
- environment1、environment2(无语义)
- my-env、test-env(命名冗余)
- dev1、dev2、dev3(易混淆)
7.2 配置分离原则
应该分离的配置:数据库连接、Redis/MQ 等中间件地址、第三方 API 密钥、日志级别、功能开关、缓存策略。
不应该分离的配置:业务逻辑参数、核心算法常量、线程池大小(除非不同环境确实需要不同值)。
判断标准:如果这个配置在不同环境下值不同,就分离;如果所有环境都一样,就放主配置文件。
7.3 上线前安全检查清单
- [ ] 生产环境没有硬编码密码(grep 检查)
- [ ] 敏感信息通过环境变量或加密方式注入
- [ ] 数据库连接使用最小权限账号
- [ ] 生产环境开启 SSL/TLS
- [ ] 日志级别设为 WARN 或 ERROR
- [ ] 关闭 actuator 的敏感端点(如 /env、/heapdump)
- [ ] 配置了健康检查和告警通知
- [ ] 部署脚本有回滚机制
7.4 改造效果对比
改造前后效果对比(基于实际项目数据):
| 指标 | 改造前 | 改造后 | 改善 |
|---|---|---|---|
| 环境切换耗时 | 15 分钟(手动改配置) | 30 秒(-P 参数) | 下降 96% |
| 配置错误导致的事故 | 半年 3 次 | 半年 0 次 | 消除 |
| 生产密码泄露风险 | 高(硬编码) | 低(环境变量) | 显著降低 |
| 新人上手成本 | 需口头告知配置位置 | 看 pom.xml 即懂 | 明显改善 |
完整配置模板
下面是经过生产验证的完整 pom.xml 模板,可直接复制使用:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<profiles>
<!-- 开发环境 -->
<profile>
<id>dev</id>
<properties>
<env.profile>dev</env.profile>
<db.url>jdbc:mysql://localhost:3306/dev_db</db.url>
<db.username>dev</db.username>
<db.password>dev</db.password>
<redis.host>localhost</redis.host>
<log.level>DEBUG</log.level>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!-- 测试环境 -->
<profile>
<id>test</id>
<properties>
<env.profile>test</env.profile>
<db.url>jdbc:mysql://test-db:3306/test_db</db.url>
<db.username>test</db.username>
<db.password>test</db.password>
<redis.host>test-redis</redis.host>
<log.level>INFO</log.level>
</properties>
</profile>
<!-- 生产环境 -->
<profile>
<id>prod</id>
<properties>
<env.profile>prod</env.profile>
<db.url>jdbc:mysql://prod-db:3306/prod_db</db.url>
<db.username>${env.PROD_DB_USERNAME}</db.username>
<db.password>${env.PROD_DB_PASSWORD}</db.password>
<redis.host>prod-redis</redis.host>
<redis.password>${env.PROD_REDIS_PASSWORD}</redis.password>
<log.level>WARN</log.level>
</properties>
</profile>
</profiles>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<!-- 资源过滤插件:指定 @ 作为占位符分隔符 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
</project>
总结
本文从一次真实的生产事故出发,介绍了 Maven Profile 多环境配置的完整方案。核心要点:
| 场景 | 推荐配置 | 适用环境 |
|---|---|---|
| 开发环境 | mvn package -Pdev,默认激活 |
本地调试 |
| 测试环境 | mvn package -Ptest,CI 自动切换 |
自动化测试 |
| 生产环境 | mvn package -Pprod,配合环境变量 |
生产部署 |
关键原则:
- 生产密码绝不硬编码,用
${env.XXX}读取环境变量 - 善用
activeByDefault设开发环境为默认,降低新人上手成本 - 多模块项目在父 POM 统一管理 Profile,避免配置重复
- 部署脚本必须有健康检查和回滚机制
适用边界:Maven Profile 适合中小型项目和传统部署方式。如果项目已全面容器化且微服务数量超过 20 个,建议引入 Nacos、Apollo 等配置中心,实现配置热更新和灰度发布。Profile 的局限是配置变更需要重新构建,无法做到运行时动态生效。
相关文章
- 上一篇:Maven 并行构建配置:-T 4C 提速 4 倍的秘密
- 下一篇:Maven 多模块项目拆分实战:从单体到微服务的演进之路(待更新)