PropertySource ordering & relax binding — thứ tự ưu tiên và K8s env var
Boot xếp chồng nhiều nguồn config theo thứ tự ưu tiên cố định. Bài này bóc cơ chế PropertySource ordering (command line uu tien cao nhat, tại sao), 12-factor externalize config, và relax binding (DB_URL map sang db.url mà không cần config thêm) — nền tảng để deploy app trên K8s/Docker không hardcode.
TL;DR: Spring Boot xếp chồng nhiều PropertySource theo thứ tự ưu tiên cố định — command line args ưu tiên cao nhất, rồi env var, rồi file application-{profile}.yml, cuối cùng là application.yml mặc định. Khi resolve ${db.url}, Boot tra từ nguồn cao đến thấp, lấy giá trị đầu tiên tìm được. Relax binding tự động map SPRING_DATASOURCE_URL (UPPER_SNAKE_CASE của K8s) thành spring.datasource.url — không cần config thêm. Command line được ưu tiên cao nhất vì nó là override cuối cùng của operator, không phải developer. Bài này giải thích cơ chế bên dưới hai tính năng đó để bạn debug cấu hình production chính xác, không đoán mò.
Bài Environment & PropertySource đã giới thiệu Environment là lớp trừu tượng tập trung mọi nguồn config và cơ chế PropertySourcesPropertyResolver. Bài này đi sâu hơn vào ứng dụng thực tế: tại sao thứ tự ưu tiên được thiết kế như vậy, làm sao relax binding hoạt động, và pattern chuẩn cho K8s/Docker deployment.
1. Vấn đề: config hardcode và 12-factor App
Spring 4 era, cấu hình thường nằm trong code:
// Thoi Spring 4 — hardcode moi truong
DataSource ds = new HikariDataSource(
"jdbc:postgresql://localhost/dev",
"dev_user",
"dev_pass"
);
Khi cần deploy lên production: sửa code → rebuild → redeploy. Ba vấn đề rõ ràng:
- Build per env: dev/staging/prod khác DB URL → phải build 3 jar khác nhau. Không reproducible — jar chạy ở staging không phải jar đã test ở dev.
- Secret leak: password trong source code → commit git → security incident. Repository scanner như GitGuardian detect và alert.
- Override khó: muốn thử một setting khác cho một lần debug → phải sửa code, rebuild, redeploy.
12-factor App (methodology từ Heroku, 2011) — factor III ("Config") nói: lưu config trong môi trường, không trong code. Config là thứ thay đổi giữa các deployment (dev/staging/prod); code thì không. Hệ quả: một artifact jar duy nhất chạy mọi môi trường, config inject từ bên ngoài.
# Cung 1 jar, config khac nhau theo moi truong:
java -jar app.jar --spring.profiles.active=dev # dev
DB_URL=jdbc:postgresql://prod/app java -jar app.jar # prod
java -jar app.jar --db.url=jdbc:postgresql://test/app # smoke test
Spring Boot hiện thực 12-factor Config qua PropertySource — nhiều nguồn config xếp chồng theo thứ tự ưu tiên.
2. PropertySource ordering — thứ tự ưu tiên từ cao xuống thấp
Environment của Spring gom nhiều PropertySource lại và tra theo thứ tự cố định. Khi một property xuất hiện ở nhiều nguồn, nguồn ưu tiên cao hơn thắng.
Thứ tự đó, từ cao xuống thấp:
| Nguồn | Ví dụ | Khi dùng |
|---|---|---|
| Command line args | --db.url=jdbc:... | Ad-hoc override, smoke test, debug |
| Java System Properties | -Ddb.url=jdbc:... | JVM-level override, legacy tooling |
| OS env var | DB_URL=jdbc:... | K8s/Docker — standard injection |
| Profile-specific file | application-prod.yml | Config khác nhau giữa môi trường |
| Default file | application.yml | Base config + dev defaults |
@PropertySource | @PropertySource("classpath:extra.properties") | Config bổ sung từ file custom |
| Default properties | app.setDefaultProperties(...) | Fallback trong code framework |
Quy tắc nhớ: càng "ngoài" (do operator quyết định lúc chạy) → càng cao priority. application.yml nằm trong jar (do developer quyết định lúc build) — ưu tiên thấp nhất trong nhóm thực tế.
2.1 Cơ chế bên dưới — PropertySourcesPropertyResolver
Giả sử bốn nguồn cùng set db.url, mỗi nơi một giá trị:

Class PropertySourcesPropertyResolver (trong spring-core) duyệt danh sách MutablePropertySources theo thứ tự, gọi PropertySource.getProperty(name) trên từng cái, trả về giá trị đầu tiên khác null — rồi dừng. Nó không gom mọi nguồn lại rồi chọn, cũng không cảnh báo rằng có ba nguồn khác cũng khai key này. Khi bạn thêm CommandLinePropertySource (tự động khi dùng SpringApplication.run()), nó được thêm vào đầu danh sách — do đó luôn thắng.
Hệ quả: một giá trị "không có tác dụng" hầu như luôn là chuyện vị trí, không phải cú pháp — env var của bạn vẫn đúng, chỉ là có tầng cao hơn đã trả lời trước.
Bạn có thể quan sát trực tiếp tại runtime qua /actuator/env:
{
"name": "db.url",
"property": {
"value": "jdbc:postgresql://prod/app",
"origin": "System Environment Property \"DB_URL\""
}
}
Dòng origin cho biết chính xác PropertySource nào win — công cụ debug mạnh nhất khi "tại sao config của tôi không có tác dụng".
3. Tại sao command line được ưu tiên cao nhất
Câu hỏi hợp lý: tại sao không đặt env var ưu tiên cao nhất? Lý do thiết kế:
Command line là override cuối cùng của operator tại thời điểm chạy. Khi bạn chạy:
java -jar app.jar --db.url=jdbc:postgresql://canary/app
Đây là lệnh tường minh, explicit — operator/SRE đang nói rõ "tôi muốn dùng giá trị này cho lần chạy này". Nó không nên bị override bởi bất kỳ thứ gì đã được set sẵn trong môi trường.
Ngược lại, env var thường được set ở cấp deployment (K8s manifest, Docker compose) — không phải cho từng lần chạy cụ thể. Command line cho phép override env var mà không cần sửa manifest. Ứng dụng thực tế:
# CI/CD standard: env var set trong K8s manifest
DB_URL=jdbc:postgresql://prod/app
# SRE can test 1 lan voi DB backup ma khong sua manifest:
kubectl exec pod -- java -jar app.jar --db.url=jdbc:postgresql://backup/app
Nếu env var ưu tiên cao hơn command line, pattern này không hoạt động được.
Java System Properties (-D...) nằm dưới command line cũng có lý: system property set ở cấp JVM process, còn command line args là app-level explicit override. Thứ tự phản ánh "app config cụ thể hơn JVM config".
4. Relax binding — vì sao DB_URL map được sang db.url
K8s và Docker convention dùng UPPER_SNAKE_CASE cho env var (SPRING_DATASOURCE_URL, DB_MAX_POOL_SIZE). YAML dùng kebab-case (spring.datasource.url, db.max-pool-size). Hai format không match trực tiếp.
Spring Boot giải quyết bằng relax binding: Boot chuẩn hoá tên property về dạng canonical trước khi so sánh.
4.1 Quy tắc chuẩn hoá
Boot convert mọi tên property về dạng lowercase + không separator:
spring.datasource.url → springdatasourceurl
SPRING_DATASOURCE_URL → springdatasourceurl
spring_datasource_url → springdatasourceurl
spring.datasource-url → springdatasourceurl
Tất cả đều cùng một canonical form → match với nhau. Quy tắc canonical form được implement trong SpringConfigurationPropertySource và RelaxedPropertyResolver.
// Simplified logic trong RelaxedNames:
static String canonicalize(String name) {
return name.toLowerCase()
.replace("-", "")
.replace("_", "")
.replace(".", "");
}
Canonical form là thước đo duy nhất: khớp thì bind, lệch một segment thì không — và Boot không kêu tiếng nào ở trường hợp thứ hai.

Nửa dưới của hình là ca hay gặp nhất: env var trông rất hợp lý nhưng thiếu một segment so với tên property Boot thật sự dùng — không exception, không log, property chỉ giữ nguyên mặc định.
4.2 Mapping thực tế K8s/Docker
# K8s deployment.yaml — env var UPPER_SNAKE_CASE
spec:
containers:
- name: app
env:
- name: SPRING_DATASOURCE_URL # → spring.datasource.url
value: jdbc:postgresql://prod/app
- name: SPRING_DATASOURCE_USERNAME # → spring.datasource.username
value: app_prod
- name: DB_PASSWORD # → db.password
valueFrom:
secretKeyRef:
name: db-secret
key: password
- name: SPRING_PROFILES_ACTIVE # → spring.profiles.active
value: prod
# application.yml — kebab-case canonical
spring:
datasource:
url: jdbc:postgresql://localhost/dev # default dev
username: dev_user
password: ${DB_PASSWORD} # required, khong co default
profiles:
active: dev
Boot resolve spring.datasource.url:
- Check command line — không có.
- Check env var
SPRING_DATASOURCE_URL(canonical:springdatasourceurl) — matchspring.datasource.url(cùng canonical form), nên Boot trả về giá trị prod.
Không cần config thêm gì. Relax binding là tính năng "zero-config" cho K8s.
4.3 Tại sao relax binding tồn tại — lý do thực tiễn
Bash/Zsh không cho phép . hoặc - trong tên env var (chỉ accept [A-Za-z_][A-Za-z0-9_]*). Nếu Boot yêu cầu env var match chính xác spring.datasource.url, bạn không thể set env var đó trong shell hoặc K8s YAML một cách thông thường.
UPPER_SNAKE_CASE (SPRING_DATASOURCE_URL) là format hợp lệ trong mọi shell, Docker, K8s, CI/CD. Relax binding là cầu nối giữa YAML convention (kebab, hierarchical) và shell/container convention (UPPER_SNAKE) — không cần user config gì, Boot tự lo.
Bật management.endpoints.web.exposure.include=env và check /actuator/env. Response chỉ ra từng property đến từ nguồn nào, ví dụ origin: "System Environment Property SPRING_DATASOURCE_URL" — xác nhận relax binding đã map đúng.
5. Pattern production chuẩn 2026
Kết hợp PropertySource ordering và relax binding, pattern deployment chuẩn:
application.yml ← base defaults + dev config (commit git)
application-prod.yml ← prod-specific non-secret (commit git)
K8s env var ← secrets + runtime override (không commit)
Command line ← ad-hoc override (debug, canary)
# application.yml — commit git, base defaults
spring:
datasource:
url: jdbc:postgresql://localhost/dev
username: dev_user
password: ${DB_PASSWORD:dev_pass} # fallback cho local dev
app:
max-orders: 100
allowed-origins:
- http://localhost:3000
# application-prod.yml — commit git, prod non-secret
spring:
datasource:
url: jdbc:postgresql://prod-db.internal:5432/app
username: app_prod
password: ${DB_PASSWORD} # required, khong co default
jpa:
show-sql: false
app:
max-orders: 5000
allowed-origins:
- https://olhub.org
- https://www.olhub.org
# K8s manifest — secrets inject qua env var
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
Boot resolve db.password cho request production:
- Command line — không.
- Env var
DB_PASSWORD— có (K8s Secret inject) → win.
application-prod.yml được load (vì SPRING_PROFILES_ACTIVE=prod) nhưng db.password ở đó chỉ là ${DB_PASSWORD} — Boot tiếp tục resolve placeholder này từ env var. Đây là placeholder resolution trong YAML, không phải PropertySource priority — cả hai cơ chế phối hợp nhau.
6. Pitfall phổ biến
Pitfall 1 — "env var không có tác dụng" khi debug:
# Dat env var nhu nay, app van dung application.yml:
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar --spring.profiles.active=dev # command line override env var
Command line ưu tiên cao hơn env var. Nếu CI/CD script truyền --spring.profiles.active=dev hardcoded, env var SPRING_PROFILES_ACTIVE bị override. Debug bằng /actuator/env để xem nguồn nào win.
Pitfall 2 — thiếu một segment trong tên env var:
# SAI — thieu segment HIKARI:
export SPRING_DATASOURCE_MAXIMUMPOOLSIZE=20
# DUNG — du segment, moi . hoac - thanh mot _:
export SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE=20
Property thật là spring.datasource.hikari.maximum-pool-size (canonical springdatasourcehikarimaximumpoolsize); env var thiếu HIKARI cho canonical khác nên không bind — đúng nửa dưới của hình ở mục 4.1. Riêng dấu _ ở ranh giới từ chỉ là convention cho dễ đọc: canonical bỏ hết separator nên khi đã đủ segment thì ..._MAXIMUM_POOL_SIZE và ..._MAXIMUMPOOLSIZE là một.
Pitfall 3 — hardcode secret trong YAML:
# SAI — commit git = security incident
spring:
datasource:
password: prod-secret-2026
Luôn dùng placeholder ${DB_PASSWORD} và inject qua env var hoặc K8s Secret. Repository scanner (GitHub Secret Scanning) detect password pattern trong git commit.
@PropertySource("classpath:custom.yml") không hoạt động cho file YAML — annotation này chỉ hỗ trợ .properties format mặc định. Để load YAML custom, cần implement PropertySourceFactory (viết riêng) hoặc dùng spring.config.import thay thế.
Liên hệ các bài khác
Bài này là một mảnh của bức tranh config rộng hơn trong course:
- Environment & PropertySource: cơ chế
Environmentinterface,PropertySourcesPropertyResolver, và cách@Value("${...}")resolve — bài này build on top của foundation đó. - @ConfigurationProperties: bước tiếp theo từ bài này — thay vì inject từng property bằng
@Value, bind cả nhóm property vào một Java record type-safe với validation. Hiểu PropertySource ordering ở đây giúp debug khi@ConfigurationPropertieskhông bind đúng giá trị. - Profiles — activation và bean-level:
application-prod.ymltrong bài này activate qua profile — bài đó đào sâu cơ chế@Profile, profile groups, và bean conditional dựa trên profile.
Tóm tắt
- 12-factor Config — config sống ngoài artifact, inject tại runtime. Một jar chạy mọi môi trường.
- PropertySource ordering — Boot xếp chồng nguồn config theo thứ tự ưu tiên cố định: command line ưu tiên cao nhất, rồi env var, rồi profile-specific file, rồi
application.yml, rồi default. - Cơ chế bên dưới —
PropertySourcesPropertyResolverduyệt danh sáchMutablePropertySourcestheo thứ tự, trả giá trị đầu tiên khác null. - Command line ưu tiên cao nhất vì nó là override explicit của operator tại thời điểm chạy — không bị override bởi env var đã set sẵn trong deployment manifest.
- Relax binding — Boot chuẩn hoá tên property về lowercase + no separator trước khi so sánh.
SPRING_DATASOURCE_URLmap sangspring.datasource.urltự động, không cần config. - Relax binding tồn tại vì shell/container convention dùng UPPER_SNAKE, còn YAML convention dùng kebab. Không cần user config, Boot tự lo cầu nối.
- Production pattern — non-secret trong git (
application.yml,application-prod.yml); secret qua K8s env var hoặc Vault. Không bao giờ commit password. - Debug tool —
/actuator/envcho thấy chính xác PropertySource nào win cho từng property.
Tự kiểm tra
- Q1App có 4 nguồn cùng set property
db.url: (1)application.ymlcó giá trịlocalhost, (2) env varDB_URL=staging, (3) JVM arg-Ddb.url=prod1, (4) command line--db.url=prod2. Giá trị nào win? Giải thích theo thứ tự PropertySource. - Q2Tại sao command line args được thiết kế có ưu tiên cao hơn env var, thay vì ngược lại? Cho ví dụ thực tế trong K8s deployment.
- Q3K8s deployment set env var
SPRING_DATASOURCE_URL=jdbc:postgresql://prod/app. App dùng@Value("${spring.datasource.url}"). Cơ chế nào cho phép env var match property name? Viết lại canonical form của cả hai. - Q4Bạn set env var
SPRING_DATASOURCE_MAXIMUMPOOLSIZE=20nhưngspring.datasource.hikari.maximum-pool-sizekhông binding đúng. Vấn đề ở đâu? Env var đúng nên đặt là gì? - Q5Mô tả production deployment pattern chuẩn cho một Spring Boot app: file nào commit git, secret inject thế nào, command line dùng cho mục đích gì? Vì sao không commit password vào
application-prod.yml?
Bài tiếp theo: @ConfigurationProperties vs @Value
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.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên