Spring Core & Boot/Environment & PropertySource — externalized config và thứ tự ưu tiên
11/41
Bài 11 / 41~12 phútContainer InternalsMiễn phí lượt xem

Environment & PropertySource — externalized config và thứ tự ưu tiên

Environment là 1 trong 4 capability của ApplicationContext: quản lý property (từ command line, env var, application.properties, ...) và profile (dev/prod/test). Bài này bóc cơ chế PropertySource ordering, relax binding, @Value/${...}/SpEL — giải thích vì sao command line override mọi thứ và vì sao env var tồn tại song song system property.

TL;DR: Environment là lớp trừu tượng Spring đặt giữa app code và toàn bộ nguồn cấu hình bên ngoài. Nó quản lý hai thứ: property (giá trị config từ command line, env var, application.properties, ...) và profile (dev/prod/test). Các nguồn config được xếp thứ tự ưu tiên — command line ưu tiên cao nhất, rồi đến system property, env var, file application-{profile}.properties, cuối cùng là application.properties. Khi resolve ${db.url}, Spring tra lần lượt từ nguồn ưu tiên cao đến thấp, dừng khi tìm được giá trị đầu tiên. @Value("${...}") resolve qua Environment; @Value("#{...}") là SpEL — biểu thức mạnh hơn, truy cập được system property, gọi method. Relax binding (DB_URLdb.url) tồn tại để config dễ dàng trên K8s/Docker.

Bài BeanFactory vs ApplicationContext đã liệt kê Environment là 1 trong 4 capability mà ApplicationContext cộng thêm vào BeanFactory. Bài này bóc đúng capability đó: bên dưới nó làm gì, tại sao thứ tự ưu tiên lại như vậy, và tại sao relax binding tồn tại.

1. Environment là gì — và vì sao cần nó

Trước Spring, code Java đọc config theo kiểu:

// Canh khi muon doi env, phai sua code hoac rebuild
String dbUrl = System.getProperty("db.url");
String apiKey = System.getenv("API_KEY");
String fromFile = bundle.getString("app.name");

Ba API khác nhau cho ba nguồn khác nhau. Không có ưu tiên, không có override. Muốn prod dùng env var mà dev dùng file — tự viết logic.

Environment giải quyết điều này bằng cách tập trung toàn bộ nguồn config vào một interface duy nhất:

// Dieu application code thuc su can:
String value = environment.getProperty("db.url");
// Spring tu tra theo thu tu uu tien, app code khong can biet nguon nao win

Bạn không tự gọi environment.getProperty trong service — Spring làm điều đó cho bạn khi resolve @Value. Nhưng hiểu interface này là hiểu tại sao @Value lại "biết" cần lấy từ đâu.

Environment quản lý hai phần tách biệt:

PhầnVí dụ
PropertiesKey-value config từ các nguồn bên ngoàidb.url=jdbc:postgresql://prod/app
ProfilesNhãn môi trường, kích hoạt nhóm bean/config khác nhauspring.profiles.active=dev

2. PropertySource — đơn vị đóng gói một nguồn config

PropertySource là abstraction cho một nguồn config cụ thể. Mỗi nguồn được đóng gói thành một PropertySource object. Spring Boot xếp chúng vào một danh sách có thứ tự — MutablePropertySources — bên trong Environment.

Khi resolve ${db.url}, Environment.getProperty("db.url") duyệt qua danh sách theo thứ tự từ đầu đến cuối, trả về giá trị từ PropertySource đầu tiên có key đó. Các PropertySource phía sau không được hỏi nữa.

Đây chính là cơ chế override: PropertySource có ưu tiên cao hơn được đặt đầu danh sách.

flowchart TB
    Q["environment.getProperty('db.url')"]
    PS1["PropertySource 1: CommandLineArgs<br/>co 'db.url'?"]
    PS2["PropertySource 2: SystemProperties<br/>co 'db.url'?"]
    PS3["PropertySource 3: EnvVariables<br/>co 'db.url'?"]
    PS4["PropertySource 4: application.properties<br/>co 'db.url'?"]
    RET["Tra ve gia tri tim thay"]
    NULL["Tra ve null"]

    Q --> PS1
    PS1 -->|"CO - dung lai"| RET
    PS1 -->|"KHONG"| PS2
    PS2 -->|"CO - dung lai"| RET
    PS2 -->|"KHONG"| PS3
    PS3 -->|"CO - dung lai"| RET
    PS3 -->|"KHONG"| PS4
    PS4 -->|"CO"| RET
    PS4 -->|"KHONG"| NULL

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

Spring Boot xếp các nguồn theo thứ tự cố định. Đây là thứ tự ưu tiên cao đến thấp cho các nguồn thường gặp nhất:

Thứ tựNguồnVí dụ
1 (cao nhất)Command line arguments--db.url=jdbc:postgresql://prod/app
2System properties (JVM)-Ddb.url=jdbc:postgresql://prod/app
3OS environment variablesDB_URL=jdbc:postgresql://prod/app
4application-{profile}.propertiesapplication-prod.properties
5 (thấp hơn)application.propertiesCấu hình mặc định
Danh sách đầy đủ

Spring Boot Reference docs có bảng 17 mục đầy đủ tại Externalized Configuration. Bảng trên chỉ liệt kê 5 nguồn hay gặp nhất trong thực tế.

3.1 Vì sao command line override mọi thứ

Đây là thiết kế có chủ đích. Contract của externalized config là: thay đổi behavior của app mà không cần build lại.

Trong pipeline CI/CD, kịch bản điển hình:

  • application.properties trong source code chứa cấu hình dev: db.url=jdbc:postgresql://localhost/dev
  • Khi deploy staging, CD pipeline truyền: java -jar app.jar --db.url=jdbc:postgresql://staging/app
  • Khi deploy prod, truyền: java -jar app.jar --db.url=jdbc:postgresql://prod/app

Cùng một jar, ba môi trường, ba cấu hình — không build lại, không sửa file. Command line là điểm override cuối cùng, có quyền cao nhất, cho phép người vận hành điều chỉnh runtime mà không động vào artifact.

3.2 Vì sao env var tồn tại bên cạnh system property

System.getenv() (env var của OS) và System.getProperty() (JVM system property qua -D) là hai cơ chế khác nhau. Nhiều người nhầm chúng tương đương.

Sự khác biệt quan trọng trong thực tế triển khai:

Env var (OS)System property (JVM -D)
Đặt ở đâuDocker ENV, K8s env:, shell exportJVM startup flag -Dkey=value
Thừa kế child processCó — child process kế thừa env var của parentKhông — chỉ sống trong JVM hiện tại
Phù hợp choK8s Secret, ConfigMap, 12-factor appOverride ngắn hạn, test local
Ưu tiên trong SpringThấp hơn system propertyCao hơn env var

K8s inject secret vào container qua env var vì đây là cơ chế tiêu chuẩn container runtime hỗ trợ. Đây là lý do relax binding tồn tại (xem phần 4).

Pitfall hay gặp:

# application.properties:
db.url=jdbc:postgresql://localhost/dev

# K8s ConfigMap dat env var:
DB_URL=jdbc:postgresql://prod/app

# Developer debug bang system property:
java -Ddb.url=jdbc:postgresql://test/app -jar app.jar

Kết quả resolve: -Ddb.url (system property, ưu tiên cao hơn env var) → dùng jdbc:postgresql://test/app, không phải giá trị K8s. Nếu không biết thứ tự ưu tiên, debug sẽ bối rối.

4. Relax binding — cầu nối giữa env var và property key

Env var theo quy ước POSIX chỉ dùng chữ hoa và dấu gạch dưới: DB_URL, APP_MAX_CONNECTIONS. Property key trong Spring dùng chữ thường và dấu chấm: db.url, app.max-connections.

Nếu không có relax binding, developer phải đặt env var tên db.url (có dấu chấm) — không hợp lệ ở hầu hết shell.

Spring Boot giải quyết bằng relax binding: khi tra key db.url, Spring cũng chấp nhận:

  • DB_URL — chữ hoa + gạch dưới thay dấu chấm
  • DBURL — chữ hoa không phân cách
  • db_url — gạch dưới thay dấu chấm
  • db.url — dạng chuẩn

Bảng mapping đầy đủ cho key app.max-connections:

DạngVí dụ
Chuẩn (camelCase/kebab-case)app.maxConnections hoặc app.max-connections
Chữ hoa + gạch dưới (env var)APP_MAX_CONNECTIONS
Chữ thường + gạch dướiapp_max_connections

Cơ chế này không phải magic — Binder trong Spring Boot đọc key gốc (app.max-connections), chuẩn hoá nó về dạng canonical, rồi so khớp với từng PropertySource theo mọi biến thể tương đương.

# K8s deployment.yaml -- inject qua env var
env:
  - name: DB_URL
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: url
  - name: APP_MAX_CONNECTIONS
    value: "50"
// Application code -- dung property key chuan
@Value("${db.url}")
private String dbUrl;               // lay tu DB_URL env var

@Value("${app.max-connections:20}")
private int maxConnections;         // lay tu APP_MAX_CONNECTIONS, default 20

Relax binding là lý do K8s deployment có thể inject config qua env var theo chuẩn POSIX, trong khi Spring code vẫn dùng property key dạng dấu chấm quen thuộc.

5. @Value và SpEL — hai cú pháp khác nhau

@Value có hai cú pháp, dùng cho hai mục đích khác nhau:

5.1 ${...} — property placeholder

@Value("${key}") yêu cầu Spring resolve key qua Environment.getProperty("key") — tức là tra PropertySource theo thứ tự ưu tiên.

@Service
public class OrderService {

    @Value("${db.url}")
    private String dbUrl;

    @Value("${db.pool-size:10}")      // default 10 neu key khong co
    private int poolSize;

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

Cú pháp ${key:defaultValue} cho phép khai báo giá trị mặc định ngay tại annotation — hữu ích khi property không bắt buộc phải có.

Spring Boot 3 cũng hỗ trợ ${key:} (default rỗng) và ${key:${other.key}} (default là giá trị của key khác).

5.2 #{...} — SpEL (Spring Expression Language)

SpEL (Spring Expression Language) là ngôn ngữ biểu thức Spring. @Value("#{...}") không tra PropertySource — thay vào đó, Spring evaluate biểu thức trả về giá trị.

@Service
public class SystemInfo {

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

    @Value("#{systemEnvironment['HOME']}")
    private String homeDir;              // tra System.getenv()

    @Value("#{T(java.lang.Runtime).getRuntime().availableProcessors()}")
    private int cpuCount;                // goi static method Java

    @Value("#{@appConfig.maxRetries * 2}")
    private int doubleRetries;           // tham chieu bean khac trong context
}

SpEL là ngôn ngữ đầy đủ: toán tử số học, so sánh, ternary, regex match, truy cập field/method, gọi constructor, làm việc với collection. Tuy nhiên trong production code, nên tránh SpEL phức tạp trong @Value — logic đó nên nằm trong Java code thay vì annotation, dễ test và debug hơn.

Bảng so sánh hai cú pháp:

${...}#{...}
Nguồn dữ liệuEnvironment (PropertySource)SpEL evaluate
Dùng choConfig từ file/env var/command lineBiểu thức, system property, tham chiếu bean
Có thể kết hợp@Value("#{environment['db.url']}")Truy cập Environment qua SpEL
Khi nào dùngHầu hết config thực tếBiểu thức động, hiếm gặp
Nguyên tắc chọn

Dùng ${...} cho config externalized (property từ file/env). Dùng #{...} khi cần evaluate biểu thức hoặc truy cập system property trực tiếp. Khi cả hai đều làm được, ưu tiên ${...} vì dễ override qua env var hơn.

6. Profiles — cấu hình theo môi trường

Profile là phần còn lại của Environment. Profile là một nhãn (string) để kích hoạt nhóm bean hoặc property file theo môi trường.

Kích hoạt profile:

# Cach 1: command line (uu tien cao nhat)
java -jar app.jar --spring.profiles.active=prod

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

# Cach 3: application.properties
spring.profiles.active=dev

Khi profile prod active, Spring Boot tự động nạp application-prod.properties (hoặc .yml) bên cạnh application.properties. File profile-specific có ưu tiên cao hơn file mặc định.

Profile còn dùng để kích hoạt bean theo điều kiện:

@Configuration
@Profile("dev")
public class DevDataConfig {

    @Bean
    public DataSource dataSource() {
        // H2 in-memory cho dev
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }
}

Bean DevDataConfig chỉ được tạo khi profile dev active. Profile prod sẽ có ProdDataConfig tương ứng với DataSource kết nối thật.

Profile activation trong test

Trong test, dùng @ActiveProfiles("test") thay vì đặt spring.profiles.active trong file. Property spring.profiles.activeapplication.properties có thể bị override bởi môi trường, gây test chạy với profile khác ngoài ý muốn.

7. Cơ chế bên dưới — PropertySource chain trong heap

Khi SpringApplication.run() khởi động, ConfigurableEnvironment được tạo trước khi ApplicationContext refresh. Đây là bước ApplicationEnvironmentPreparedEvent trong vòng đời container.

Trong heap, StandardEnvironment (hoặc StandardServletEnvironment cho web app) giữ một MutablePropertySources — một danh sách PropertySource có thứ tự:

StandardServletEnvironment
 └─ propertySources : MutablePropertySources
     ├─ [0] CommandLinePropertySource        -- uu tien cao nhat
     ├─ [1] SystemPropertiesPropertySource   -- System.getProperties()
     ├─ [2] SystemEnvironmentPropertySource  -- System.getenv(), co relax binding
     ├─ [3] RandomValuePropertySource        -- ${random.int}, ${random.uuid}
     ├─ [4] Config: application-prod.yaml    -- profile-specific
     └─ [5] Config: application.yaml         -- default, uu tien thap nhat

environment.getProperty(key) là một vòng lặp đơn giản:

// Logic don gian ben duoi (pseudocode)
for (PropertySource<?> ps : this.propertySources) {
    Object value = ps.getProperty(key);
    if (value != null) {
        return convertIfNecessary(value, targetType);
    }
}
return null;

Không có thuật toán phức tạp — chỉ là duyệt danh sách có thứ tự, dừng lại khi tìm thấy. Thứ tự trong danh sách chính là thứ tự ưu tiên.

BeanFactoryPostProcessor tên PropertySourcesPlaceholderConfigurer chạy ở bước 5 của refresh() — nó resolve toàn bộ ${...} trong @Value annotation bằng cách gọi environment.getProperty() cho từng placeholder. Đây là cầu nối giữa PropertySource chain và annotation @Value trong bean.

8. Pitfall

Pitfall 1: Kỳ vọng env var override system property

# Dat env var:
DB_URL=jdbc:postgresql://staging/app

# Chay voi system property:
java -Ddb.url=jdbc:postgresql://local/dev -jar app.jar

Kết quả: db.url resolve thành jdbc:postgresql://local/dev (system property thắng env var). Thứ tự là: system property ưu tiên cao hơn env var. Nhiều developer nghĩ ngược lại vì quen với shell script (env var override child process).

✅ Để env var thắng: bỏ -Ddb.url, chỉ dùng DB_URL. Hoặc dùng command line --db.url=... nếu muốn ưu tiên cao nhất.

Pitfall 2: @Value không hoạt động trong @Configuration class static method

@Configuration
public class AppConfig {

    @Value("${db.pool-size:10}")
    private int poolSize;  // KHONG inject duoc trong @Bean method ben duoi

    @Bean
    public DataSource dataSource() {
        // poolSize co the la 0 o day vi BPP inject @Value chua chay
        config.setMaximumPoolSize(poolSize);
    }
}

@Value được inject bởi AutowiredAnnotationBeanPostProcessor (BPP) — chạy sau khi bean được tạo. @Configuration class bản thân là một bean; nhưng @Bean method trong nó được gọi trong quá trình instantiate AppConfig bean — đôi khi trước khi @Value được inject xong.

✅ Dùng constructor injection hoặc nhận tham số trực tiếp vào @Bean method:

@Configuration
public class AppConfig {

    @Bean
    public DataSource dataSource(
            @Value("${db.pool-size:10}") int poolSize) {
        // poolSize duoc inject dung luc vao tham so method -- an toan
        config.setMaximumPoolSize(poolSize);
        return new HikariDataSource(config);
    }
}

Pitfall 3: Nhầm prefix env var cho profile activation

# SAI: khong co effect
SPRING_PROFILES=prod java -jar app.jar

# DUNG: ten chinh xac la SPRING_PROFILES_ACTIVE
SPRING_PROFILES_ACTIVE=prod java -jar app.jar

Relax binding chuyển SPRING_PROFILES_ACTIVE thành spring.profiles.active — tên property đầy đủ. Bỏ _ACTIVE không có relax binding nào map tới key đúng.

9. Deep Dive

Tài liệu chính chủ

Đọc để hiểu thêm cơ chế và tham số chính thức:

Liên hệ các bài khác

  • BeanFactory vs ApplicationContextEnvironment là 1 trong 4 capability ApplicationContext cộng thêm vào BeanFactory. Bài đó giới thiệu khái niệm, bài này đào sâu cơ chế bên dưới.
  • Externalized configuration — đào sâu @ConfigurationProperties, type-safe binding cho nhóm property, validation với @Validated. Đây là cách hiện đại hơn so với nhiều @Value rải rác.
  • Profiles — profile activation nâng cao, @ConditionalOnProfile, profile groups, test với @ActiveProfiles. Bài này chỉ giới thiệu profile ở mức cơ bản.

Tóm tắt

  • Environment là interface trung tâm quản lý property và profile — app code không cần biết config đến từ file hay env var.
  • PropertySource là abstraction cho một nguồn config. MutablePropertySources giữ danh sách PropertySource có thứ tự — thứ tự đó chính là thứ tự ưu tiên.
  • Thứ tự ưu tiên: command line ưu tiên cao nhất → system property → env var → application-{profile}.propertiesapplication.properties.
  • Command line override mọi thứ: contract cho phép thay đổi config theo môi trường mà không build lại artifact.
  • Env var ưu tiên thấp hơn system property — pitfall hay gặp khi kết hợp -D với env var trong K8s.
  • Relax binding: DB_URLdb.url, APP_MAX_CONNECTIONSapp.max-connections — cầu nối giữa chuẩn POSIX env var và property key Spring.
  • @Value("${...}") resolve qua Environment; @Value("#{...}") là SpEL evaluate biểu thức.
  • PropertySourcesPlaceholderConfigurer (BFPP, bước 5 của refresh()) resolve toàn bộ ${...} trước khi bean được instantiate.

Tự kiểm tra

Tự kiểm tra
Q1
App của bạn có 3 nguồn config đặt db.url: (1) application.properties với giá trị localhost, (2) env var DB_URL với giá trị staging, (3) JVM flag -Ddb.url với giá trị prod. Giá trị nào được dùng? Giải thích theo cơ chế PropertySource chain.

Giá trị được dùng là prod từ JVM flag -Ddb.url (system property).

Cơ chế: Environment.getProperty("db.url") duyệt danh sách MutablePropertySources theo thứ tự ưu tiên từ cao đến thấp. SystemPropertiesPropertySource (giữ giá trị từ -D flags) đứng trước SystemEnvironmentPropertySource (env var) trong danh sách. Vòng lặp dừng ngay khi tìm thấy key ở nguồn đầu tiên — system property thắng.

Thứ tự đầy đủ của 3 nguồn: system property ưu tiên cao hơn env var, env var ưu tiên cao hơn application.properties. Command line --db.url=... sẽ thắng tất cả nếu được thêm vào.

Q2
Giải thích vì sao Spring Boot cần relax binding. Cho ví dụ: key app.max-connections trong Spring code tương ứng với env var nào trong K8s deployment YAML?

Relax binding tồn tại vì quy ước đặt tên xung đột nhau: env var theo chuẩn POSIX chỉ dùng chữ hoa và gạch dưới (vd APP_MAX_CONNECTIONS), trong khi property key Spring dùng chữ thường và dấu chấm/gạch nối (vd app.max-connections). Nếu không có relax binding, developer phải đặt env var tên app.max-connections có dấu chấm — không hợp lệ ở hầu hết shell và không phải chuẩn K8s.

Với relax binding, app.max-connections tương ứng với env var APP_MAX_CONNECTIONS trong K8s deployment YAML:

env:
- name: APP_MAX_CONNECTIONS
  value: "100"

Binder trong Spring Boot chuẩn hoá cả hai về dạng canonical (app.max-connections) trước khi so khớp, nên hai cách viết được coi là tương đương.

Q3
Phân biệt @Value("${db.url}")@Value("#{systemProperties['db.url']}"). Hai cái này khác nhau ở đâu trong cơ chế resolve?

${db.url} là property placeholder — Spring gọi environment.getProperty("db.url"), duyệt toàn bộ PropertySource chain theo thứ tự ưu tiên. Kết quả có thể đến từ command line, env var, file properties — tuỳ nguồn nào có key đó và ưu tiên cao nhất.

#{systemProperties['db.url']} là SpEL — Spring evaluate biểu thức, truy cập trực tiếp System.getProperties() (JVM system properties). Nguồn duy nhất là JVM -D flag; env var, command line Spring, và file properties đều bị bỏ qua.

Hệ quả thực tế: với SpEL systemProperties, bạn chỉ tìm được giá trị từ -Ddb.url=..., không tìm được từ DB_URL env var hay application.properties. Với ${db.url}, bạn nhận được giá trị từ nguồn ưu tiên cao nhất hiện có, kể cả env var. Trong thực tế, hầu như luôn nên dùng ${...}.

Q4
PropertySourcesPlaceholderConfigurerBeanFactoryPostProcessor. Giải thích tại sao việc resolve ${...} phải chạy ở bước BFPP (bước 5 của refresh()), không thể hoãn đến sau khi bean được tạo.

@Value("$...") được nhúng vào bean definition dưới dạng placeholder string tại bước load definition (bước 2 của refresh()). Ví dụ, @Value("$db.url") trên field được lưu trong BeanDefinition như một chuỗi "$db.url" — chưa resolve.

BFPP chạy ở bước 5 — trước khi bất kỳ bean nào được instantiate (bước 11). PropertySourcesPlaceholderConfigurer duyệt toàn bộ bean definition, tìm $${...}, gọi environment.getProperty() để lấy giá trị thật, ghi đè ngược vào definition.

Nếu resolve diễn ra sau khi bean đã tạo, thì lúc constructor được gọi, giá trị field vẫn là chuỗi literal "$db.url" thay vì URL thật — bean tạo ra với config sai. BFPP là giai đoạn can thiệp metadata trước instantiate — đây chính xác là mục đích của nó.

Q5
Bạn set DB_URL=jdbc:postgresql://staging/app trong K8s env nhưng khi debug local bạn chạy java -Ddb.url=jdbc:postgresql://local/dev -jar app.jar. App dùng DB nào? Làm sao để env var K8s thắng trong môi trường staging mà không ảnh hưởng local?

App dùng local (jdbc:postgresql://local/dev) vì system property (-Ddb.url) ưu tiên cao hơn env var (DB_URL) trong PropertySource chain.

Để env var K8s thắng trong staging: không truyền -Ddb.url khi deploy lên staging. Để env var hoạt động đúng, không có PropertySource nào có ưu tiên cao hơn ghi đè nó.

Best practice tách local vs staging:

  • Local: dùng application-local.properties với giá trị local, activate bằng --spring.profiles.active=local. Không dùng -D flags vì chúng có ưu tiên cao hơn env var.
  • Staging/prod: inject hoàn toàn qua env var K8s — không có -D flag nào được pass vào JVM command trong container.
  • Rule đơn giản: env var là nguồn duy nhất cho staging/prod; -D flag chỉ dùng local debug ngắn hạn, biết rõ nó override env var.

Bài tiếp theo: Resource & @SpringBootApplication

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