Hình dung việc cấu hình như một chuỗi mệnh lệnh trong quân đội. Cùng một thuộc tính có thể bị nhiều nguồn cùng "hô" một lúc: tham số dòng lệnh, biến môi trường, application.yml, giá trị mặc định trong mã. Khi hai lệnh mâu thuẫn, Spring không bối rối — nó nghe theo cấp cao nhất. Tham số dòng lệnh là vị chỉ huy, biến môi trường là sĩ quan, application.yml là lính. Gần như mọi lần bạn "đặt giá trị mà nó không chịu đổi" là vì một cấp cao hơn đang lặng lẽ đè lên. Và cái hay là bạn in được toàn bộ bảng cấp bậc đó ra từ chính ứng dụng — hết phải đoán. Spring Boot đọc cấu hình từ hơn mười nguồn; bài này về thứ tự giữa chúng.
application.yml
app:
ten: "dịch vụ đơn hàng"
cong: 8080
han-cho: 30s
YAML hoặc .properties đều được. YAML gọn hơn cho cấu hình phân cấp; properties ít cạm bẫy cú pháp hơn.
Quy ước tên: gạch nối, viết thường (han-cho). Spring có cơ chế "relaxed binding" nên hanCho, han_cho, HAN_CHO đều ánh xạ về cùng một thuộc tính — điều này quan trọng vì biến môi trường không dùng được gạch nối.
Thứ tự ưu tiên, in ra thật
1. server.ports
2. configurationProperties
3. servletConfigInitParams
4. servletContextInitParams
5. systemProperties (-D...)
6. systemEnvironment (biến môi trường)
7. random
8. application.yml
Danh sách này in bằng ((ConfigurableEnvironment) env).getPropertySources() — nguồn đứng trước thắng. Đây đúng là bảng cấp bậc, đọc từ chỉ huy xuống lính.
Cộng thêm hai nguồn không hiện trong danh sách trên vì tôi không dùng: tham số dòng lệnh (--app.ten=x) đứng gần đầu, và application-{profile}.yml đứng ngay trên application.yml.
Kiểm bằng thực nghiệm:
APP_TEN="từ biến môi trường" java -jar app.jar --spring.profiles.active=prod
app.ten = từ biến môi trường <- biến môi trường thắng
app.cong = 9090 <- từ application-prod.yml
app.ten có ở cả ba nơi (application.yml, application-prod.yml, biến môi trường) và biến môi trường thắng. app.cong không có trong biến môi trường nên giá trị của profile thắng file gốc.
Chú ý cách viết biến môi trường: APP_TEN ánh xạ về app.ten — chữ hoa, gạch dưới thay dấu chấm.
Profile
java -jar app.jar --spring.profiles.active=prod
SPRING_PROFILES_ACTIVE=prod java -jar app.jar
=== PROFILE ĐANG BẬT ===
[prod]
Profile làm hai việc:
Nạp thêm application-{profile}.yml, đè lên application.yml. Đây là hợp nhất, không phải thay thế — thuộc tính không khai lại vẫn giữ giá trị gốc.
Bật tắt bean:
@Component
@Profile("prod")
class GuiMailThat implements GuiMail { }
@Component
@Profile("!prod")
class GuiMailGia implements GuiMail { }
Đây là cách gọn nhất để thay cài đặt theo môi trường, và nó ghép với @Primary cùng @Qualifier ở bài 4.
Bật nhiều profile cùng lúc được: --spring.profiles.active=prod,asia. Cái sau đè cái trước.
Từ Boot 2.4, có thể gộp profile vào một tệp bằng --- và spring.config.activate.on-profile — tiện cho dự án nhỏ, nhưng tệp riêng vẫn dễ đọc hơn khi cấu hình dài.
Chọn nguồn cho từng loại giá trị
Đây là phần thực dụng nhất — giao đúng việc cho đúng cấp:
Vào application.yml: giá trị mặc định hợp lý cho môi trường phát triển, thứ hiếm khi đổi, cấu trúc cấu hình.
Vào application-prod.yml: khác biệt theo môi trường mà không nhạy cảm — mức log, kích thước pool, tên dịch vụ.
Vào biến môi trường: mọi thứ khác nhau giữa các lần triển khai — địa chỉ CSDL, cờ tính năng.
Vào kho bí mật: mật khẩu, khoá API, chứng chỉ. Đừng để trong tệp cấu hình, kể cả tệp không commit.
Nguyên tắc từ Twelve-Factor App: cấu hình khác nhau giữa các môi trường thì nằm ngoài mã, còn cấu trúc thì nằm trong.
@Value và giá trị mặc định
@Value("${app.ten}") String ten;
@Value("${app.khong-co:giá trị mặc định}") String thieu;
ten=từ application.yml | thiếu -> giá trị mặc định
Dấu hai chấm khai mặc định. Không có mặc định mà thuộc tính thiếu thì ứng dụng không khởi động được — và đó là hành vi đúng, giống nguyên tắc "chết ngay nếu thiếu" ở bài 56 sê-ri Go.
Nhưng @Value có ba nhược điểm, và bài mai sẽ nói về cách thay thế tốt hơn.
Đừng đọc cấu hình rải rác
@Value("${app.han-cho}") String hanCho; // ở lớp A
@Value("${app.han-cho}") String hanCho2; // ở lớp B
Cùng một thuộc tính khai ở nhiều nơi là chỗ để lệch nhau. Gom vào một lớp cấu hình — bài mai.
Bí mật
Đừng commit bí mật, kể cả trong application-dev.yml. Thêm application-local.yml vào .gitignore cho giá trị cá nhân.
Đừng log toàn bộ cấu hình. Endpoint /actuator/env lộ mọi thuộc tính — bài 10 sẽ nói cách khoá nó lại.
Spring Boot có che một phần: thuộc tính có tên chứa password, secret, key, token sẽ hiện thành ****** trong actuator. Nhưng nó dựa vào tên, nên app.mat-khau không được che.
Nếu muốn hết đoán về "cấp nào đang thắng", in thẳng bảng cấp bậc ra — ba mươi giây:
var src = ((ConfigurableEnvironment) env).getPropertySources();
src.forEach(s -> System.out.println(s.getName()));
Lần tới khi một giá trị "không chịu đổi", danh sách này cho biết nguồn nào đang đứng trên — thay vì ngồi thử từng chỗ.
Mẫu số chung
Mẫu "một cấu hình nền, rồi một lớp mỏng đè lên theo từng môi trường" không phải của Spring — nó là cách mọi framework hiện đại tách "chạy ở đâu" khỏi "chạy cái gì", chỉ khác tên gọi.
- .NET gần như sao chép y hệt:
appsettings.jsonlàm nền,appsettings.Production.jsonđè lên theoASPNETCORE_ENVIRONMENT— đúng cặpapplication.yml/application-prod.ymlcủa Spring. - Rails có
config/environments/production.rbchọn theoRAILS_ENV; Node thì rẽ nhánh theoNODE_ENV; Django đổi module settings theo môi trường. Cùng một ý: một bộ mặc định, vài lớp phủ mỏng.
Và hai lời cảnh báo đi kèm thì phổ quát, đáng mang theo hơn cả cú pháp. Một: giữ lớp phủ thật mỏng — mỗi profile, mỗi appsettings.{env}, mỗi nhánh NODE_ENV là một tổ hợp chưa được kiểm so với các tổ hợp khác; càng nhiều môi trường khác nhau, càng nhiều đường chạy không ai test tới. Hai, sắc hơn: đổi bean hay code theo môi trường nghĩa là production chạy thứ mà test của bạn chưa bao giờ chạm vào — đúng cái @Profile("prod") ở trên. Mục tiêu vàng (Twelve-Factor gọi là "dev/prod parity") là production chỉ khác dev ở giá trị cấu hình, không khác ở hành vi mã; một khi logic nghiệp vụ rẽ theo môi trường, bạn đã tự tạo ra một phiên bản chưa ai kiểm thử chạy ở đúng nơi quan trọng nhất. Sợi chỉ chung: cấu hình theo môi trường là để thay số, không phải để thay việc — và ở ngôn ngữ nào cũng vậy, cái overlay càng mỏng thì cái bạn test càng giống cái bạn chạy.
Ngày mai: @ConfigurationProperties — gom cấu hình thành đối tượng có kiểu.