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ồn | Ví dụ |
|---|---|---|
| 1 | SpringApplication.setDefaultProperties | app.setDefaultProperties(Map.of("port","8080")) |
| 2 | @PropertySource trên @Configuration | @PropertySource("classpath:custom.properties") |
| 3 | Config data files (default) | application.properties, application.yml |
| 4 | Profile-specific config data | application-prod.yml |
| 5 | OS env var | DB_URL=... |
| 6 | Java system property | -Ddb.url=... |
| 7 | JNDI attributes | rare, app server context |
| 8 | ServletContext init params | web.xml legacy |
| 9 | ServletConfig init params | servlet-specific |
| 10 | SPRING_APPLICATION_JSON env var | JSON-encoded properties |
| 11 | SPRING_APPLICATION_JSON system prop | tương tự |
| 12 | Command line args | --db.url=... |
| 13 | @TestPropertySource (test only) | test override |
| 14 | Devtools properties (~/.spring-boot-devtools.properties) | dev only |
| 15 | Imported config trees (Boot 3+) | K8s ConfigMap mounted |
| 16 | Imported config data | spring.config.import= |
| 17 | Default property values từ binding | fallback |
Đả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:#d1fae5Quy 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:
- Check command line — không có.
- Check env var
DB_PASSWORD— có (relax binding → matchdb.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
| Aspect | Properties | YAML |
|---|---|---|
| Format | Flat key-value | Hierarchical |
| Comment | # | # |
| Multi-value | Index [0], [1] | List - value |
| Multi-line string | \ line continuation | ` |
| Type | All string | Native (string, number, bool, list, map) |
| Verbosity | Verbose (lặp prefix) | Compact |
| Diff readability | Mỗi dòng độc lập | Indent matters → diff khó |
| Tool support | Universal | Cần YAML parser |
| Risk | Typo dễ bắt | Indent 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.propertiesvàapplication.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,@Maxtừ 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 YAML | Match với env var | Match với Java property |
|---|---|---|
app.max-orders | APP_MAXORDERS hoặc APP_MAX_ORDERS | app.max-orders |
app.maxOrders (camelCase) | APP_MAXORDERS | app.maxOrders |
app.max_orders (snake_case) | APP_MAX_ORDERS | app.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:
- Convert kebab → uppercase + underscore:
APP_MAX_ORDERS. - Tra env var: tìm
APP_MAX_ORDERS→ có → dùng. - Nếu không, tra system prop:
app.max-ordersliteral. - Nếu không, tra config file.
- 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.yml và bootstrap.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
Spring Boot Reference docs:
- Spring Boot Reference — Externalized Configuration — overview chính thức + danh sách 17 PropertySource.
- Spring Boot Reference — Configuration Properties —
@ConfigurationPropertiesđầy đủ. - Spring Boot Reference — Profiles — profile activation, group, multi-document YAML.
- Spring Boot Reference — Common Application Properties — bảng property phổ biến (1500+ entry chính chủ).
- Spring Boot Reference — Configuration Property Metadata — generate metadata cho IDE.
Source:
ConfigurationPropertiesBindingPostProcessor— BPP bind property → object.ConfigDataEnvironmentPostProcessor— loadapplication.ymlvà profile-specific files.
Bài viết:
- Phil Webb — Spring Boot Properties Migrator — tool migrate property name khi upgrade Boot.
- Stéphane Nicoll — Spring Boot Configuration Properties — best practice từ Boot maintainer.
Tool:
/actuator/env— runtime liệt kê mọi PropertySource active + value (sanitize secrets)./actuator/configprops— runtime liệt kê mọi@ConfigurationPropertiesbean + 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) overrideapplication.yml. Activate qua--spring.profiles.active=prodhoặcSPRING_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;@ConfigurationPropertiescho group config — type-safe, validate, IDE autocomplete. - Records +
@ConfigurationProperties+@Validated= pattern modern 2026 — gọn, immutable, fail-fast. spring-boot-configuration-processorannotation processor sinhspring-configuration-metadata.jsoncho IDE hint.- Relax binding: kebab-case canonical match camelCase/snake_case/UPPER_SNAKE_CASE. K8s env var
SPRING_DATASOURCE_URL→spring.datasource.urltự 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:--debugflag.
11. Tự kiểm tra
Q1App 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?▸
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 argument → jdbc:postgresql://prod2.
Thứ tự ưu tiên (cao đến thấp):
- Command line args (
--key=value) - Java System Properties (
-Dkey=value) - OS environment variables (
KEY=value) application-{profile}.ymlapplication.yml
Cách dùng trong CI/CD:
- Application defaults trong git:
application.ymlchứa default cho dev. Commit code. - Profile-specific config commit git:
application-prod.ymlchứ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.
Q2So sánh @Value và @ConfigurationProperties. Cho 1 ví dụ cụ thể nào nên dùng @Value, 1 ví dụ nên dùng @ConfigurationProperties.▸
@Value và @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-safe | String/primitive ok, complex type cần parse | Full type — nested object, List, Map, enum |
| Validation | Không (chỉ default value) | Có — `@Validated` + Jakarta Validation |
| SpEL | Có — #{...} | Không (pure binding) |
| IDE autocomplete | Không | Có (qua metadata) |
| Refactor-friendly | String literal — rename khó | Class field — IDE rename OK |
| Setup overhead | 0 — inline annotation | Class + getters/setters + register |
| Best for | 1-2 prop độc lập, SpEL | Group 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
▸
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- Indent typo:
hikari:indent 3 space thay 4 → YAML parse fail.hikaribị parse như sibling củapasswordthay child củadatasource. Spring Boot startup throwInvalidConfigurationPropertiesExceptionhoặc property không bind.Fix: indent đúng 4 space (hoặc 2 space đồng nhất toàn file):
Tránh issue này: dùng IDE với YAML linter (IntelliJ built-in, VSCode extension). Hoặc dùngspring: datasource: url: jdbc:postgresql://prod-db.internal:5432/app username: app_prod password: prod-secret-2026 hikari: maximum-pool-size: 50application.propertiescho file ngắn — flat, no indent. - Hardcode password trong git:
password: prod-secret-2026commit lên repo → security incident. Repository scanner (GitGuardian, GitHub Secret Scanning) detect và alert.Fix: reference env var:
Set env var qua K8s Secret hoặc Vault. Không bao giờ commit secret.spring: datasource: password: ${DB_PASSWORD} # required, fail nếu thiếu
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.
Q4K8s 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?▸
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:
- Env var name:
SPRING_DATASOURCE_URL(UPPER_SNAKE_CASE). - Boot convert: lowercase + replace
_bằng.→spring.datasource.url. - 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ằngexport spring.datasource.url=...sẽ fail. - Workaround: dùng
envcommand: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".
Q5Bạ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?▸
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ự:
- Load
application.yml→ propertyapp.urlset giá trị "default". - Detect profile
prodactive → loadapplication-prod.yml→ overrideapp.urlvới giá trị "prod". application-staging.ymlkhô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:
- Bỏ
app.urlkhỏiapplication-prod.yml(đơn giản nhất). Chỉ override property thực sự khác giữa env. Property chung giữ trongapplication.yml. - Dùng env var với priority cao hơn file: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.
# K8s manifest env: - name: APP_URL value: "global-default" - Dùng default value trong code:Default trong code thay file — không thể override từ file. Hữu ích khi default thực sự là "constant".
@ConfigurationProperties(prefix = "app") public record AppProps(String url) { public AppProps { if (url == null) url = "global-default"; // canonical default } }
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 { ... }
▸
@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 đề:
- Không validate: nếu user không set
app.email.host, fieldhost= 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 ) {} - Không register class:
@ConfigurationPropertieskhông tự register thành bean. Cần 1 trong 2:Quên 1 trong 2 → record không bind, inject bean fail với// Cach 1: scan toan project @SpringBootApplication @ConfigurationPropertiesScan public class App { ... } // Cach 2: register cu the @SpringBootApplication @EnableConfigurationProperties(EmailProps.class) public class App { ... }NoSuchBeanDefinitionException. - 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:
Annotation processor đọc<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>EmailPropslúc compile, sinhspring-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: truePattern 2026 chuẩn: record + @Validated + Javadoc + @ConfigurationPropertiesScan + metadata generator + env var binding.
Q7Production 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?▸
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:
- Check
/actuator/configprops: in ra mọi@ConfigurationPropertiesbean + bound value. Tìmapp.timeout→ nếu null hoặc missing → confirm cause. - Check
/actuator/env: tìmapp.timeouttrong PropertySource. Nếu không có → user quên config. - Check log startup: nếu có
@Validated+@NotNull, app sẽ fail-fast tại startup vớiBindValidationExceptionrõ ràng. Không có validation → bug latent.
Fix:
- Thêm validation để fail-fast lần sau:Thiếu config → app không start, log clear: "Field 'timeout': must not be null".
@ConfigurationProperties(prefix = "app") @Validated public record AppProps( @NotNull Duration timeout, // bat buoc String name ) {} - 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 } } - 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...