PropertySource Ordering: Thứ Tự Ưu Tiên Cấu Hình Spring
Khám phá thứ tự ưu tiên các nguồn cấu hình PropertySource trong Spring Boot và cơ chế relax binding chuyển đổi biến môi trường K8s.
TL;DR: Spring Boot xếp chồng nhiều PropertySource theo thứ tự ưu tiên cố định: command line args đứng đầu, kế đến biến môi trường OS, file profile application-{profile}.yml, và cuối cùng là application.yml. Khi tìm khóa cấu hình, Spring tra từ trên xuống và lấy ngay giá trị đầu tiên tìm thấy. Tính năng relax binding tự động ánh xạ tên biến UPPER_SNAKE_CASE từ Kubernetes thành kebab-case trong file cấu hình mà không cần khai báo bổ sung.
Tại sao cần tách cấu hình khỏi mã nguồn theo 12-factor?
Trong các hệ thống Spring truyền thống, thông tin kết nối thường bị gán cứng vào mã nguồn:
// Hardcode connection parameters directly in code
DataSource ds = new HikariDataSource(
"jdbc:postgresql://localhost/dev",
"dev_user",
"dev_pass"
);
Cách làm này tạo ra ba rào cản nghiêm trọng trong vận hành:
- Không thể tái sử dụng artifact: Mỗi khi chuyển từ dev sang staging hay production, đội ngũ phải sửa mã nguồn và build lại file jar mới.
- Dễ lộ lọt bí mật hệ thống: Mật khẩu và khóa bí mật nằm trong git history rất dễ bị lộ lọt khi kho mã nguồn bị truy cập trái phép.
- Khó khăn khi ghi đè runtime: Muốn kiểm tra khẩn cấp một tham số tạm thời đòi hỏi toàn bộ quy trình build và deploy lặp lại từ đầu.
Nguyên lý 12-factor App (yếu tố III - Config) quy định: mọi thông số thay đổi giữa các môi trường triển khai phải được tách khỏi mã nguồn và nạp từ bên ngoài. Một file jar duy nhất phải vận hành được trên mọi máy chủ:
# Single artifact runs across multiple environments
java -jar app.jar --spring.profiles.active=dev
DB_URL=jdbc:postgresql://prod/app java -jar app.jar
Spring Boot hiện thực hóa nguyên lý này thông qua mô hình phân tầng PropertySource.
Spring Boot giải quyết xung đột thứ tự PropertySource như thế nhau?
Giao diện Environment của Spring quản lý danh sách các PropertySource theo một trật tự xác định. Khi cùng một khóa xuất hiện tại nhiều nơi, nguồn có độ ưu tiên cao hơn sẽ quyết định giá trị.
| Thứ tự | Nguồn cấu hình | Ví dụ cú pháp | Ngữ cảnh sử dụng |
|---|---|---|---|
| 1 (Cao nhất) | Command line arguments | --db.url=jdbc:... | Ghi đè tạm thời, smoke test của SRE |
| 2 | Java System Properties | -Ddb.url=jdbc:... | Thiết lập mức tiến trình JVM |
| 3 | OS Environment Variables | DB_URL=jdbc:... | Inject từ Kubernetes Secret / Docker |
| 4 | Profile-specific file | application-prod.yml | Cấu hình chuyên biệt từng môi trường |
| 5 (Thấp nhất) | Default application file | application.yml | Cấu hình gốc và giá trị mặc định cho local dev |
Quy tắc ghi nhớ: Nguồn càng gần thời điểm thực thi do người vận hành chỉ định thì độ ưu tiên càng cao.

Bộ xử lý PropertySourcesPropertyResolver duyệt qua danh sách nguồn tuần tự. Ngay khi tìm thấy giá trị hợp lệ đầu tiên, nó lập tức trả về kết quả mà không quét tiếp các tầng bên dưới. Bạn có thể kiểm tra nguồn gốc thắng cuộc tại runtime qua endpoint actuator:
{
"name": "db.url",
"property": {
"value": "jdbc:postgresql://prod/app",
"origin": "System Environment Property \"DB_URL\""
}
}
Tại sao command line args có quyền ưu tiên cao nhất?
Command line arguments được trao quyền cao nhất vì đây là quyết định can thiệp trực tiếp của kỹ sư vận hành tại thời điểm chạy ứng dụng:
java -jar app.jar --db.url=jdbc:postgresql://canary/app
Khi gặp sự cố khẩn cấp trên cụm Kubernetes, kỹ sư vận hành có thể cần trỏ tạm thời một Pod về cơ sở dữ liệu dự phòng mà không muốn cập nhật manifest của toàn bộ Deployment:
kubectl exec pod-app -- java -jar app.jar --db.url=jdbc:postgresql://backup/app
Nếu biến môi trường được ưu tiên hơn command line, kỹ sư sẽ bị tước quyền ghi đè tức thời này và buộc phải thay đổi tài nguyên Kubernetes chung, gây ảnh hưởng diện rộng.
Cơ chế relax binding chuyển đổi tên biến K8s ra sao?
Hệ điều hành Unix và các hệ thống container như Docker, Kubernetes quy ước biến môi trường ở dạng UPPER_SNAKE_CASE (SPRING_DATASOURCE_URL). Ngược lại, Spring Boot định nghĩa cấu hình file YAML theo phong cách kebab-case (spring.datasource.url).
Spring Boot giải quyết khác biệt này bằng cơ chế relax binding. Trước khi đối soát, hệ thống lược bỏ toàn bộ dấu chấm, dấu gạch nối, dấu gạch dưới và đưa chuỗi về dạng chữ thường duy nhất:
spring.datasource.url -> springdatasourceurl
SPRING_DATASOURCE_URL -> springdatasourceurl
spring_datasource_url -> springdatasourceurl

Nhờ dạng chuẩn hóa (canonical form) trùng khớp, Spring Boot tự động kết nối giá trị biến môi trường vào thuộc tính mà nhà phát triển không cần viết bất kỳ đoạn mã điều phối nào.
# K8s manifest maps to spring.datasource.url via relaxed binding
spec:
containers:
- name: app
env:
- name: SPRING_DATASOURCE_URL
value: jdbc:postgresql://prod/app
- name: SPRING_DATASOURCE_USERNAME
value: app_prod
Chiến lược quản trị cấu hình production chuẩn gồm những gì?
Một quy trình quản lý cấu hình đạt chuẩn an toàn bao gồm sự phối hợp giữa file mã nguồn và biến môi trường:
# application.yml: tracked in git, contains defaults for local dev
spring:
datasource:
url: jdbc:postgresql://localhost/dev
username: dev_user
password: <code>{'${DB_PASSWORD:dev_pass}'}</code>
# K8s deployment: Inject secrets from Secret store
spec:
containers:
- name: api
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
Trong mô hình này:
- Lưu trữ trong git: File
application.ymlchứa giá trị mặc định cho local dev vàapplication-prod.ymlchứa các tham số không nhạy cảm như pool size, timeout. - Cung cấp tại runtime: Nạp mật khẩu, access token và connection string qua biến môi trường lấy từ Kubernetes Secret hoặc HashiCorp Vault.
Những lỗi sai kinh điển nào thường gặp khi cấu hình môi trường?
Dưới đây là các tình huống khiến hệ thống không nhận cấu hình như kỳ vọng:
- Bị command line ghi đè ngầm: Đặt biến môi trường
SPRING_PROFILES_ACTIVE=prodnhưng script khởi động trong Dockerfile lại chứa cờ--spring.profiles.active=dev. Vì command line ưu tiên cao hơn, cấu hình production bị vô hiệu hóa. - Thiếu phân đoạn (segment) trong tên biến môi trường: Khai báo
SPRING_DATASOURCE_MAXIMUMPOOLSIZE=20sẽ không thể gắn vàospring.datasource.hikari.maximum-pool-sizevì thiếu mất từ khóaHIKARI. Tên chính xác phải làSPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE. - Khai báo file YAML với annotation
@PropertySource: Annotation@PropertySource("classpath:custom.yml")mặc định chỉ đọc file định dạng.properties. Muốn nạp YAML tùy biến, bạn phải định nghĩaPropertySourceFactoryriêng biệt.
Tóm tắt
- Nguyên lý 12-factor Config: Giữ mã nguồn độc lập với môi trường chạy; nạp thông số qua biến môi trường để một file jar dùng chung cho toàn bộ hạ tầng.
- Thứ tự phân tầng: Command line arguments > Java System Properties > OS Environment Variables > Profile YAML > Default YAML.
- Quy tắc giải quyết:
PropertySourcesPropertyResolverquét tuần tự từ trên xuống dưới và ngắt ngay khi tìm thấy giá trị hợp lệ đầu tiên. - Relax binding: Chuẩn hóa tên thuộc tính về dạng chữ thường không ngăn cách, giúp tương thích hoàn hảo giữa shell container và file YAML.
- Bảo mật: Tuyệt đối không commit thông tin nhạy cảm vào repository; luôn sử dụng placeholder kết hợp biến môi trường runtime.
Tự kiểm tra
- Q1Ứng dụng đồng thời nhận 4 nguồn cấu hình cho db.url: application.yml (localhost), OS env DB_URL (staging), JVM flag -Ddb.url (prod1), và command line --db.url (prod2). Giá trị nào sẽ được chọn?
- Q2Vì sao các tham số dòng lệnh (command line args) lại được thiết kế có quyền ưu tiên cao hơn biến môi trường OS?
- Q3Cơ chế nào giúp Spring Boot tự động nhận diện biến SPRING_DATASOURCE_URL và gán vào thuộc tính spring.datasource.url?
- Q4Để ghi đè thuộc tính spring.datasource.hikari.maximum-pool-size qua biến môi trường OS, tên biến chuẩn xác là gì?
- Q5Theo chuẩn quản trị cấu hình 12-factor, thông tin mật khẩu cơ sở dữ liệu production nên được quản lý như thế nào?
Bài tiếp theo: @ConfigurationProperties vs @Value
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Tổng Quan Module 06: Cấu Hình, Profile Và Logging
Bài tiếp@ConfigurationProperties vs @Value: Type-Safe Binding
Lộ trình Junior Java Backend Developer
Chặng 14/21 · Spring Boot & autoconfiguration — bài 10/16 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này