Profile trong Spring Boot: cấu hình dev, test, prod
1. Tổng quan
Một ứng dụng thường chạy ở nhiều môi trường. Máy của bạn dùng database H2. Server test dùng PostgreSQL riêng. Production dùng cluster thật kèm SMTP thật. Nếu sửa tay file cấu hình mỗi lần deploy, sớm muộn bạn sẽ đẩy nhầm cấu hình dev lên prod.
Spring Boot giải quyết việc này bằng profile. Đọc xong bài, bạn sẽ biết cách tách file cấu hình theo profile, bật/tắt bean bằng @Profile, kích hoạt profile theo nhiều cách, viết test với @ActiveProfiles và tránh các lỗi phổ biến.
2. Profile là gì?
Profile là một cái nhãn có tên, ví dụ dev, test, prod. Spring dùng nhãn này để quyết định hai việc:
- Cấu hình nào được nạp:
application-dev.ymlchỉ được đọc khi profiledevđang active. - Bean nào được tạo: bean gắn
@Profile("prod")chỉ được đăng ký khi profileprodđang active.
Nếu không có profile nào được kích hoạt, Spring dùng profile mặc định tên là default. Bean không gắn @Profile luôn được tạo, bất kể profile nào đang active.
Bạn chỉ cần dependency web cơ bản cho các ví dụ trong bài:
<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. File cấu hình theo profile
3.1. Tách thành nhiều file
Quy ước đặt tên là application-{profile}.yml (hoặc .properties). File application.yml chứa cấu hình chung, luôn được nạp:
# application.yml
spring:
application:
name: shop-service
profiles:
default: dev
app:
mail:
from: no-reply@shop.vn
host: localhost
Các file theo profile chỉ ghi phần khác biệt:
# 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}
Giải thích:
spring.profiles.default: devnghĩa là khi bạn không kích hoạt profile nào, Spring dùngdevthay vìdefault. Máy dev chạy ngay mà không cần tham số.- Khi profile
prodactive, Spring nạpapplication.ymltrước rồi đếnapplication-prod.yml. Giá trị trong file profile ghi đè giá trị chung. Vì vậyapp.mail.hostthành giá trị của biến môi trườngMAIL_HOST, cònapp.mail.fromvẫn giữno-reply@shop.vn. server.port: 0trongtestcho Spring chọn port ngẫu nhiên, tránh xung đột khi chạy song song.
3.2. Gộp vào một file YAML
Nếu muốn ít file hơn, bạn có thể dùng YAML nhiều document, phân cách bằng ---:
# application.yml
app:
mail:
host: localhost
---
spring:
config:
activate:
on-profile: prod
app:
mail:
host: smtp.shop.vn
Document thứ hai chỉ có hiệu lực khi prod active. Cách này gọn khi mỗi profile có ít khác biệt. Khi cấu hình dài, tách file vẫn dễ đọc và dễ review hơn.
Lưu ý: Trong file
.properties, bạn dùng dòng#---để tách document thay cho---.
4. Kích hoạt profile
Có nhiều cách. Thứ tự ưu tiên tuân theo quy tắc chung của Spring Boot: tham số dòng lệnh > biến môi trường > file cấu hình.
Tham số dòng lệnh:
java -jar shop-service.jar --spring.profiles.active=prod
System property của JVM:
java -Dspring.profiles.active=prod -jar shop-service.jar
Biến môi trường, cách phổ biến nhất trên Docker và Kubernetes:
export SPRING_PROFILES_ACTIVE=prod
java -jar shop-service.jar
Khi chạy bằng Maven plugin:
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
Bằng code, hữu ích khi bạn muốn luôn thêm một profile nhất định:
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ShopApplication.class);
app.setAdditionalProfiles("metrics");
app.run(args);
}
}
setAdditionalProfiles thêm profile vào danh sách, không thay thế profile đã kích hoạt từ bên ngoài. Chạy với --spring.profiles.active=prod thì cả prod và metrics đều active.
Bạn có thể kích hoạt nhiều profile cùng lúc bằng dấu phẩy: --spring.profiles.active=prod,eu. Nếu hai file cùng định nghĩa một key, profile đứng sau thắng. Ở ví dụ này, application-eu.yml ghi đè application-prod.yml.
Khi ứng dụng khởi động, log cho bạn biết profile nào đang chạy:
INFO c.e.shop.ShopApplication : The following 1 profile is active: "prod"
Nếu không có profile nào và cũng không đặt spring.profiles.default, bạn sẽ thấy:
INFO c.e.shop.ShopApplication : No active profile set, falling back to 1 default profile: "default"
Hãy luôn liếc dòng log này khi deploy. Đây là cách nhanh nhất để phát hiện server chạy nhầm profile.
5. Bật/tắt bean bằng @Profile
Cấu hình khác nhau mới là một nửa vấn đề. Đôi khi bạn cần cả implementation khác nhau. Ví dụ: dev chỉ in email ra console, test lưu vào bộ nhớ để kiểm tra, prod gửi qua SMTP.
Định nghĩa interface chung:
public interface NotificationService {
void send(String to, String message);
}
Implementation cho dev:
@Service
@Profile("dev")
public class ConsoleNotificationService implements NotificationService {
@Override
public void send(String to, String message) {
System.out.println("[DEV] Gửi tới " + to + ": " + message);
}
}
Implementation cho test, lưu lại tin nhắn để assert:
@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);
}
}
Implementation cho 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) {
// Thực tế sẽ dùng JavaMailSender; ở đây chỉ minh họa
System.out.println("[SMTP " + host + "] Gửi tới " + to);
}
}
Class sử dụng không cần biết đang ở môi trường nào:
@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, "Đơn hàng #" + orderId + " đã được tạo");
}
}
Tại một thời điểm, chỉ một trong ba bean NotificationService được tạo. Spring inject đúng bean đó vào OrderService. Chạy với profile dev và gọi placeOrder("an@shop.vn", 42), bạn nhận được:
[DEV] Gửi tới an@shop.vn: Đơn hàng #42 đã được tạo
5.1. Biểu thức profile
@Profile hỗ trợ biểu thức logic:
@Configuration
public class SecurityConfig {
@Bean
@Profile("!prod")
public CommandLineRunner seedDemoData() {
return args -> System.out.println("Nạp dữ liệu demo...");
}
@Bean
@Profile("dev | test")
public CommandLineRunner relaxedCors() {
return args -> System.out.println("CORS mở cho mọi origin");
}
}
!prod: tạo bean ở mọi profile trừprod.dev | test: tạo khidevhoặctestactive. Có cả&cho phép “và”, ví dụprod & eu.
@Profile đặt được trên class @Component/@Configuration hoặc trên từng method @Bean. Đặt trên @Configuration thì toàn bộ bean bên trong cùng chịu điều kiện đó.
5.2. Kiểm tra profile trong code
Đôi khi bạn cần rẽ nhánh trong logic. Hãy dùng 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("Đang chạy PRODUCTION");
}
}
}
Dùng cách này một cách tiết kiệm. Nếu code đầy if (prod), bạn nên tách thành các bean riêng với @Profile.
6. Profile group
Khi hệ thống lớn dần, bạn có thể muốn chia nhỏ cấu hình, chẳng hạn proddb cho database và prodmq cho message broker. Profile group cho phép kích hoạt cả cụm bằng một tên:
# application.yml
spring:
profiles:
group:
prod: proddb, prodmq
dev: devdb
Giờ chạy --spring.profiles.active=prod sẽ kích hoạt prod, proddb và prodmq. Spring nạp lần lượt application-prod.yml, application-proddb.yml, application-prodmq.yml. Người vận hành chỉ cần nhớ một tên.
7. Viết test với @ActiveProfiles
Trong test, bạn chọn profile bằng @ActiveProfiles. Test dưới đây kiểm tra hai điều: đúng bean được inject, và cấu hình từ application-test.yml có hiệu lực.
@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|Đơn hàng #7 đã được tạo");
}
@Test
void shouldOverrideMailHostFromTestProfile() {
assertThat(mailHost).isEqualTo("fake-smtp");
}
}
Giải thích:
@ActiveProfiles("test")kích hoạt profiletestcho context của class này, bất kểspring.profiles.defaultlà gì.- Test đầu xác nhận Spring chọn
InMemoryNotificationServicethay vì gửi email thật. - Test thứ hai dùng bean in-memory để kiểm tra nội dung tin nhắn mà không cần mock.
- Test thứ ba chứng minh
app.mail.hosttừapplication-test.ymlđã ghi đè giá trịlocalhosttrongapplication.yml.
Lưu ý: Mỗi tổ hợp profile khác nhau tạo ra một Spring context riêng. Nếu mỗi class test dùng một bộ profile khác, cache context không tái sử dụng được và test suite chạy chậm hẳn. Hãy giữ số tổ hợp profile trong test ở mức tối thiểu.
8. Lỗi hay gặp
Đặt spring.profiles.active trong file theo profile. Từ Spring Boot 2.4, khai báo spring.profiles.active hoặc spring.profiles.include trong application-dev.yml (hoặc trong document có on-profile) sẽ làm ứng dụng không khởi động được:
InvalidConfigDataPropertyException: Property 'spring.profiles.active' imported from
location 'class path resource [application-dev.yml]' is invalid in a profile specific resource
Hãy dùng profile group (mục 6) thay thế.
Hard-code spring.profiles.active=dev trong application.yml. File này được đóng gói vào jar. Nếu ai đó quên truyền biến môi trường trên server, prod sẽ chạy bằng cấu hình dev. Hãy dùng spring.profiles.default cho môi trường local, và luôn kích hoạt profile một cách tường minh ở server.
Để bí mật trong application-prod.yml. Mật khẩu database hay API key không nên nằm trong Git. Hãy dùng placeholder như ${DB_PASSWORD} và cung cấp giá trị qua biến môi trường hoặc secret manager.
Không có bean nào khớp profile. Nếu bạn chạy với profile staging mà chỉ có implementation cho dev, test, prod, ứng dụng sẽ báo lỗi:
Parameter 0 of constructor in OrderService required a bean of type
'NotificationService' that could not be found.
Hãy cân nhắc một bean dự phòng với @Profile("!prod") hoặc thêm implementation cho profile mới.
Lạm dụng profile cho feature flag. Profile phù hợp để mô tả môi trường. Nếu bạn muốn bật/tắt một tính năng độc lập với môi trường, @ConditionalOnProperty thường rõ ràng hơn:
@Bean
@ConditionalOnProperty(name = "app.feature.loyalty-points", havingValue = "true")
public LoyaltyService loyaltyService() {
return new LoyaltyService();
}
Khi bạn có các profile kiểu prod-with-loyalty hay dev-no-cache, đó là dấu hiệu nên chuyển sang property.
9. Kết luận
Profile giúp bạn tách biệt cấu hình và hành vi theo từng môi trường mà không phải sửa code. Bạn đã thấy cách dùng application-{profile}.yml để ghi đè cấu hình, @Profile để chọn implementation, profile group để gom nhiều profile, và @ActiveProfiles để test.
Một vài nguyên tắc nên nhớ: đặt cấu hình chung trong application.yml, chỉ ghi phần khác biệt vào file profile. Kích hoạt profile ở server qua biến môi trường hoặc tham số dòng lệnh. Không đưa bí mật vào Git. Và luôn kiểm tra dòng log The following profiles are active sau mỗi lần deploy.