Spring Boot 中的 Profile:开发、测试、生产配置
1. 概述
应用程序通常在多个环境中运行。你的本地机器使用 H2 数据库。测试服务器使用独立的 PostgreSQL。生产环境使用真实的集群和真实的 SMTP。如果每次部署都手工修改配置文件,迟早你会不小心把开发配置推送到生产环境。
Spring Boot 通过 profile 解决这个问题。读完本文,你将学会如何按 profile 分离配置文件,使用 @Profile 开启/关闭 bean,通过多种方式激活 profile,用 @ActiveProfiles 编写测试,并避免常见错误。
2. 什么是 Profile?
Profile 是一个有名字的标签,例如 dev、test、prod。Spring 用这个标签来决定两件事:
- 哪些配置会被加载:
application-dev.yml只有在 profiledev激活时才会被读取。 - 哪些 bean 会被创建:标注
@Profile("prod")的 bean 只有在 profileprod激活时才会被注册。
如果没有激活任何 profile,Spring 会使用一个名为 default 的默认 profile。不标注 @Profile 的 bean 始终被创建,无论哪个 profile 激活。
你只需要基本的 web 依赖来完成本文中的示例:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
3. 按 Profile 分离配置文件
3.1. 分离成多个文件
命名约定是 application-{profile}.yml(或 .properties)。文件 application.yml 包含公共配置,总是被加载:
# application.yml
spring:
application:
name: shop-service
profiles:
default: dev
app:
mail:
from: no-reply@shop.vn
host: localhost
按 profile 的文件只包含不同的部分:
# application-dev.yml
server:
port: 8080
logging:
level:
root: DEBUG
# application-test.yml
server:
port: 0
app:
mail:
host: fake-smtp
# application-prod.yml
server:
port: 80
logging:
level:
root: WARN
app:
mail:
host: ${MAIL_HOST}
说明:
spring.profiles.default: dev表示当你没有激活任何 profile 时,Spring 使用dev而不是default。开发机可以直接运行而无需传递参数。- 当 profile
prod激活时,Spring 先加载application.yml,然后是application-prod.yml。profile 文件中的值会覆盖公共配置中的值。因此app.mail.host变成环境变量MAIL_HOST的值,而app.mail.from仍然保持no-reply@shop.vn。 test中的server.port: 0让 Spring 选择一个随机端口,避免并行运行时的端口冲突。
3.2. 合并到一个 YAML 文件
如果你想要更少的文件,可以使用 YAML 多文档格式,用 --- 分隔:
# application.yml
app:
mail:
host: localhost
---
spring:
config:
activate:
on-profile: prod
app:
mail:
host: smtp.shop.vn
第二个文档仅在 prod 激活时生效。当每个 profile 差异较小时,这种方式很简洁。当配置很长时,分离成文件仍然更易读和易于审查。
注意:在
.properties文件中,使用#---行来分隔文档,而不是---。
4. 激活 Profile
有多种方式。优先级遵循 Spring Boot 的通用规则:命令行参数 > 环境变量 > 配置文件。
命令行参数:
java -jar shop-service.jar --spring.profiles.active=prod
JVM 系统属性:
java -Dspring.profiles.active=prod -jar shop-service.jar
环境变量,这是 Docker 和 Kubernetes 中最常见的方式:
export SPRING_PROFILES_ACTIVE=prod
java -jar shop-service.jar
使用 Maven 插件运行时:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
通过代码,当你想始终添加某个特定 profile 时很有用:
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ShopApplication.class);
app.setAdditionalProfiles("metrics");
app.run(args);
}
}
setAdditionalProfiles 是添加 profile 到列表中,不会替换从外部激活的 profile。用 --spring.profiles.active=prod 运行时,prod 和 metrics 都会激活。
你可以用逗号同时激活多个 profile:--spring.profiles.active=prod,eu。如果两个文件都定义同一个 key,后面的 profile 胜出。在这个例子中,application-eu.yml 覆盖 application-prod.yml。
应用启动时,日志会告诉你哪个 profile 正在运行:
INFO c.e.shop.ShopApplication : The following 1 profile is active: "prod"
如果没有激活任何 profile,也没有设置 spring.profiles.default,你会看到:
INFO c.e.shop.ShopApplication : No active profile set, falling back to 1 default profile: "default"
每次部署时都要查看这行日志。这是快速发现服务器运行了错误 profile 的最好方法。
5. 使用 @Profile 开启/关闭 Bean
不同的配置只是问题的一部分。有时你需要完全不同的实现。例如:dev 只是将电子邮件打印到控制台,test 将其保存到内存以便检查,prod 通过 SMTP 发送。
定义一个公共接口:
public interface NotificationService {
void send(String to, String message);
}
dev 的实现:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println("[DEV] 发送给 " + to + ": " + message);
}
}
test 的实现,保存消息以进行断言:
@Service
@Profile("test")
public class InMemoryNotificationService implements NotificationService {
private final List<String> sentMessages = new CopyOnWriteArrayList<>();
@Override
public void send(String to, String message) {
sentMessages.add(to + "|" + message);
}
public List<String> getSentMessages() {
return List.copyOf(sentMessages);
}
}
prod 的实现:
@Service
@Profile("prod")
public class SmtpNotificationService implements NotificationService {
private final String host;
public SmtpNotificationService(@Value("${app.mail.host}") String host) {
this.host = host;
}
@Override
public void send(String to, String message) {
// 实际上会使用 JavaMailSender;这里只是为了演示
System.out.println("[SMTP " + host + "] 发送给 " + to);
}
}
使用这个类的代码不需要知道它在哪个环境中:
@Service
public class OrderService {
private final NotificationService notificationService;
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void placeOrder(String customerEmail, long orderId) {
notificationService.send(customerEmail, "订单 #" + orderId + " 已创建");
}
}
在任何时刻,只有三个 NotificationService bean 中的一个被创建。Spring 会把正确的 bean 注入到 OrderService 中。用 profile dev 运行并调用 placeOrder("an@shop.vn", 42),你会得到:
[DEV] 发送给 an@shop.vn: 订单 #42 已创建
5.1. Profile 表达式
@Profile 支持逻辑表达式:
@Configuration
public class SecurityConfig {
@Bean
@Profile("!prod")
public CommandLineRunner seedDemoData() {
return args -> System.out.println("加载演示数据...");
}
@Bean
@Profile("dev | test")
public CommandLineRunner relaxedCors() {
return args -> System.out.println("对所有源开放 CORS");
}
}
!prod:在除了prod之外的所有 profile 中创建 bean。dev | test:当dev或test激活时创建。也支持&表示“且”,例如prod & eu。
@Profile 可以放在 @Component/@Configuration 类上,或者放在单个 @Bean 方法上。放在 @Configuration 上时,其中的所有 bean 都受该条件限制。
5.2. 在代码中检查 Profile
有时你需要在逻辑中进行分支。使用 Environment:
@Component
public class StartupBanner implements CommandLineRunner {
private final Environment environment;
public StartupBanner(Environment environment) {
this.environment = environment;
}
@Override
public void run(String... args) {
if (environment.acceptsProfiles(Profiles.of("prod"))) {
System.out.println("正在运行生产环境");
}
}
}
尽量少用这种方式。如果代码中到处是 if (prod),你应该把它们分离成带 @Profile 的独立 bean。
6. Profile 组
随着系统增长,你可能想把配置分成更小的部分,比如 proddb 用于数据库,prodmq 用于消息代理。Profile 组允许你用一个名字激活整个集群:
# application.yml
spring:
profiles:
group:
prod: proddb, prodmq
dev: devdb
现在运行 --spring.profiles.active=prod 会激活 prod、proddb 和 prodmq。Spring 会依次加载 application-prod.yml、application-proddb.yml、application-prodmq.yml。运维人员只需要记住一个名字。
7. 使用 @ActiveProfiles 编写测试
在测试中,使用 @ActiveProfiles 选择 profile。下面的测试检查两件事:正确的 bean 被注入,以及来自 application-test.yml 的配置生效。
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceProfileTest {
@Autowired
private OrderService orderService;
@Autowired
private NotificationService notificationService;
@Value("${app.mail.host}")
private String mailHost;
@Test
void shouldUseInMemoryNotificationInTestProfile() {
assertThat(notificationService).isInstanceOf(InMemoryNotificationService.class);
}
@Test
void shouldRecordNotificationWhenOrderPlaced() {
orderService.placeOrder("binh@shop.vn", 7);
var inMemory = (InMemoryNotificationService) notificationService;
assertThat(inMemory.getSentMessages())
.contains("binh@shop.vn|订单 #7 已创建");
}
@Test
void shouldOverrideMailHostFromTestProfile() {
assertThat(mailHost).isEqualTo("fake-smtp");
}
}
说明:
@ActiveProfiles("test")为这个类激活 profiletest,无论spring.profiles.default是什么。- 第一个测试确认 Spring 选择了
InMemoryNotificationService而不是真正发送邮件。 - 第二个测试使用内存中的 bean 来检查消息内容,无需 mock。
- 第三个测试证明来自
application-test.yml的app.mail.host已覆盖application.yml中的localhost。
注意:每个不同的 profile 组合都会创建一个单独的 Spring context。如果每个测试类使用不同的 profile 集合,context 缓存就无法重用,整个测试套件会变得很慢。应该将测试中的 profile 组合数量保持在最小值。
8. 常见错误
在 profile 文件中设置 spring.profiles.active。 从 Spring Boot 2.4 开始,在 application-dev.yml 中声明 spring.profiles.active 或 spring.profiles.include(或在带有 on-profile 的文档中)会导致应用无法启动:
InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from
location 'class path resource [application-dev.yml]' is invalid in a profile specific resource
应该使用 profile 组(第 6 节)来替代。
在 application.yml 中硬编码 spring.profiles.active=dev。 这个文件被打包到 jar 中。如果有人忘记在服务器上传递环境变量,生产环境会用开发配置运行。应该在本地环境使用 spring.profiles.default,并在服务器上始终显式激活 profile。
在 application-prod.yml 中放置密钥。 数据库密码或 API 密钥不应该在 Git 中。应该使用像 ${DB_PASSWORD} 这样的占位符,并通过环境变量或 secret 管理器提供值。
没有 bean 匹配 profile。 如果你用 staging profile 运行,但只有 dev、test、prod 的实现,应用会报错:
Parameter 0 of constructor in OrderService required a bean of type
'NotificationService' that could not be found.
考虑使用 @Profile("!prod") 添加一个回退 bean,或为新 profile 添加实现。
将 profile 滥用于功能开关。 Profile 适合用来描述环境。如果你想独立于环境来开启/关闭某个功能,使用 @ConditionalOnProperty 通常更清楚:
@Bean
@ConditionalOnProperty(name = "app.feature.loyalty-points", havingValue = "true")
public LoyaltyService loyaltyService() {
return new LoyaltyService();
}
当你有像 prod-with-loyalty 或 dev-no-cache 这样的 profile 时,这表明你应该转向使用属性。
9. 总结
Profile 帮助你按环境隔离配置和行为,而无需修改代码。你已经看到了如何使用 application-{profile}.yml 覆盖配置、使用 @Profile 选择实现、使用 profile 组来组合多个 profile,以及使用 @ActiveProfiles 进行测试。
几个要记住的原则:在 application.yml 中放置公共配置,仅在 profile 文件中记录差异。在服务器上通过环境变量或命令行参数激活 profile。不要把密钥提交到 Git。在每次部署后始终检查日志行 The following profiles are active。