Spring Core & Boot/PropertySource ordering & relax binding — thứ tự ưu tiên và K8s env var
35/41
Bài 35 / 41~12 phútConfig, Profiles & LoggingMiễn phí lượt xem

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:#059669

Giải thích từng tầng:

NguồnVí 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 varDB_URL=jdbc:...K8s/Docker — standard injection
Profile-specific fileapplication-prod.ymlConfig khác nhau giữa môi trường
Default fileapplication.ymlBase config + dev defaults
@PropertySource@PropertySource("classpath:extra.properties")Config bổ sung từ file custom
Default propertiesapp.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"| NULL

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. 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 SpringConfigurationPropertySourceRelaxedPropertyResolver.

// 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:

  1. Check command line — không có.
  2. Check env var SPRING_DATASOURCE_URL (canonical: springdatasourceurl) — match spring.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.

Verify relax binding tại runtime

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:

  1. Command line — không.
  2. 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-sizespringdatasourcemaximumpoolsize. 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.

Pitfall: @PropertySource không load YAML

@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ế Environment interface, 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 @ConfigurationProperties không bind đúng giá trị.
  • Profiles — activation và bean-level: application-prod.yml trong 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ướiPropertySourcesPropertyResolver duyệt danh sách MutablePropertySources theo 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_URL map sang spring.datasource.url tự độ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/env cho thấy chính xác PropertySource nào win cho từng property.

Tự kiểm tra

Tự kiểm tra
Q1
App 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.

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.urlCommandLinePropertySource, 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".

Q2
Tạ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/app

Nế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.

Q3
K8s 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.

Cơ chế: relax binding trong SpringConfigurationPropertySourceRelaxedPropertyResolver. 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ì.

Q4
Bạ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ì?

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=20

Kiể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-sizeSPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE.

Q5
Mô 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?

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

Đặt 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