Spring Boot/application.properties vs application.yml — externalized configuration
~26 phútSpring Boot FoundationsMiễn phí

application.properties vs application.yml — externalized configuration

Boot có 17 nguồn property xếp theo thứ tự ưu tiên. Bài này bóc cơ chế PropertySource ordering, properties vs YAML format, @ConfigurationProperties vs @Value, validation, profile-specific files, relax binding cho env var, type-safe binding với records, và pattern config sensitive data qua secrets.

Auto-config (bài 03) tạo bean dựa vào classpath. Nhưng config giá trị (DB URL, API key, pool size) đến từ đâu? Đó là externalized configuration — Boot 1 trong 5 trụ cột. Bài này bóc cơ chế: 17 nguồn property xếp ưu tiên, 2 format file (properties/YAML), 2 cách inject (@Value vs @ConfigurationProperties), profile-specific, relax binding, và pattern thực tế cho production.

Đa phần dev biết "config trong application.yml". Bài này trả lời câu hỏi sâu: nếu cùng property db.url xuất hiện ở 5 nguồn, nguồn nào win? Khi nào dùng @Value thay @ConfigurationProperties? Vì sao K8s env var DB_URL map được sang db.url mà không phải config?

1. Vì sao externalize config

Spring 4 era — cấu hình hardcode trong code:

public class App {
    DataSource ds = new HikariDataSource("jdbc:postgresql://localhost/dev", "user", "pass");
}

Pain rõ ràng:

  • Build per env: dev/staging/prod khác URL → build 3 jar khác nhau, không reproducible.
  • Secret leak: password trong source code → commit lên git → security incident.
  • Khó debug: muốn override 1 setting cho 1 lần test → phải sửa code, rebuild.

Lời giải: externalize — config sống ngoài jar, đọc tại runtime:

@Value("${db.url}")
private String dbUrl;
# application.yml
db.url: jdbc:postgresql://localhost/dev
# Override khi run:
java -Ddb.url=jdbc:postgresql://prod/app -jar app.jar
java -jar app.jar --db.url=jdbc:postgresql://prod/app
DB_URL=jdbc:postgresql://prod/app java -jar app.jar

3 cách override khác nhau (system prop, command line, env var). Boot quản theo ưu tiên — section 2.

2. 17 nguồn PropertySource — ưu tiên cao đến thấp

Boot 3.4 có 17 nguồn property. Khi resolve \${db.url}, Boot tra theo thứ tự:

#NguồnVí dụ
1SpringApplication.setDefaultPropertiesapp.setDefaultProperties(Map.of("port","8080"))
2@PropertySource trên @Configuration@PropertySource("classpath:custom.properties")
3Config data files (default)application.properties, application.yml
4Profile-specific config dataapplication-prod.yml
5OS env varDB_URL=...
6Java system property-Ddb.url=...
7JNDI attributesrare, app server context
8ServletContext init paramsweb.xml legacy
9ServletConfig init paramsservlet-specific
10SPRING_APPLICATION_JSON env varJSON-encoded properties
11SPRING_APPLICATION_JSON system proptương tự
12Command line args--db.url=...
13@TestPropertySource (test only)test override
14Devtools properties (~/.spring-boot-devtools.properties)dev only
15Imported config trees (Boot 3+)K8s ConfigMap mounted
16Imported config dataspring.config.import=
17Default property values từ bindingfallback

Đảo ngược thứ tự ưu tiên — cao xuống thấp:

flowchart TB
    L1["1. Command line args<br/>--db.url=..."]
    L2["2. SPRING_APPLICATION_JSON"]
    L3["3. Java System Properties<br/>-Ddb.url=..."]
    L4["4. OS Environment Variables<br/>DB_URL=..."]
    L5["5. application-\{profile\}.properties"]
    L6["6. application.properties / .yml"]
    L7["7. @PropertySource"]
    L8["8. Default properties"]

    L1 --> L2 --> L3 --> L4 --> L5 --> L6 --> L7 --> L8

    style L1 fill:#fef3c7
    style L4 fill:#fef3c7
    style L6 fill:#d1fae5

Quy tắc nhớ: càng "ngoài" (command line) → càng cao priority. application.yml ở giữa — base default, override được từ trên (env var, command line) và override từ dưới (default properties).

2.1 Pattern thực tế deployment

# application.yml (commit git, default cho dev)
db:
  url: jdbc:postgresql://localhost:5432/dev
  username: dev_user
spring:
  profiles:
    active: dev
# application-prod.yml (commit git, override cho prod)
db:
  url: jdbc:postgresql://prod-db.internal:5432/app
  username: app_prod
# K8s deployment.yaml (production secrets)
spec:
  containers:
    - name: app
      env:
        - name: SPRING_PROFILES_ACTIVE
          value: prod
        - name: DB_PASSWORD                    # Secret, override application.yml
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password

Boot resolve db.password:

  1. Check command line — không có.
  2. Check env var DB_PASSWORD — có (relax binding → match db.password). Win. ← prod secret từ K8s Secret.

Pattern này standard 2026:

  • application.yml ↔ default + dev.
  • application-\{profile\}.yml ↔ prod-specific non-secret.
  • Env var ↔ secrets từ Vault/K8s Secret.
  • Command line ↔ ad-hoc override (debug, smoke test).

3. Properties vs YAML — chọn cái nào

Boot support cả 2 format. Cùng config viết 2 cách:

application.properties:

spring.datasource.url=jdbc:postgresql://localhost/app
spring.datasource.username=app
spring.datasource.password=secret
spring.datasource.hikari.maximum-pool-size=20
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
logging.level.com.olhub=DEBUG

application.yml:

spring:
  datasource:
    url: jdbc:postgresql://localhost/app
    username: app
    password: secret
    hikari:
      maximum-pool-size: 20
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    com.olhub: DEBUG
AspectPropertiesYAML
FormatFlat key-valueHierarchical
Comment##
Multi-valueIndex [0], [1]List - value
Multi-line string\ line continuation`
TypeAll stringNative (string, number, bool, list, map)
VerbosityVerbose (lặp prefix)Compact
Diff readabilityMỗi dòng độc lậpIndent matters → diff khó
Tool supportUniversalCần YAML parser
RiskTypo dễ bắtIndent typo gây bug ngầm

Khuyến nghị 2026:

  • YAML cho file lớn (>30 entry) — hierarchical đọc dễ hơn.
  • Properties cho file nhỏ hoặc khi tool không support YAML.
  • Mix được — 1 project có cả application.propertiesapplication.yml (Boot merge cả 2). Nhưng tránh mix — confusing.

Cảnh báo YAML:

# YAML — indent matters!
spring:
  datasource:
    url: jdbc:postgresql://...
   username: app           # 3 space thay 4 → typo, parse fail

vs:

# Properties — flat, no indent risk
spring.datasource.url=jdbc:postgresql://...
spring.datasource.username=app

Khoá này dùng YAML mặc định, properties cho ví dụ ngắn.

4. Profile-specific configuration

3 file phổ biến:

src/main/resources/
├── application.yml              <- chung cho moi profile
├── application-dev.yml          <- override khi profile=dev active
├── application-staging.yml      <- override khi profile=staging
└── application-prod.yml         <- override khi profile=prod

Activate profile qua:

# Cach 1: command line
java -jar app.jar --spring.profiles.active=prod

# Cach 2: env var
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

# Cach 3: trong application.yml (rare, tu activate)
spring:
  profiles:
    active: dev

Boot load application.yml → đè bằng application-prod.yml. Property nào không có trong prod.yml → giữ giá trị từ application.yml.

4.1 Profile groups (Boot 2.4+)

Group nhiều profile thành 1 logic:

spring:
  profiles:
    group:
      prod: prod-db, prod-cache, prod-tracing
      staging: staging-db, staging-cache

Activate prod → tự include prod-db, prod-cache, prod-tracing. Tách thành 3 file:

application-prod-db.yml          # config DB cho prod
application-prod-cache.yml       # config cache
application-prod-tracing.yml     # config tracing

Lợi ích: tách concern, file nhỏ dễ maintain. Khi 1 file lớn 100 dòng, group là cách clean.

4.2 Multi-document YAML (Boot 2.4+)

Spring Boot 2.4 cho phép nhiều "document" trong 1 YAML file, ngăn cách bằng ---:

# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost/dev   # default cho dev

---
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: jdbc:postgresql://prod/app

---
spring:
  config:
    activate:
      on-profile: staging
  datasource:
    url: jdbc:postgresql://staging/app

3 document trong 1 file — Boot pick document có profile match. Dùng khi không muốn tách nhiều file.

5. @Value vs @ConfigurationProperties — 2 cách inject

5.1 @Value — inline injection

@Service
public class OrderService {
    @Value("${app.max-orders:100}")            // default 100 neu thieu
    private int maxOrders;

    @Value("${app.allowed-origins}")            // CSV → split
    private List<String> allowedOrigins;

    @Value("#{systemProperties['user.home']}")  // SpEL
    private String userHome;
}

Pros: đơn giản, inline. Cons:

  • Không type-safe khi property phức tạp.
  • Spread khắp class — khó refactor.
  • Không validate (typo property name → fail runtime).
  • Không IDE autocomplete (string literal).

Khi dùng: 1-2 property đơn giản, hoặc khi property là SpEL expression động.

5.2 @ConfigurationProperties — type-safe binding

@ConfigurationProperties(prefix = "app")
@Validated                                     // bat validation
public class AppProperties {

    @NotBlank
    private String name;

    @Min(1) @Max(1000)
    private int maxOrders = 100;

    @NotEmpty
    private List<String> allowedOrigins;

    private Database database = new Database();

    public static class Database {
        @NotBlank
        private String url;

        private int poolSize = 10;
        // getters/setters
    }

    // getters/setters
}
app:
  name: OLHub
  max-orders: 500
  allowed-origins:
    - https://olhub.org
    - https://www.olhub.org
  database:
    url: jdbc:postgresql://localhost/app
    pool-size: 30

Register:

@SpringBootApplication
@ConfigurationPropertiesScan                   // scan @ConfigurationProperties
public class App { ... }

// Hoac:
@EnableConfigurationProperties(AppProperties.class)
@Configuration
public class AppConfig { ... }

Sử dụng:

@Service
public class OrderService {
    private final AppProperties props;

    public OrderService(AppProperties props) {
        this.props = props;
    }

    public void process() {
        if (orders.size() > props.getMaxOrders()) { ... }
    }
}

Pros:

  • Type-safe: int, List, nested object đúng kiểu.
  • Validation: @NotBlank, @Min, @Max từ Jakarta Validation.
  • IDE autocomplete: nếu generate metadata (qua spring-boot-configuration-processor), IDE hint property + Javadoc.
  • Nested: object lồng nhau, inject 1 lần dùng nhiều nơi.
  • Refactor-friendly: rename property qua IDE, IDE tự update YAML.

Cons:

  • Setup nhiều (class + getters/setters + register).
  • Verbose cho 1-2 property.

Khi dùng: mọi config domain-specific cho app/feature. Đây là best practice 2026.

5.3 Records cho immutable config (Boot 2.6+)

Java 17 records giúp @ConfigurationProperties cực kỳ gọn:

@ConfigurationProperties(prefix = "app")
@Validated
public record AppProperties(
    @NotBlank String name,
    @Min(1) @Max(1000) int maxOrders,
    @NotEmpty List<String> allowedOrigins,
    Database database
) {
    public record Database(
        @NotBlank String url,
        int poolSize
    ) {}
}

Tương đương 50 dòng class trên — gọn hơn 5x. Immutable (record final by default), thread-safe, no setter pollution.

Đây là pattern khoá này dùng — record + @ConfigurationProperties + @Validated.

5.4 Generate IDE metadata

Thêm dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Annotation processor đọc @ConfigurationProperties class lúc compile, sinh file META-INF/spring-configuration-metadata.json. IDE (IntelliJ, VSCode) đọc file này → autocomplete property + show Javadoc + warn typo.

app:
  ma|        # IDE goi y: max-orders, name

6. Relax binding — cứu cánh K8s/Docker

Boot có cơ chế relax binding: 1 property name match nhiều format khác nhau:

Format trong YAMLMatch với env varMatch với Java property
app.max-ordersAPP_MAXORDERS hoặc APP_MAX_ORDERSapp.max-orders
app.maxOrders (camelCase)APP_MAXORDERSapp.maxOrders
app.max_orders (snake_case)APP_MAX_ORDERSapp.max_orders
app.allowedOrigins (List)APP_ALLOWEDORIGINS_0_ (legacy) hoặc APP_ALLOWED_ORIGINS_0_app.allowed-origins[0]

Quy tắc canonical: kebab-case (app.max-orders) trong YAML/properties. Boot tự convert kebab ↔ camel ↔ env var.

6.1 Cơ chế relax cụ thể

Khi Boot resolve app.max-orders:

  1. Convert kebab → uppercase + underscore: APP_MAX_ORDERS.
  2. Tra env var: tìm APP_MAX_ORDERS → có → dùng.
  3. Nếu không, tra system prop: app.max-orders literal.
  4. Nếu không, tra config file.
  5. Default value (nếu có).

Pattern K8s/Docker:

# K8s deployment.yaml
spec:
  containers:
    - name: app
      env:
        - name: SPRING_DATASOURCE_URL          # → spring.datasource.url
          value: jdbc:postgresql://prod/app
        - name: APP_MAX_ORDERS                  # → app.max-orders
          value: "1000"
        - name: SPRING_PROFILES_ACTIVE          # → spring.profiles.active
          value: prod

K8s standard dùng UPPER_SNAKE_CASE env var. Boot relax binding tự match → không cần file config trong container.

6.2 Edge case

SPRING_APPLICATION_JSON — env var đặc biệt accept JSON object:

SPRING_APPLICATION_JSON='{"app":{"max-orders":1000,"allowed-origins":["https://olhub.org"]}}'
java -jar app.jar

Hữu ích khi cần override nhiều property cùng lúc qua 1 env var (vd CI/CD inject config block).

7. Pattern thực tế cho production

7.1 Tách config theo concern

application.yml                 # default + dev
application-staging.yml         # staging override
application-prod.yml            # prod override (non-secret)

# Secrets KHONG commit vao git:
.env (local)                    # symlink to actual values
K8s Secret (prod)               # mount qua env var hoac file
Vault (prod)                    # Spring Cloud Config / Spring Cloud Vault

Quy tắc: không bao giờ commit secret vào git. Repository scanner (GitGuardian, GitHub Secret Scanning) detect và alert. Mọi password/API key qua env var hoặc secrets manager.

7.2 Reference env var trong YAML

spring:
  datasource:
    url: ${DB_URL}                              # required
    username: ${DB_USER:app}                    # default 'app'
    password: ${DB_PASS}                        # required, fail nếu thiếu
    hikari:
      maximum-pool-size: ${DB_POOL_SIZE:20}    # default 20

Cú pháp \${VAR:default} — fallback nếu env var không có. Required (no default) → app fail to start nếu thiếu, log clear error message.

7.3 Spring Cloud Config Server (preview)

Cho enterprise có nhiều service, centralize config qua Config Server:

# bootstrap.yml (special file load truoc application.yml)
spring:
  config:
    import: configserver:https://config.acme.internal
  application:
    name: order-service

Config Server expose REST API trả config cho order-service. Service start → fetch config → merge với local → start app. Module 11 (Microservices) sẽ đào sâu.

8. Pitfall tổng hợp

Nhầm 1: Hardcode secret trong application.yml commit git.

spring:
  datasource:
    password: prod-secret-2026     # LO TREN GITHUB

✅ Reference env var: password: \${DB_PASS}. Set env var qua K8s Secret hoặc Vault.

Nhầm 2: Dùng @Value cho 20 property cùng prefix.

@Value("${app.name}") String name;
@Value("${app.max-orders}") int maxOrders;
@Value("${app.allowed-origins}") List<String> origins;
// ... 17 dong nua

✅ Bind qua @ConfigurationProperties(prefix = "app") — type-safe, validate, refactor.

Nhầm 3: YAML indent inconsistent.

spring:
  datasource:
    url: jdbc:postgresql://...
   username: app          # 3 space — parse fail

✅ Dùng IDE với YAML linter (IntelliJ, VSCode YAML extension), hoặc dùng application.properties cho file ngắn.

Nhầm 4: Tin "env var override application.yml luôn." ✅ Đúng cho hầu hết case, nhưng command line args override env var (priority cao hơn). Verify thứ tự ưu tiên.

Nhầm 5: Không validate @ConfigurationProperties.

@ConfigurationProperties(prefix = "app")
public record Props(String url) {}

User quên set app.url → field null → NPE runtime sau khi app đã start. ✅ Thêm @Validated + @NotBlank/@NotNull → fail-fast tại startup nếu thiếu.

Nhầm 6: Nhầm application.ymlbootstrap.yml. ✅ application.yml — config app thường. bootstrap.yml — load trước application.yml, dùng cho Spring Cloud Config bootstrap. Không nhầm — Boot 3+ migrate sang spring.config.import, dần bỏ bootstrap.yml.

Nhầm 7: Dùng underscore trong YAML key.

app_max_orders: 100      # Boot van resolve, nhung khong canonical

✅ Dùng kebab-case canonical: app.max-orders: 100 hoặc app:\n max-orders: 100.

Nhầm 8: Quên @ConfigurationPropertiesScan hoặc @EnableConfigurationProperties. ✅ Add 1 trong 2 vào @SpringBootApplication class. Class @ConfigurationProperties không tự register.

9. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

Spring Boot Reference docs:

Source:

Bài viết:

Tool:

  • /actuator/env — runtime liệt kê mọi PropertySource active + value (sanitize secrets).
  • /actuator/configprops — runtime liệt kê mọi @ConfigurationProperties bean + bound values.
  • IntelliJ "Spring Boot Profiles" tool window — switch profile easy.

Ghi chú: bookmark "Common Application Properties" appendix — 1500+ property chính chủ documented. Bất kỳ lúc nào không nhớ "property name là gì cho tính năng X", tra appendix.

10. Tóm tắt

  • Externalize config = config sống ngoài jar, đọc tại runtime — giải quyết build-per-env, secret leak, debug khó.
  • Boot có 17 PropertySource xếp ưu tiên: command line > env var > application-{profile}.yml > application.yml > default.
  • 2 format file: application.properties (flat) và application.yml (hierarchical). YAML tốt hơn cho file lớn.
  • Profile-specific files (application-prod.yml) override application.yml. Activate qua --spring.profiles.active=prod hoặc SPRING_PROFILES_ACTIVE.
  • Profile groups (Boot 2.4+) gom profile thành logic: prod = prod-db + prod-cache + prod-tracing.
  • Multi-document YAML (Boot 2.4+) — nhiều profile trong 1 file qua --- separator + spring.config.activate.on-profile.
  • 2 cách inject: @Value("${...}") cho 1-2 property đơn giản; @ConfigurationProperties cho group config — type-safe, validate, IDE autocomplete.
  • Records + @ConfigurationProperties + @Validated = pattern modern 2026 — gọn, immutable, fail-fast.
  • spring-boot-configuration-processor annotation processor sinh spring-configuration-metadata.json cho IDE hint.
  • Relax binding: kebab-case canonical match camelCase/snake_case/UPPER_SNAKE_CASE. K8s env var SPRING_DATASOURCE_URLspring.datasource.url tự nhiên.
  • \${VAR:default} syntax cho fallback. Required env var (no default) → fail-fast nếu thiếu.
  • Production pattern: non-secret trong git (application.yml, application-prod.yml); secret qua env var (K8s Secret, Vault). Không bao giờ commit secret.
  • Debug runtime: /actuator/env + /actuator/configprops. Dev local: --debug flag.

11. Tự kiểm tra

Tự kiểm tra
Q1
App của bạn có 4 nguồn cùng đặt db.url: (1) application.yml (jdbc:postgresql://localhost), (2) env var DB_URL=jdbc:postgresql://staging, (3) JVM arg -Ddb.url=jdbc:postgresql://prod1, (4) command line --db.url=jdbc:postgresql://prod2. Giá trị nào win? Vì sao? Cách dùng implicit này trong CI/CD ra sao?

Win: (4) command line argumentjdbc:postgresql://prod2.

Thứ tự ưu tiên (cao đến thấp):

  1. Command line args (--key=value)
  2. Java System Properties (-Dkey=value)
  3. OS environment variables (KEY=value)
  4. application-{profile}.yml
  5. application.yml

Cách dùng trong CI/CD:

  • Application defaults trong git: application.yml chứa default cho dev. Commit code.
  • Profile-specific config commit git: application-prod.yml chứa URL prod (không secret), endpoint internal.
  • Environment variables qua K8s/Docker: SPRING_PROFILES_ACTIVE=prod, DB_PASSWORD (secret từ K8s Secret).
  • Command line cho ad-hoc/debug: chạy với --db.url=jdbc:postgresql://canary/... 1 lần test version mới mà không thay K8s manifest. Override env + file.

Pattern mạnh: command line override env var override file → cho phép layered config từ "default" → "env-specific" → "ad-hoc override" mà không phải thay tầng dưới.

Pitfall thực tế: nếu CI/CD set env var DB_URL nhưng deploy script vô tình truyền command line --db.url=... hardcoded, command line win → bug "tại sao env var không có effect". Cách debug: log environment.getPropertySources() tại startup, in ra source nào active. Hoặc dùng /actuator/env endpoint check runtime.

Q2
So sánh @Value@ConfigurationProperties. Cho 1 ví dụ cụ thể nào nên dùng @Value, 1 ví dụ nên dùng @ConfigurationProperties.
Aspect@Value@ConfigurationProperties
Type-safeString/primitive ok, complex type cần parseFull type — nested object, List, Map, enum
ValidationKhông (chỉ default value)Có — `@Validated` + Jakarta Validation
SpELCó — #{...}Không (pure binding)
IDE autocompleteKhôngCó (qua metadata)
Refactor-friendlyString literal — rename khóClass field — IDE rename OK
Setup overhead0 — inline annotationClass + getters/setters + register
Best for1-2 prop độc lập, SpELGroup prop cùng prefix

Ví dụ @Value phù hợp:

@Service
public class HealthCheck {
  @Value("#{T(java.time.Instant).now()}")     // SpEL
  private Instant startedAt;

  @Value("${git.commit.id:unknown}")          // 1 prop don gian
  private String commitId;
}

Ví dụ @ConfigurationProperties phù hợp:

@ConfigurationProperties(prefix = "app.email")
@Validated
public record EmailProperties(
  @NotBlank String host,
  @Min(1) @Max(65535) int port,
  @NotBlank String username,
  @NotBlank String password,
  boolean tlsEnabled,
  Duration timeout,
  List<String> ccAddresses
) {}

7 property cùng prefix app.email — type-safe (Duration, List), validate, refactor-friendly. @Value cho 7 prop sẽ dài + dễ typo.

Quy tắc: mặc định @ConfigurationProperties. Chỉ dùng @Value khi cần SpEL hoặc inject 1-2 prop standalone.

Q3
Đoạn YAML sau có 2 vấn đề. Liệt kê + fix.
spring:
datasource:
  url: jdbc:postgresql://prod-db.internal:5432/app
  username: app_prod
  password: prod-secret-2026
 hikari:
    maximum-pool-size: 50

logging:
level:
  com.olhub: DEBUG
  1. Indent typo: hikari: indent 3 space thay 4 → YAML parse fail. hikari bị parse như sibling của password thay child của datasource. Spring Boot startup throw InvalidConfigurationPropertiesException hoặc property không bind.

    Fix: indent đúng 4 space (hoặc 2 space đồng nhất toàn file):

    spring:
    datasource:
      url: jdbc:postgresql://prod-db.internal:5432/app
      username: app_prod
      password: prod-secret-2026
      hikari:
        maximum-pool-size: 50
    Tránh issue này: dùng IDE với YAML linter (IntelliJ built-in, VSCode extension). Hoặc dùng application.properties cho file ngắn — flat, no indent.
  2. Hardcode password trong git: password: prod-secret-2026 commit lên repo → security incident. Repository scanner (GitGuardian, GitHub Secret Scanning) detect và alert.

    Fix: reference env var:

    spring:
    datasource:
      password: ${DB_PASSWORD}       # required, fail nếu thiếu
    Set env var qua K8s Secret hoặc Vault. Không bao giờ commit secret.

Fix tổng:

spring:
datasource:
  url: ${DB_URL}
  username: ${DB_USER:app}
  password: ${DB_PASSWORD}
  hikari:
    maximum-pool-size: ${DB_POOL_SIZE:20}

logging:
level:
  com.olhub: ${LOG_LEVEL:INFO}

Mọi config có default cho dev local, override qua env var cho prod. Sensitive data không có default — bắt buộc set qua secret.

Q4
K8s deployment set env var SPRING_DATASOURCE_URL=jdbc:postgresql://prod/app. App đọc property spring.datasource.url qua @ConfigurationProperties. Cơ chế nào cho phép env var match property name? Nếu env var đặt spring.datasource.url (literal lowercase + dot) thì có work không?

Cơ chế: relax binding của Spring Boot.

Quy tắc convert env var → property:

  1. Env var name: SPRING_DATASOURCE_URL (UPPER_SNAKE_CASE).
  2. Boot convert: lowercase + replace _ bằng .spring.datasource.url.
  3. Match với property canonical name → bind value.

Cùng property spring.datasource.url match được nhiều format env var:

  • SPRING_DATASOURCE_URL ✅ (UPPER_SNAKE — chuẩn K8s/Docker)
  • spring.datasource.url ✅ (lowercase + dot — nếu shell cho phép)
  • spring_datasource_url ✅ (lowercase + underscore)

Câu 2: env var spring.datasource.url literal có work không?

Có — nếu shell cho phép env var có dấu .. Nhưng:

  • Bash/Zsh on Linux/macOS: không cho phép . trong env var name (chỉ accept [A-Za-z_][A-Za-z0-9_]*). Set bằng export spring.datasource.url=... sẽ fail.
  • Workaround: dùng env command: env "spring.datasource.url=jdbc:..." java -jar app.jar. Hoặc set qua Docker -e.
  • K8s/Docker env: support tên với dot (vì YAML key, không phải shell var). Nhưng convention vẫn là UPPER_SNAKE để consistency với shell.

Best practice: dùng UPPER_SNAKE cho env var (SPRING_DATASOURCE_URL) — work mọi nơi, conventional, không phụ thuộc shell.

Verify relax binding: bật /actuator/env → tìm property spring.datasource.url → in ra value source: "systemEnvironment".

Q5
Bạn có 3 file: application.yml (default), application-staging.yml, application-prod.yml. Active profile prod. Property app.url đặt ở cả 3 file. Giá trị nào win? Nếu bạn muốn property của application.yml win bất chấp profile (vd hardcoded global default), làm sao?

Câu 1: application-prod.yml win.

Boot load thứ tự:

  1. Load application.yml → property app.url set giá trị "default".
  2. Detect profile prod active → load application-prod.yml → override app.url với giá trị "prod".
  3. application-staging.yml không load (profile staging không active).

Profile-specific file override default — đó là intent.

Câu 2 — muốn default win bất chấp profile:

Có 3 cách:

  1. Bỏ app.url khỏi application-prod.yml (đơn giản nhất). Chỉ override property thực sự khác giữa env. Property chung giữ trong application.yml.
  2. Dùng env var với priority cao hơn file:
    # K8s manifest
    env:
    - name: APP_URL
      value: "global-default"
    Env var override mọi file (priority 4 vs 5-6). Nhưng pattern này khó hơn — phải set env var ở mọi env.
  3. Dùng default value trong code:
    @ConfigurationProperties(prefix = "app")
    public record AppProps(String url) {
      public AppProps {
          if (url == null) url = "global-default";   // canonical default
      }
    }
    Default trong code thay file — không thể override từ file. Hữu ích khi default thực sự là "constant".

Quy tắc design: property profile-specific = chỉ khác biệt giữa env. Common config giữ trong application.yml. DRY: nếu 90% config giống nhau giữa profile, đừng duplicate trong từng application-{profile}.yml.

Q6
Đoạn record sau có gì sai? Code đúng nên là gì?
@ConfigurationProperties(prefix = "app.email")
public record EmailProps(
  String host,
  int port,
  String username,
  String password,
  boolean tlsEnabled
) {}

// Trong App.java:
@SpringBootApplication
public class App { ... }

3 vấn đề:

  1. Không validate: nếu user không set app.email.host, field host = null → NPE runtime sau khi app đã start. Nên fail-fast tại startup.

    Fix: thêm @Validated + Jakarta Validation:

    @ConfigurationProperties(prefix = "app.email")
    @Validated
    public record EmailProps(
      @NotBlank String host,
      @Min(1) @Max(65535) int port,
      @NotBlank String username,
      @NotBlank String password,
      boolean tlsEnabled
    ) {}
  2. Không register class: @ConfigurationProperties không tự register thành bean. Cần 1 trong 2:
    // Cach 1: scan toan project
    @SpringBootApplication
    @ConfigurationPropertiesScan
    public class App { ... }
    
    // Cach 2: register cu the
    @SpringBootApplication
    @EnableConfigurationProperties(EmailProps.class)
    public class App { ... }
    Quên 1 trong 2 → record không bind, inject bean fail với NoSuchBeanDefinitionException.
  3. Không có metadata cho IDE: không có annotation processor → IDE không autocomplete property. User phải nhớ tên property hoặc tra docs.

    Fix: thêm dependency:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>
    Annotation processor đọc EmailProps lúc compile, sinh spring-configuration-metadata.json → IDE autocomplete + show Javadoc.

Code đúng đầy đủ:

// pom.xml: them spring-boot-configuration-processor (optional)

@ConfigurationProperties(prefix = "app.email")
@Validated
public record EmailProps(
  /** SMTP server host. */
  @NotBlank String host,
  /** SMTP port. Common: 25, 465 (SSL), 587 (TLS). */
  @Min(1) @Max(65535) int port,
  /** SMTP username. */
  @NotBlank String username,
  /** SMTP password. */
  @NotBlank String password,
  /** Enable TLS encryption. */
  boolean tlsEnabled
) {}

@SpringBootApplication
@ConfigurationPropertiesScan
public class App {
  public static void main(String[] args) {
      SpringApplication.run(App.class, args);
  }
}

# application.yml
app:
email:
  host: ${SMTP_HOST}
  port: ${SMTP_PORT:587}
  username: ${SMTP_USER}
  password: ${SMTP_PASS}
  tls-enabled: true

Pattern 2026 chuẩn: record + @Validated + Javadoc + @ConfigurationPropertiesScan + metadata generator + env var binding.

Q7
Production app crash với NullPointerException trên field props.timeout() — record property type là Duration. Diagnose: vấn đề có thể nằm ở đâu? Cách fix?

Cause khả năng nhất: record không có default cho Duration timeout, user không set app.timeout → field bind = null. Code call .timeout() → NPE.

Diagnose qui trình:

  1. Check /actuator/configprops: in ra mọi @ConfigurationProperties bean + bound value. Tìm app.timeout → nếu null hoặc missing → confirm cause.
  2. Check /actuator/env: tìm app.timeout trong PropertySource. Nếu không có → user quên config.
  3. Check log startup: nếu có @Validated + @NotNull, app sẽ fail-fast tại startup với BindValidationException rõ ràng. Không có validation → bug latent.

Fix:

  1. Thêm validation để fail-fast lần sau:
    @ConfigurationProperties(prefix = "app")
    @Validated
    public record AppProps(
      @NotNull Duration timeout,            // bat buoc
      String name
    ) {}
    Thiếu config → app không start, log clear: "Field 'timeout': must not be null".
  2. Hoặc set default trong record:
    @ConfigurationProperties(prefix = "app")
    public record AppProps(Duration timeout, String name) {
      public AppProps {
          if (timeout == null) timeout = Duration.ofSeconds(30);    // canonical default
      }
    }
  3. Hoặc set default trong YAML:
    app:
    timeout: PT30S    # ISO-8601 duration: 30 seconds

Best practice quan trọng: fail-fast luôn tốt hơn fail-late. App start xong rồi NPE 5 phút sau khi user request đến = downtime + customer impact. Validation tại startup = "không start được" = K8s health check fail = pod không serve traffic. Đó là intended.

Quy tắc 2026: mọi @ConfigurationProperties domain-critical phải có @Validated + constraint annotation. Cost setup nhỏ, lợi ích huge.

Bài tiếp theo: Profiles — dev/staging/prod activation, profile-specific bean, profile group

Bài này có giúp bạn hiểu bản chất không?

Bình luận (0)

Đang tải...