99 Ngày Spring — Ngày 08: Cấu hình với properties & YAML
Ngày 07 ta hẹn đưa những con số hard-code ra khỏi code — hôm nay thực hiện. URL database, cổng server, kích thước trang… đổi theo môi trường, mà code thì không nên compile lại chỉ vì đổi một con số. Spring Boot giải quyết bằng cấu hình ngoại: khai báo trong application.yml, đọc bằng @Value hoặc gom nhóm type-safe bằng @ConfigurationProperties, và relaxed binding để biến môi trường ghi đè tất cả lúc lên production.

Vì sao phải đưa cấu hình ra ngoài?
Một bản build — nhiều môi trường: cùng file JAR chạy dev, staging, production; chỉ cấu hình khác nhau.
Bí mật không nằm trong code: mật khẩu database mà nằm trong Git là tai nạn chờ ngày xảy ra.
Đổi không cần dev: vận hành chỉnh timeout, bật tắt tính năng — không chờ build mới.
Spring Boot tự tìm application.properties / application.yml trong src/main/resources — đây là "nhà" của mọi cấu hình, và cũng chính là file bạn đã thấy Boot tự đọc từ Ngày 02.
application.properties vs application.yml
# application.properties — phẳng, mỗi dòng một keyapp.name=Mot Dev Blogapp.page-size=20app.mail.from=hello@motdev.vn
# application.yml — cùng nội dung, cấu trúc cây, đỡ lặp prefixapp:name: Mot Dev Blogpage-size: 20 # kebab-case là chuẩn trong file cấu hìnhmail:
from: hello@motdev.vn
retry: 3Hai định dạng tương đương — chọn một và thống nhất cả team. Series này dùng YAML: cấu trúc cây đọc thoáng hơn khi cấu hình phình to, đỡ lặp prefix. Đổi lại YAML nhạy cảm thụt lề — thụt sai một cấp là key rơi nhầm nhóm, app đọc thiếu cấu hình mà không báo lỗi rõ ràng. Còn key của chính bạn (như app.page-size) nên đặt kebab-case — lý do nằm ở mục relaxed binding bên dưới.
@Value — đọc một giá trị đơn lẻ
@Servicepublic class PostService {
private final int pageSize;
// Đọc MỘT giá trị — sau dấu ':' là mặc định khi key không tồn tại
public PostService(@Value("${app.page-size:10}") int pageSize) {
this.pageSize = pageSize; // yml có 20 → nhận 20
}
}
// Thiếu key mà KHÔNG có mặc định → app từ chối khởi động (fail fast — điều tốt!)// Spring tự ép kiểu: "20" → int, "true" → boolean, "10s" → DurationCú pháp ${key:mặc-định} tiêm thẳng vào tham số constructor — đúng tinh thần constructor injection Ngày 04, field vẫn final. @Value hợp với một hai giá trị lẻ; nhưng khi cấu hình thành cụm (mail có from, retry, timeout…), rải @Value khắp nơi là chuỗi ký tự lặp đi lặp lại, gõ sai key chỉ phát hiện lúc chạy — lúc đó hãy nâng cấp:
@ConfigurationProperties — gom nhóm type-safe
// Gom cả nhóm app.* vào MỘT object có kiểu — cách được khuyên dùng@ConfigurationProperties(prefix = "app")public record AppProperties(
String name,
int pageSize, // ← tự khớp app.page-size (relaxed binding)
Mail mail // ← nhóm lồng nhau khớp app.mail.*
) {
public record Mail(String from, int retry) { }
}
// Bật quét — một lần cho cả app:@SpringBootApplication@ConfigurationPropertiesScanpublic class BlogApplication { }
// Rồi tiêm như mọi bean khác (constructor injection — Ngày 04):@Servicepublic class MailService {
private final AppProperties props;
public MailService(AppProperties props) { this.props = props; }
}Type-safe: gõ
props.pageSize()có IDE gợi ý và compiler soát lưng — không còn chuỗi"app.page-size"rải rác chờ gõ nhầm.Record (Java 21) cho cấu hình bất biến — Boot 3 bind thẳng qua constructor, không cần setter.
Thêm
@Validated+@Min/@NotBlanklà cấu hình sai bị chặn ngay lúc khởi động — fail fast thay vì lỗi ngầm giữa đêm.Chính các starter cũng ăn cấu hình kiểu này —
server.port,spring.datasource.*đều là@ConfigurationPropertiescủa Boot; giờ bạn hiểu cơ chế sau lưng chúng.
Relaxed binding & biến môi trường
Vì sao khuyên kebab-case? Vì Spring Boot bind lỏng — cùng một thuộc tính, nhiều cách viết đều khớp:
# Cùng MỘT thuộc tính pageSize — relaxed binding chấp nhận mọi "chính tả":app.page-size=20 # kebab-case (chuẩn trong file)app.pageSize=20 # camelCaseapp.page_size=20 # snake_caseAPP_PAGESIZE=20 # BIẾN MÔI TRƯỜNG — viết hoa, '.' thành '_', bỏ '-'
# Nguồn sau ĐÈ nguồn trước (trích chuỗi ưu tiên của Spring Boot):# application.yml < biến môi trường < tham số dòng lệnh# → production đổi cấu hình bằng env var: KHÔNG sửa file, KHÔNG build lạiMảnh ghép cuối cùng của bức tranh 12-factor: file yml giữ mặc định an toàn cho dev, còn production ghi đè bằng biến môi trường — APP_PAGESIZE=50 là xong, không đụng vào artifact đã build. Bí mật (mật khẩu DB, API key) chỉ đi đường env var hoặc secret manager, tuyệt đối không commit. Đây cũng chính là cách blog bạn đang đọc nhận DATABASE_URL trên hosting đấy.
Bài tập nhỏ
Thêm nhóm
app.greeting(message, times) vàoapplication.yml, đọc bằng@Valuevà in ra khi khởi động (gợi ý:@PostConstructcủa Ngày 07).Nâng cấp nhóm đó thành record
@ConfigurationProperties— xóa dần từng@Valuevà cảm nhận khác biệt khi refactor.Ghi đè bằng biến môi trường: đặt
APP_GREETING_TIMES=5rồi chạy lại — xác nhận env var thắng file yml.Cố tình xóa một key không có mặc định — đọc kỹ thông báo lỗi lúc khởi động để quen mặt nó.
Kết luận
Cấu hình đã ra khỏi code đúng nghĩa: application.yml làm nhà, @Value cho giá trị lẻ, @ConfigurationProperties + record cho nhóm type-safe, relaxed binding nối tới biến môi trường ở production. Nhưng khoan — dev cần H2, production cần PostgreSQL, config cả cụm phải đổi theo môi trường thì sao? Ngày 09: Profiles — bộ công tắc môi trường của Spring. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


