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.
flowchart TB
A["Command line args<br/>--db.url=..."]
B["Java System Properties<br/>-Ddb.url=..."]
C["OS Environment Variables<br/>DB_URL=..."]
D["application-prod.yml<br/>(profile-specific)"]
E["application.yml<br/>(default)"]
F["@PropertySource tren @Configuration"]
G["Default properties<br/>(setDefaultProperties)"]
A -->|"uu tien cao hon"| B
B --> C
C --> D
D --> E
E --> F
F --> G
style A fill:#fef3c7,stroke:#d97706
style C fill:#fef3c7,stroke:#d97706
style E fill:#d1fae5,stroke:#059669Giải thích từng tầng:
| 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
Khi Boot resolve ${db.url}, luồng thực tế:
flowchart TB
R["environment.getProperty('db.url')"]
PSR["PropertySourcesPropertyResolver<br/>duyet danh sach PropertySource theo thu tu"]
PS1{"CommandLinePropertySource<br/>co 'db.url' khong?"}
PS2{"SystemPropertiesPropertySource<br/>co 'db.url' khong?"}
PS3{"SystemEnvironmentPropertySource<br/>co 'DB_URL' / 'db_url' khong?"}
PS4{"ResourcePropertySource<br/>(application-prod.yml, application.yml)<br/>co 'db.url' khong?"}
RET["Return value dau tien tim thay"]
NULL["Return null hoac default value"]
R --> PSR
PSR --> PS1
PS1 -->|"CO"| RET
PS1 -->|"KHONG"| PS2
PS2 -->|"CO"| RET
PS2 -->|"KHONG"| PS3
PS3 -->|"CO"| RET
PS3 -->|"KHONG"| PS4
PS4 -->|"CO"| RET
PS4 -->|"KHONG"| NULLClass 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. 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.
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(".", "");
}
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 — nhầm UPPER_SNAKE_CASE mapping:
# SAI — khong match voi spring.datasource.maximum-pool-size:
export SPRING_DATASOURCE_MAXIMUMPOOLSIZE=20
# DUNG — thêm _ cho moi dau - hoac ranh gioi tu:
export SPRING_DATASOURCE_MAXIMUM_POOL_SIZE=20
Canonical form của spring.datasource.maximum-pool-size là springdatasourcemaximumpoolsize. Env var SPRING_DATASOURCE_MAXIMUM_POOL_SIZE (lowercase + remove _) = springdatasourcemaximumpoolsize — match. Env var SPRING_DATASOURCE_MAXIMUMPOOLSIZE (lowercase + remove _) = springdatasourcemaximumpoolsize — cũng match. Nhưng theo convention nên dùng _ để phân cách từ cho dễ đọc.
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.yml có giá trị localhost, (2) env var DB_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.▸
db.url: (1) application.yml có giá trị localhost, (2) env var DB_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.Win: command line arg → giá trị prod2.
Thứ tự ưu tiên từ cao đến thấp: (1) command line --key=value, (2) Java system properties -Dkey=value, (3) OS env var, (4) profile-specific file, (5) application.yml.
PropertySourcesPropertyResolver duyệt MutablePropertySources theo thứ tự. CommandLinePropertySource được thêm vào đầu danh sách khi SpringApplication.run() — nên luôn được tra trước.
Tìm thấy db.url ở CommandLinePropertySource, resolver trả về prod2 ngay, không tra tiếp. Ba nguồn còn lại bị bỏ qua.
Để verify: bật /actuator/env → tìm property db.url → dòng origin sẽ là Command line property "--db.url".
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.▸
Command line là override explicit và tức thời của operator tại thời điểm chạy cụ thể — nó phải thắng để có ý nghĩa. Env var thường được set ở cấp deployment manifest (K8s YAML, Docker Compose), áp dụng cho tất cả instance của service đó.
Ví dụ K8s thực tế: production có DB_URL=jdbc:postgresql://prod/app set trong deployment. SRE cần test một lần với DB backup mà không muốn sửa manifest (vì sẽ ảnh hưởng tất cả pod):
kubectl exec pod -- java -jar app.jar --db.url=jdbc:postgresql://backup/appNếu env var ưu tiên cao hơn command line, lệnh trên không có tác dụng — SRE buộc phải sửa manifest, apply lại, ảnh hưởng toàn bộ deployment chỉ để test một lần.
Command line ưu tiên cao nhất = operator có thể override bất kỳ thứ gì đã được set sẵn mà không cần thay đổi config hạ tầng.
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.▸
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.Cơ chế: relax binding trong SpringConfigurationPropertySource và RelaxedPropertyResolver. Boot chuẩn hoá tên property về lowercase + bỏ mọi separator trước khi so sánh.
Canonical form:
- Env var
SPRING_DATASOURCE_URL→ lowercase + remove_→springdatasourceurl - Property name
spring.datasource.url→ lowercase + remove.→springdatasourceurl
Cả hai canonical form đều là springdatasourceurl → match → Boot dùng giá trị từ env var.
Lý do relax binding tồn tại: Bash/shell không cho phép . hoặc - trong tên env var. UPPER_SNAKE_CASE là format hợp lệ trong mọi shell, Docker, K8s. Boot tự lo cầu nối giữa YAML convention (kebab-case, dấu chấm) và shell/container convention (UPPER_SNAKE) — không cần user config thêm gì.
Q4Bạn set env var SPRING_DATASOURCE_MAXIMUMPOOLSIZE=20 nhưng spring.datasource.hikari.maximum-pool-size không binding đúng. Vấn đề ở đâu? Env var đúng nên đặt là gì?▸
SPRING_DATASOURCE_MAXIMUMPOOLSIZE=20 nhưng spring.datasource.hikari.maximum-pool-size không binding đúng. Vấn đề ở đâu? Env var đúng nên đặt là gì?Vấn đề: env var SPRING_DATASOURCE_MAXIMUMPOOLSIZE không map sang spring.datasource.hikari.maximum-pool-size vì thiếu phần HIKARI trong tên.
Property đầy đủ có ba segment con: spring.datasource.hikari + tên thuộc tính maximum-pool-size. Env var thiếu hẳn segment HIKARI nên canonical hai bên không khớp.
Env var đúng cho spring.datasource.hikari.maximum-pool-size:
SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE=20Kiểm chứng bằng canonical form (bỏ mọi ., -, _ rồi lowercase): property spring.datasource.hikari.maximum-pool-size cho ra springdatasourcehikarimaximumpoolsize; env var SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE cũng cho ra springdatasourcehikarimaximumpoolsize. Hai canonical bằng nhau → bind đúng.
Cách chắc chắn nhất: dùng /actuator/env sau khi set env var để verify property nào bind. Nếu không thấy trong systemEnvironment source, tên env var sai.
Quy tắc thực tế: mỗi . trong property name = một _ trong env var; mỗi - trong property name = một _ trong env var. Kết quả cho spring.datasource.hikari.maximum-pool-size là SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE.
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?▸
application-prod.yml?Pattern chuẩn 2026:
- Commit git:
application.yml(base defaults + dev config),application-prod.yml(prod-specific non-secret — URL internal, pool size, timeout). Hai file này public trong repo, ai cũng đọc được. - Inject qua env var: passwords, API key, connection string có credential — từ K8s Secret hoặc Vault. Env var ưu tiên cao hơn file nên override được
application-prod.yml. - Command line: ad-hoc override cho debug, canary test, smoke test một lần mà không cần thay manifest. Command line ưu tiên cao nhất nên override được mọi thứ.
Vì sao không commit password vào application-prod.yml?
Git repository có lịch sử vĩnh viễn. Kể cả nếu xoá file hoặc thay đổi password sau, commit cũ vẫn tồn tại trong git history — ai clone repo đều có thể git log -p để tìm lại. Repository scanner như GitHub Secret Scanning, GitGuardian tự động quét và alert khi phát hiện pattern password/API key.
Pattern đúng: password: ${DB_PASSWORD} trong YAML (placeholder, không có giá trị thật), inject DB_PASSWORD qua K8s Secret. Secret không nằm trong git, chỉ tồn tại trong K8s etcd (encrypted at rest).
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?
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