Command Palette

Search for a command to run...

[Spring Boot Basics] application.properties và application.yml trong Spring Boot: cú pháp và @Value

Mọi project do Spring Initializr sinh ra đều có sẵn src/main/resources/application.properties với đúng một dòng, spring.application.name=demo. Series này đã ghi vào file đó ba lần — đổi port, bật lazy initialization, exclude một auto-configuration — mà chưa hề nói gì về chính file đó. Bài này nói về file đó: hai định dạng Spring Boot đọc thực sự chấp nhận những gì, mỗi định dạng âm thầm làm gì mà bạn không hề yêu cầu, và một giá trị đi từ một dòng text vào constructor parameter như thế nào.

Cú pháp trông đơn giản đến mức chẳng cần cả một bài viết, và đó chính là vấn đề. File .properties được decode bằng ISO-8859-1, nên Xin chào đến tay bạn thành Xin chà o. YAML biến 0123 thành 83on thành true mà không một lời cảnh báo. Chỉ một @Value thiếu default là đủ khiến application không khởi động được. Tất cả đều đã được chạy thật, và output bên dưới được trích nguyên văn.

Một file .properties phẳng và một file .yml lồng nhau cùng đổ vào một @Value

Mọi thứ bên dưới chạy trên OpenJDK 21.0.6 với Spring Boot 4.1.1 (Spring Framework 7.0.9, SnakeYAML 2.6) và Gradle 9.7.1, trên project sinh bởi Spring Initializr với dependencies=web. Mọi giá trị parse ra, kết quả convert và error message đều được copy từ các lần chạy project đó.

Spring Boot đọc application.properties và application.yml từ đâu

Mọi thứ trong src/main/resources được copy vào gốc classpath: build/resources/main khi đang phát triển, BOOT-INF/classes/ bên trong jar. Lúc khởi động, Boot tìm ở đó application.properties, application.ymlapplication.yaml, parse từng file tìm thấy thành một property source, rồi thêm các property source đó vào Environment. @Value, Environment.getProperty() và mọi thứ khác đọc cấu hình đều đọc từ đó.

Một property source là một map phẳng với key là String, và đây là điều hữu ích nhất trong cả bài: file trông như thế nào không quan trọng, application chỉ thấy một danh sách key nối bằng dấu chấm. Cách nhanh nhất để thấy tận mắt là in map đó ra. Runner dưới đây duyệt Environment và in mọi property source được load từ file cấu hình:

ConfigFileDump.java
@Component
public class ConfigFileDump implements ApplicationRunner {
 
    private final ConfigurableEnvironment environment;
 
    public ConfigFileDump(ConfigurableEnvironment environment) {
        this.environment = environment;
    }
 
    @Override
    public void run(ApplicationArguments args) {
        for (PropertySource<?> source : environment.getPropertySources()) {
            if (source instanceof OriginTrackedMapPropertySource file) {
                System.out.println(file.getName());
                for (String key : file.getPropertyNames()) {
                    Object value = file.getProperty(key);
                    System.out.println("  " + key + " = " + value + " (" + value.getClass().getSimpleName() + ")");
                }
            }
        }
    }
}

Boot dùng cùng một class, OriginTrackedMapPropertySource, cho cả hai định dạng. Mọi bản dump trong phần còn lại của bài đều lấy từ runner này, hoặc từ việc đọc từng key bằng Environment.getProperty().

Cú pháp application.properties

Với file .properties, Boot không dùng java.util.Properties: PropertiesPropertySourceLoader giao file cho loader riêng của Boot là OriginTrackedPropertiesLoader. Vì vậy các quy tắc dưới đây được kiểm tra trực tiếp trên Boot — ghi từng dòng vào application.properties rồi đọc lại — chứ không chép từ Javadoc của Properties.

Dấu phân cách: =, : và khoảng trắng

application.properties
app.eq=equals sign
app.colon: colon separator
app.space whitespace separator
app.spaced   =   around the separator
    app.indented=leading spaces before the key

Thêm ba dấu cách phía sau around the separator, đây là những gì Boot lưu lại:

DòngKeyGiá trị
app.eq=equals signapp.eqequals sign
app.colon: colon separatorapp.coloncolon separator
app.space whitespace separatorapp.spacewhitespace separator
app.spaced = around the separatorapp.spacedaround the separator cộng ba dấu cách ở cuối — 23 ký tự
app.indented=leading spaces before the keyapp.indentedleading spaces before the key

Key kết thúc ở ký tự =, : hoặc khoảng trắng đầu tiên không bị escape. Khoảng trắng trước key và ở hai bên dấu phân cách bị bỏ đi; khoảng trắng ở cuối giá trị thì được giữ lại. Một dấu cách thừa sau password hay URL sẽ thành một phần của nó, và không có gì cảnh báo bạn.

Comment, nối dòng và key trùng lặp

application.properties
# a comment starting with a hash
! a comment starting with an exclamation mark
app.hash=value # not a comment
app.continued=first,\
              second,\
              third
app.empty=
app.dup=first
app.dup=second
  • Dòng có ký tự đầu tiên (không tính khoảng trắng) là # hoặc ! là comment và không tạo ra key nào.
  • app.hashvalue # not a comment. Dấu # ở giữa dòng là một phần của giá trị — YAML làm ngược lại, như bạn sẽ thấy ở dưới.
  • app.continuedfirst,second,third. Dấu gạch chéo ngược ở cuối dòng nối dòng đó với dòng sau, và khoảng trắng đầu dòng sau bị bỏ đi, nên các dòng nối có thể thụt lề tùy ý.
  • app.empty tồn tại và chứa chuỗi rỗng. Nó không phải key bị thiếu, điều này sẽ quan trọng khi nói tới default.
  • app.dupsecond. Định nghĩa cuối cùng thắng, không một lời báo.

Escape, dấu gạch chéo ngược và đường dẫn Windows

application.properties
app.tab=a\tb
app.newline=line1\nline2
app.unicode=Xin ch\u00e0o
app.backslash=C:\\temp\\new
app.single-backslash=C:\temp\new
app.key\ with\ spaces=ok
app.equals\=in\:key=ok
app.value-with-equals=a=b
app.value-with-colon=http://localhost:8080
DòngBoot lưu lại
app.tab=a\tba, một ký tự TAB, b
app.newline=line1\nline2line1, một ký tự xuống dòng, line2
app.unicode=Xin ch\u00e0oXin chào
app.backslash=C:\\temp\\newC:\temp\new
app.single-backslash=C:\temp\newC:, TAB, emp, xuống dòng, ew — 9 ký tự
app.key\ with\ spaces=okkey app.key with spaces
app.equals\=in\:key=okkey app.equals=in:key
app.value-with-equals=a=bgiá trị a=b
app.value-with-colon=http://localhost:8080giá trị http://localhost:8080

Đường dẫn Windows là thứ gây hại nhiều nhất trong thực tế: \t\n là escape, nên C:\temp\new được load thành một tab và một ký tự xuống dòng mà không báo lỗi gì. Hãy nhân đôi mọi dấu gạch chéo ngược. Chỉ ký tự phân cách nằm trong key mới cần escape; =: trong giá trị được giữ nguyên.

List: giá trị phân cách bằng dấu phẩy và key có chỉ số

application.properties
app.names=alice,bob,carol
app.servers[0]=alpha
app.servers[1]=beta

Đây là hai thứ khác nhau. app.names là một key chứa một chuỗi; nó chỉ trở thành list khi có thứ gì đó convert nó, và @Value vào List<String> làm được việc đó, như phần sau sẽ cho thấy. app.servers[0]app.servers[1] là hai key riêng biệt, và không có key nào tên app.servers: getProperty("app.servers") trả về null. Key có chỉ số quan trọng vì đó chính là thứ mà list trong YAML biến thành.

Chữ tiếng Việt và mặc định ISO-8859-1

Ghi một giá trị tiếng Việt vào file vừa sinh ra. Editor nào hiện nay cũng lưu bằng UTF-8, và file xác nhận điều đó:

application.properties
app.greeting=Xin chào
Text
application.properties: Unicode text, UTF-8 text

Đọc lại rồi in chuỗi, độ dài và các code point của nó:

Java
String greeting = environment.getProperty("app.greeting");
System.out.println(greeting + " | length " + greeting.length());
greeting.chars().forEach(c -> System.out.printf("U+%04X ", c));
Text
Xin chà o | length 9
U+0058 U+0069 U+006E U+0020 U+0063 U+0068 U+00C3 U+00A0 U+006F

à trong UTF-8 là hai byte, C3 A0. Loader .properties của Boot decode file bằng ISO-8859-1 nếu không được chỉ định charset khác — reader bên trong OriginTrackedPropertiesLoader rơi về StandardCharsets.ISO_8859_1 khi không có charset nào được truyền vào — nên hai byte biến thành hai ký tự: Ã và một dấu cách không ngắt (non-breaking space). @Value("${app.greeting}") cũng inject đúng chín ký tự đó.

Không khâu nào khác gây ra lỗi, và cũng không khâu nào che giấu nó. processResources của Gradle copy file nguyên từng byte — cmp báo file gốc, build/resources/main/application.properties và entry bên trong jar giống hệt nhau — java -jar in ra đúng chuỗi hỏng đó, và console cũng vô can: stdout.encodingUTF-8 trong lần chạy này. File trông vẫn đúng trong editor vì các byte đều đúng; chỉ có bước decode là sai.

Cùng ba dòng code đó, chạy với từng cách sửa:

Nội dung trong fileIn ra
app.greeting=Xin chào trong application.propertiesXin chà o | length 9
app.greeting=Xin ch\u00e0o trong application.propertiesXin chào | length 8
greeting: Xin chào dưới app: trong application.ymlXin chào | length 8
spring.config.import=classpath:greeting.properties[encoding=utf-8], giá trị nằm trong greeting.propertiesXin chào | length 8
cùng lệnh import đó nhưng không có [encoding=utf-8]Xin chà o | length 9

Unicode escape chạy được, nhưng không ai đọc hay review nổi. YAML được đọc bằng UTF-8, nên đó là lựa chọn đơn giản nhất cho cấu hình có chữ tiếng Việt. Hint [encoding=utf-8] áp dụng cho file được import; bản thân config import sẽ được nói tới ở một bài sau trong series.

Cú pháp application.yml

YAML viết cùng những cặp key-value đó dưới dạng cây. Thụt lề — chỉ bằng dấu cách — quyết định cái gì thuộc về cái gì. Một key theo sau là dấu hai chấm và một dấu cách thì chứa giá trị; một key chỉ có dấu hai chấm mà không có gì phía sau là cha của các dòng thụt vào bên dưới.

Lồng nhau, list và map

application.yml
# the same configuration, written as YAML
app:
  name: Demo Shop
  owner:
    name: Hoang
    email: hoang@example.com
  servers:
    - alpha
    - beta
  regions: [eu-west, ap-southeast]
  limits: {requests: 100, burst: 20}

File đó có bốn dạng. appowner là map viết thành các khối thụt lề; servers là block list, mỗi dòng một phần tử - ; regions là flow list trong ngoặc vuông; limits là flow map trong ngoặc nhọn. Dạng block và dạng flow tạo ra cùng một dạng key, như bản dump bên dưới cho thấy. Dòng bắt đầu bằng # là comment.

Chuỗi nhiều dòng, dấu nháy và comment

application.yml
app:
  banner: |
    Welcome to Demo Shop
    Have a nice day
  description: >
    A folded string
    joins lines with spaces
  literal-strip: |-
    no trailing
    newline
  single: 'it''s ${app.name}'
  double: "tab\there, accent \u00e0"
  plain-hash: value # a comment

Những gì Boot lưu lại, viết dưới dạng string literal của Java để thấy rõ ký tự xuống dòng:

KeyGiá trị được lưuQuy tắc
app.banner"Welcome to Demo Shop\nHave a nice day\n"| giữ mọi ký tự xuống dòng, kể cả cái cuối cùng
app.description"A folded string joins lines with spaces\n"> gộp các dòng bằng dấu cách và giữ ký tự xuống dòng cuối
app.literal-strip"no trailing\nnewline"|- giữ các ký tự xuống dòng nhưng bỏ cái cuối cùng
app.single"it's ${app.name}"nháy đơn: không có escape, '' là một dấu nháy
app.double"tab\there, accent à"nháy kép xử lý \t, \u00e0 và các escape khác
app.plain-hash"value"# đứng sau một dấu cách bắt đầu comment

Ký tự xuống dòng cuối của | là một ký tự thật; nếu giá trị là token hay thứ gì đó cần so sánh, hãy dùng |-. Và dấu nháy không ngăn Spring resolve placeholder: getProperty("app.single") trả về it's Demo Shop. Dấu nháy là chuyện của YAML, còn ${...} được Spring resolve sau đó, trên chuỗi đã parse xong.

YAML được làm phẳng thành đúng những key đó

Runner dump chạy với file YAML ở ví dụ lồng nhau — bảy mục đầu tiên:

Text
Config resource 'class path resource [application.yml]' via location 'optional:classpath:/'
  app.name = Demo Shop (String)
  app.owner.name = Hoang (String)
  app.owner.email = hoang@example.com (String)
  app.servers[0] = alpha (String)
  app.servers[1] = beta (String)
  app.regions[0] = eu-west (String)
  app.regions[1] = ap-southeast (String)

Giờ xóa application.yml và viết cùng dữ liệu đó bằng .properties:

application.properties
app.name=Demo Shop
app.owner.name=Hoang
app.owner.email=hoang@example.com
app.servers[0]=alpha
app.servers[1]=beta
app.regions[0]=eu-west
app.regions[1]=ap-southeast
app.limits.requests=100
app.limits.burst=20
Text
Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'
  app.name = Demo Shop (String)
  app.owner.name = Hoang (String)
  app.owner.email = hoang@example.com (String)
  app.servers[0] = alpha (String)
  app.servers[1] = beta (String)
  app.regions[0] = eu-west (String)
  app.regions[1] = ap-southeast (String)

Cùng key, cùng thứ tự. getProperty("app.owner.email") trả về hoang@example.com dù đọc từ file nào, getProperty("app.limits.burst") trả về 20 với cả hai, và getProperty("app.servers") trả về null với cả hai — một list không bao giờ là một key riêng; chỉ các phần tử của nó mới là key. Ngoài bảy dòng ở trên, bản dump đầy đủ của hai file chỉ khác nhau một chi tiết: 100 của YAML được lưu thành Integer, 100 của .properties thành String, và khác biệt đó biến mất ngay khi giá trị được đọc ra dưới dạng chuỗi.

Cùng một cấu hình viết thành các dòng .properties phẳng và thành cây YAML lồng nhau, mỗi lá ứng với đúng một key trong Environment

Các bẫy YAML làm dừng startup hoặc âm thầm đổi giá trị

Lỗi YAML có hai loại. Loại ồn ào làm application dừng ngay lúc khởi động kèm một message của SnakeYAML. Loại im lặng thì parse bình thường nhưng đưa cho bạn một giá trị khác với cái bạn đã viết.

Tab, dấu hai chấm không quote và key trùng làm application dừng

Thụt lề một dòng bằng tab:

Text
14:44:47.209 [main] ERROR org.springframework.boot.SpringApplication -- Application run failed
while scanning for the next token
found character '\t(TAB)' that cannot start any token. (Do not use \t(TAB) for indentation)
 in 'reader', line 2, column 1:
    	name: Demo Shop
    ^

Viết dấu hai chấm kèm dấu cách bên trong một giá trị không có dấu nháy:

application.yml
app:
  message: Note: read this first
Text
14:44:48.005 [main] ERROR org.springframework.boot.SpringApplication -- Application run failed
mapping values are not allowed here
 in 'reader', line 2, column 16:
      message: Note: read this first
                   ^

Dấu hai chấm không có dấu cách phía sau thì vô hại, đó là lý do url: http://localhost:8080 chạy được mà không cần nháy. Hãy đặt giá trị trong dấu nháy — message: "Note: read this first" được load thành Note: read this first — mỗi khi nó chứa : hoặc #.

Khai báo cùng một key hai lần trong một map:

application.yml
app:
  name: Demo Shop
  name: Another Shop
Text
while constructing a mapping
 in 'reader', line 2, column 3:
      name: Demo Shop
      ^
found duplicate key name
 in 'reader', line 3, column 3:
      name: Another Shop
      ^

Boot cấu hình SnakeYAML để từ chối key trùng. SnakeYAML 2.6 thuần với option mặc định chỉ log WARNING: duplicate keys found rồi chạy tiếp, còn file .properties giữ giá trị cuối mà không nói gì. Lỗi y hệt cũng xuất hiện khi hai key khác nhau trong file nhưng bằng nhau sau khi resolve type: on:yes: trong cùng một map đều thành key boolean true và thất bại với found duplicate key true.

Giá trị âm thầm đổi type

File này parse không có lỗi nào:

application.yml
app:
  feature-enabled: on
  legacy-mode: off
  confirm-orders: yes
  country: NO
  pin: 0123
  release: 1.10
  backup-time: 12:30
  launch-date: 2026-10-10
  theme-color: #fff
  owner: ~
  pin-quoted: "0123"
  release-quoted: "1.10"
Text
Config resource 'class path resource [application.yml]' via location 'optional:classpath:/'
  app.feature-enabled = true (Boolean)
  app.legacy-mode = false (Boolean)
  app.confirm-orders = true (Boolean)
  app.country = false (Boolean)
  app.pin = 83 (Integer)
  app.release = 1.1 (Double)
  app.backup-time = 750 (Integer)
  app.launch-date = 2026-10-10 (String)
  app.theme-color =  (String)
  app.owner =  (String)
  app.pin-quoted = 0123 (String)
  app.release-quoted = 1.10 (String)

Bốn giá trị boolean chẳng ai yêu cầu, một mã PIN mất số 0 ở đầu và thành 83, một bản release giờ là 1.1, giờ backup giờ là số nguyên 750, và một mã màu giờ là chuỗi rỗng. Chỉ những giá trị có dấu nháy là còn nguyên như đã viết.

Dưới đây là mọi giá trị đã thử, với type mà SnakeYAML 2.6 tạo ra và giá trị Environment.getProperty trả về trong Boot 4.1.1. Cột cuối là cùng giá trị đó khi parse bằng một parser YAML 1.2 — package JavaScript yaml 2.8.3 ở chế độ mặc định YAML 1.2 core schema — vì phần lớn lời khuyên về những cái bẫy này được viết cho một phiên bản spec rồi đem áp dụng cho phiên bản kia:

Giá trị không quoteType trong SnakeYAML 2.6getProperty của BootYAML 1.2 core schema
onBooleantruechuỗi on
offBooleanfalsechuỗi off
yesBooleantruechuỗi yes
no, NOBooleanfalsechuỗi no, NO
TrueBooleantrueboolean true
yStringychuỗi y
0123Integer, đọc theo hệ bát phân83số 123
0189String, không phải số bát phân hợp lệ0189số 189
0o14String0o14số 12
0x1FInteger31số 31
1_000Integer1000chuỗi 1_000
1.10Double1.1số 1.1
1.2.3String1.2.3chuỗi 1.2.3
1e3Double1000.0số 1000
12:30Integer, cơ số 60750chuỗi 12:30
1:30:00Integer, cơ số 605400chuỗi 1:30:00
2026-10-10Date với SnakeYAML thuần, String trong Boot2026-10-10chuỗi 2026-10-10
null, ~ hoặc để trốngnullchuỗi rỗngnull
#ffflà comment, nên thành nullchuỗi rỗngnull
"0123", "on"String0123, onchuỗi

Ba kết luận từ bảng trên:

  1. SnakeYAML 2.6 là parser YAML 1.1, nên mọi application Spring Boot đọc application.yml đều theo quy tắc YAML 1.1. Những thay đổi của YAML 1.2 — on, yes, no là chuỗi bình thường, không còn số cơ số 60 — không áp dụng ở đây. Boot thêm hai thay đổi của riêng nó: ngày tháng vẫn là chuỗi, vì loader của Boot bỏ timestamp resolver của SnakeYAML (OriginTrackedYamlLoader.NoTimestampResolver), và null thành chuỗi rỗng.
  2. Một phần lời truyền miệng sai theo chiều ngược lại. y thuộc type boolean của YAML 1.1, và yaml 2.8.3 ở chế độ 1.1 đúng là biến nó thành true, nhưng SnakeYAML 2.6 vẫn để y là chuỗi. Và YAML 1.2 cũng không cứu bạn khỏi mọi thứ: ở đó 0123 vẫn mất số 0, còn 1.10 vẫn là 1.1.
  3. Thiệt hại rơi vào chuỗi, không phải boolean. Đọc on vào một boolean thì định dạng nào cũng cho trueapp.flag=on trong .properties cũng được convert thành true. Đọc cùng key đó vào String thì YAML cho "true" còn .properties cho "on". Mã quốc gia, mã PIN, số phiên bản và giờ trong ngày đều là chuỗi, và chính chúng là những giá trị bị hỏng.

Key cũng bị resolve type. Trong file thử nghiệm, on:0123: nằm dưới map trap.keys được lưu thành key trap.keys[true]trap.keys[83].

⚠️ Hãy đặt trong dấu nháy mọi giá trị YAML mang nghĩa văn bản mà có thể bị nhầm thành boolean, số, giờ hoặc null: "NO", "0123", "1.10", "12:30", "#fff". Quote một giá trị thực sự là số cũng chẳng mất gì, vì @Value vẫn convert chuỗi "8091" sang int như thường.

Có cả application.properties và application.yml thì file nào thắng?

Cả hai file đều được load, và với key có ở cả hai, application.properties thắng. Hai file đặt cạnh nhau trong src/main/resources:

KeyTrong .propertiesTrong .ymlgetProperty trả về
app.sourcefrom application.propertiesfrom application.ymlfrom application.properties
app.only-in-propertiespp
app.only-in-yamlyy

Hai file được gộp theo từng key chứ không phải chọn nguyên một file: key chỉ có trong một file vẫn được đọc từ file đó. Cấu hình còn có thể đến từ những nguồn nào khác, và thứ tự đầy đủ giữa tất cả các nguồn đó, là chủ đề của một bài sau; với hai file trong cùng một thư mục, đây là quy tắc duy nhất bạn cần.

Placeholder bên trong file cấu hình

Một giá trị có thể trỏ tới key khác bằng ${...}. Property source lưu nguyên văn text như đã viết, và placeholder được resolve mỗi lần giá trị được đọc.

application.properties
app.host=localhost
app.port=8091
app.url=http://${app.host}:${app.port}
app.timeout=${app.connect-timeout:30s}
app.instance-id=${random.uuid}
app.shard=${random.int(1,10)}
KeyLưu trong property sourcegetProperty trả về
app.urlhttp://${app.host}:${app.port}http://localhost:8091
app.timeout${app.connect-timeout:30s}30s — không có app.connect-timeout, nên phần text sau dấu : đầu tiên được dùng
app.instance-id${random.uuid}ca97c7cf-f034-4bde-8066-ac2bba99a24a, rồi d39d8360-189f-4186-a351-02cd34018e29 ở lần gọi sau
app.shard${random.int(1,10)}6, 73 qua ba lần gọi

Ba chi tiết dễ hiểu sai:

  • ${random.*} được tính lại mỗi lần đọc. Hai lần gọi getProperty("app.instance-id") trả về hai UUID khác nhau, và hai field cùng gắn @Value("${app.instance-id}") cũng nhận hai UUID khác nhau. Nếu instance id phải giữ nguyên suốt vòng đời của process, hãy đọc một lần rồi giữ trong một bean.
  • random.int(1,10) không bao gồm cận trên. 100.000 lần đọc cho ra đúng các giá trị từ 1 đến 9.
  • Key thiếu bên trong một giá trị chỉ gây lỗi khi giá trị đó được đọc, không phải lúc khởi động. Với app.broken-url=http://${app.nope}/x, application vẫn khởi động bình thường, và lần gọi getProperty("app.broken-url") đầu tiên sẽ throw:
Text
org.springframework.util.PlaceholderResolutionException: Could not resolve placeholder 'app.nope' in value "http://${app.nope}/x"

Placeholder hoạt động y hệt trong YAML, nơi url: http://${app.host}:${app.port} không cần dấu nháy. Để giữ nguyên văn ${ trong giá trị, đặt một dấu gạch chéo ngược trước dấu đô la — cú pháp placeholder của Spring dùng \ làm ký tự escape. Parser .properties tự nuốt một dấu gạch chéo ngược trước khi Spring nhìn thấy giá trị, nên ở đó cần hai:

FileCách viếtgetProperty trả về
.propertiesapp.dollar=\${literal}throw Could not resolve placeholder 'literal' in value "${literal}"
.propertiesapp.escaped-placeholder=\\${literal}${literal}
.yml, không nháy hoặc nháy đơnplain: \${not.a.placeholder}${not.a.placeholder}
.yml, nháy képdouble: "\\${not.a.placeholder}"${not.a.placeholder}

Đọc property bằng @Value

@Value inject một giá trị cấu hình đã được resolve vào bean trong lúc bean được tạo. Nó đặt được trên field, setter hoặc constructor parameter:

FieldStyle.java
@Component
public class FieldStyle {
 
    @Value("${app.name}")
    private String name;
}
SetterStyle.java
@Component
public class SetterStyle {
 
    private String name;
 
    @Value("${app.name}")
    public void setName(String name) {
        this.name = name;
    }
}

Cả hai đều in Demo Shop. Hãy ưu tiên constructor, với cùng lý do như mọi dependency khác: field có thể là final, và test có thể truyền giá trị vào trực tiếp. Dưới đây là một constructor nhận mỗi type phổ biến một giá trị, với cấu hình này:

application.properties
app.name=Demo Shop
app.host=localhost
app.port=8091
app.url=http://${app.host}:${app.port}
app.secure=true
app.names=alice,bob,carol
AppSettings.java
@Component
public class AppSettings {
 
    private final String url;
    private final int port;
    private final boolean secure;
    private final Duration timeout;
    private final List<String> names;
    private final String[] nameArray;
    private final int[] retryDelays;
 
    public AppSettings(@Value("${app.url}") String url,
                       @Value("${app.port}") int port,
                       @Value("${app.secure:false}") boolean secure,
                       @Value("${app.timeout:30s}") Duration timeout,
                       @Value("${app.names}") List<String> names,
                       @Value("${app.names}") String[] nameArray,
                       @Value("${app.retry-delays:100,200,400}") int[] retryDelays) {
        this.url = url;
        this.port = port;
        this.secure = secure;
        this.timeout = timeout;
        this.names = names;
        this.nameArray = nameArray;
        this.retryDelays = retryDelays;
    }
}

In ra từ bean sau khi application đã khởi động:

Text
url         = http://localhost:8091
port        = 8091 (int)
secure      = true (boolean)
timeout     = PT30S (Duration)
names       = [alice, bob, carol] (ArrayList, size 3)
nameArray   = [alice, bob, carol] (length 3)
retryDelays = [100, 200, 400] (int[])

Các bước theo thứ tự đằng sau @Value có placeholder và default, cùng hai cách nó làm startup thất bại

Mọi @Value đều đi qua cùng các bước, theo thứ tự mà DefaultListableBeanFactory.doResolveDependency chạy trong Spring Framework 7.0.9. Chuỗi trong annotation được đọc ra; placeholder được resolve dựa trên Environment, rơi về phần default sau dấu : đầu tiên khi thiếu key; biểu thức #{...} nếu có được tính; và chuỗi kết quả được convert sang type của parameter. Các phần tiếp theo đi qua lần lượt từng bước.

Giá trị mặc định với ${key:default}

Default là mọi thứ nằm sau dấu hai chấm đầu tiên bên trong ngoặc, và chỉ được dùng khi key không tồn tại. @Value("${app.timeout:30s}") Duration timeout inject PT30S khi file không có app.timeout, và PT45S sau khi thêm app.timeout=45s.

Vì chỉ dấu hai chấm đầu tiên tách key khỏi default, default có thể chứa dấu hai chấm của riêng nó, và có thể là một placeholder khác:

@Value(...)Được inject
"${app.fallback-url:http://localhost:8080/api}"http://localhost:8080/api
"${app.primary:${app.secondary:nested-default}}"nested-default
"app.name"app.name — không có ${} thì đó là chuỗi nguyên văn, không phải tra key

Dòng cuối compile được, khởi động được và inject chính tên của key. Đó là lỗi @Value dễ mắc nhất và cũng khó nhận ra nhất.

Thiếu key và không có default thì chuyện gì xảy ra?

ApiClient.java
@Component
public class ApiClient {
 
    private final String apiKey;
 
    public ApiClient(@Value("${app.api-key}") String apiKey) {
        this.apiKey = apiKey;
    }
}

Không có app.api-key ở đâu cả, application không khởi động được:

Text
2026-09-11T14:45:08.369+07:00  WARN 47311 --- [demo] [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'apiClient' defined in file [/…/demo/build/classes/java/main/com/example/demo/ApiClient.class]: Unexpected exception during bean creation
2026-09-11T14:45:08.370+07:00  INFO 47311 --- [demo] [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2026-09-11T14:45:08.374+07:00  INFO 47311 --- [demo] [           main] .s.b.a.l.ConditionEvaluationReportLogger :
 
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2026-09-11T14:45:08.378+07:00 ERROR 47311 --- [demo] [           main] o.s.boot.SpringApplication               : Application run failed
 
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'apiClient' defined in file [/…/demo/build/classes/java/main/com/example/demo/ApiClient.class]: Unexpected exception during bean creation
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:538) ~[spring-beans-7.0.9.jar:7.0.9]
	...
Caused by: org.springframework.util.PlaceholderResolutionException: Could not resolve placeholder 'app.api-key' in value "${app.api-key}"
	at org.springframework.util.PlaceholderResolutionException.withValue(PlaceholderResolutionException.java:81) ~[spring-core-7.0.9.jar:7.0.9]
	at org.springframework.util.PlaceholderParser$ParsedValue.resolve(PlaceholderParser.java:296) ~[spring-core-7.0.9.jar:7.0.9]
	at org.springframework.util.PlaceholderParser.replacePlaceholders(PlaceholderParser.java:129) ~[spring-core-7.0.9.jar:7.0.9]
	at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:96) ~[spring-core-7.0.9.jar:7.0.9]
	at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:286) ~[spring-core-7.0.9.jar:7.0.9]
	at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:257) ~[spring-core-7.0.9.jar:7.0.9]
	at org.springframework.context.support.PropertySourcesPlaceholderConfigurer.lambda$processProperties$0(PropertySourcesPlaceholderConfigurer.java:184) ~[spring-context-7.0.9.jar:7.0.9]
	at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:959) ~[spring-beans-7.0.9.jar:7.0.9]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1679) ~[spring-beans-7.0.9.jar:7.0.9]
	...

Không có failure analyzer nào xử lý exception này, nên sẽ không có khung "APPLICATION FAILED TO START" kèm description và action — hãy đọc dòng Caused by: cuối cùng. Các frame bên dưới chính là bước tra key trong sơ đồ: doResolveDependency gọi resolveEmbeddedValue, đi qua PropertySourcesPlaceholderConfigurer vào resolveRequiredPlaceholders, không tìm thấy giá trị và cũng không có default để dùng. Tomcat bị dừng và process thoát.

Cũng exception đó xuất hiện với @Value("${app.servers}") List<String> khi app.servers là một list YAML — Could not resolve placeholder 'app.servers' in value "${app.servers}" — vì chỉ có app.servers[0]app.servers[1] tồn tại. @Value("${app.servers[0]}") thì chạy được.

Default rỗng ${key:}

Dấu hai chấm không có gì phía sau biến giá trị thành tùy chọn, với kết quả tùy vào type đích:

Type đích@Value("${missing.key:}") inject
Stringchuỗi rỗng
List<String>một ArrayList rỗng
String[]một array rỗng
Integer, Boolean, Durationnull
intkhông inject được — startup thất bại với Failed to convert value of type 'java.lang.String' to required type 'int'; For input string: ""

Default rỗng trên primitive type là một lỗi startup đội lốt giá trị tùy chọn. Hãy dùng wrapper type, hoặc một default thật sự.

Type conversion: int, boolean, Duration, List và array

Mọi chuỗi mà @Value tạo ra đều được convert bởi conversion service của bean factory, và trong application Boot đó là org.springframework.boot.convert.ApplicationConversionService. Từng dòng dưới đây đều được inject vào một constructor parameter hoặc một field:

Giá trị trong fileType đíchĐược inject
8091int8091
truebooleantrue
onbooleantrue; inject vào String thì vẫn là on
30sDurationPT30S
30DurationPT0.03S — số không kèm đơn vị nghĩa là millisecond
alice,bob,carolList<String>[alice, bob, carol], một ArrayList có 3 phần tử
alice, bob , carolList<String>[alice, bob, carol] — từng phần tử được trim
alice,bob,carolString[][alice, bob, carol], length 3
100,200,400int[][100, 200, 400]
-5int-5
eightyintstartup thất bại, xem bên dưới

Giá trị không convert được sẽ làm startup thất bại ngay tại bean yêu cầu nó. Đây là app.port=eighty với AppSettings:

Text
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'appSettings' defined in file [/…/demo/build/classes/java/main/com/example/demo/AppSettings.class]: Unsatisfied dependency expressed through constructor parameter 1: Failed to convert value of type 'java.lang.String' to required type 'int'; For input string: "eighty"
	...
Caused by: org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'int'; For input string: "eighty"
	...
Caused by: java.lang.NumberFormatException: For input string: "eighty"

constructor parameter 1 đếm từ 0: đó là port, parameter thứ hai.

${...}#{...}: property placeholder và SpEL

${...}property placeholder. Phần bên trong là một key, kèm default nếu có, được tra trong Environment, và placeholder bị thay bằng giá trị tìm được. Đó là thay text, không hơn. #{...}SpEL expression — Spring Expression Language. Phần bên trong được parse như code rồi tính: toán tử, gọi method, type reference bằng T(...), các variable systemPropertiesenvironment.

Với app.port=8091, app.host=localhostapp.names=alice,bob,carol:

@Value(...)Được inject
"${app.port} + 1"8091 + 1, một String
"#{${app.port} + 1}"8092
"#{'${app.host}'.toUpperCase()}"LOCALHOST
"#{systemProperties['java.version']}"21.0.6
"#{environment['app.host']}"localhost
"#{environment['app.missing'] ?: 'elvis-default'}"elvis-default
"#{T(java.lang.Math).max(${app.min-workers:2}, 4)}"4
"#{'${app.names}'.split(',')}" vào List<String>[alice, bob, carol]
"#{'${app.names}'.split(',').length}" vào int3

Dòng đầu tiên là dòng cần nhớ: ${app.port} + 1 không làm phép cộng nào; nó tạo ra chuỗi 8091 + 1. Phép tính, gọi method và giá trị tính từ giá trị khác đều cần #{...}, thường có một ${...} bên trong.

Cái nào chạy trước?

Luôn là placeholder. Trong DefaultListableBeanFactory.doResolveDependency, lời gọi resolveEmbeddedValue đứng trước evaluateBeanDefinitionString, và ba lần chạy cho thấy hệ quả mà không cần đọc bytecode.

Placeholder viết ra expression. Với app.expr=6*7, @Value("#{${app.expr}}") int inject 42. Placeholder đã đặt 6*7 vào expression trước khi SpEL parse nó; riêng @Value("${app.expr}") String thì chỉ là 6*7.

SpEL chỉ thấy text đã được thay. Bỏ dấu nháy quanh placeholder:

Java
public SpelBroken(@Value("#{${app.names}.split(',')}") List<String> names) {
Text
Caused by: org.springframework.expression.spel.SpelParseException: Expression [alice,bob,carol.split(',')] @5: EL1041E: After parsing a valid expression, there is still more data in the expression: 'comma(,)'

Expression trong lỗi là alice,bob,carol.split(',')${app.names} đã được thay từ trước. SpEL parse alice thành một expression hoàn chỉnh rồi gặp dấu phẩy. Chính dấu nháy đơn trong #{'${app.names}'.split(',')} biến text đã thay thành một string literal của SpEL.

Giá trị trong file cũng bị tính. Với app.computed=#{2*21}, @Value("${app.computed}") int inject 42: placeholder resolve ra #{2*21}, rồi bước expression tính nó. environment.getProperty("app.computed") trả về nguyên văn text #{2*21}. Vậy mọi thứ đi tới @Value qua placeholder đều có thể chạy như một expression, và đó là một lý do để để ý giá trị cấu hình đến từ đâu.

Từ tất cả những điều trên suy ra một khác biệt về hành vi. app.names-spaced=alice, bob , carol inject bằng @Value("${app.names-spaced}") List<String> cho [alice, bob, carol], vì conversion service trim từng phần tử. Qua #{'${app.names-spaced}'.split(',')} thì cho [alice, bob , carol], và phần tử thứ hai là bob còn nguyên hai dấu cách — String.split không trim gì cả.

Resolve placeholder dựa trên Environment so với tính SpEL, và hai lượt xử lý áp lên một expression kết hợp

Đọc property qua Environment

@Value cố định key từ lúc compile. Khi key chỉ được biết lúc chạy, hoặc code cần rẽ nhánh theo việc key có tồn tại hay không, hãy inject Environment và hỏi trực tiếp. Trong application Boot, object được inject là một org.springframework.boot.ApplicationEnvironment.

EnvironmentDemo.java
@Component
public class EnvironmentDemo implements ApplicationRunner {
 
    private final Environment environment;
 
    public EnvironmentDemo(Environment environment) {
        this.environment = environment;
    }
 
    @Override
    public void run(ApplicationArguments args) {
        System.out.println(environment.getProperty("app.host"));
        System.out.println(environment.getProperty("app.api-key"));
        System.out.println(environment.getProperty("app.api-key", "not-set"));
        System.out.println(environment.getProperty("app.port", Integer.class));
        System.out.println(environment.getProperty("app.max-connections", Integer.class, 0));
        System.out.println(environment.getProperty("app.timeout", Duration.class, Duration.ofSeconds(30)));
        System.out.println(environment.getProperty("app.url"));
        System.out.println(environment.containsProperty("app.host"));
        System.out.println(environment.getRequiredProperty("app.api-key"));
    }
}
Text
localhost
null
not-set
8091
0
PT30S
http://localhost:8091
true
2026-09-11T15:06:33.604+07:00  INFO 68821 --- [demo] [           main] .s.b.a.l.ConditionEvaluationReportLogger :
 
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2026-09-11T15:06:33.607+07:00 ERROR 68821 --- [demo] [           main] o.s.boot.SpringApplication               : Application run failed
 
java.lang.IllegalStateException: Required key 'app.api-key' not found
Lời gọiTrả về
getProperty("app.host")giá trị đã resolve, dạng String
getProperty("app.api-key")null khi thiếu key — không bao giờ throw chỉ vì key không tồn tại
getProperty("app.api-key", "not-set")default khi thiếu key
getProperty("app.port", Integer.class)giá trị đã convert sang type yêu cầu
getProperty("app.max-connections", Integer.class, 0)default có type khi thiếu key
getProperty("app.timeout", Duration.class, Duration.ofSeconds(30))một Duration; các conversion giống @Value đều dùng được
getProperty("app.url")http://localhost:8091 — placeholder được resolve
containsProperty("app.host")true khi có property source nào đó chứa key
getRequiredProperty("app.api-key")throw IllegalStateException: Required key 'app.api-key' not found

Ba trường hợp biên đáng biết:

  • Key tồn tại nhưng không convert được thì throw. getProperty("app.bad-port", Integer.class) với app.bad-port=eighty cho ra ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.lang.Integer] for value [eighty].
  • Environment resolve ${...} nhưng không bao giờ tính #{...}: getProperty("app.computed") trả về #{2*21}.
  • resolvePlaceholders("${app.host}:${app.port} ${app.api-key}") trả về localhost:8091 ${app.api-key}, để nguyên placeholder không biết; còn resolveRequiredPlaceholders thì throw PlaceholderResolutionException cho trường hợp đó.

Giới hạn của @Value

@Value là công cụ đúng cho một hai giá trị. AppSettings ở trên cho thấy chỗ nó bắt đầu không còn phù hợp:

  • Không có validation. app.port=-5 inject -5 mà không phàn nàn gì, và app.url vui vẻ trở thành http://localhost:-5. Mọi kiểm tra khoảng giá trị đều là code bạn tự viết.
  • Không gom nhóm được. Bảy setting liên quan là bảy annotation và một constructor bảy parameter, và mọi class khác cần chúng đều phải lặp lại cả bảy.
  • Key là chuỗi rải rác khắp các class. Đổi tên app.timeout trong file thì compiler không có gì để nói. Gõ sai còn tệ hơn khi có default: ${app.timout:30s} khởi động êm và dùng 30s mãi mãi.
  • Không có metadata cho IDE. Không có gì mô tả các key app.*, nên IDE không thể gợi ý chúng trong application.properties hay đánh dấu key nào không có code nào đọc.
  • Không có giá trị có cấu trúc. Một list YAML dưới app.servers không inject nguyên khối được, như đã thấy ở trên.

@ConfigurationProperties giải quyết cả năm điểm này, và đó là chủ đề của bài tiếp theo.

Nên dùng .properties hay .yml?

application.propertiesapplication.yml
Cấu trúcmỗi dòng một key đầy đủ nối bằng dấu chấmlồng nhau bằng thụt lề, chỉ dùng dấu cách
Dấu phân cách=, : hoặc khoảng trắngdấu hai chấm kèm một dấu cách
Comment# hoặc ! ở đầu dòng; # giữa dòng là một phần của giá trị# sau khoảng trắng, ở bất kỳ đâu trên dòng
Lista,b,c trong một key, hoặc key[0]=key[1]=khối - item hoặc [a, b]
Chuỗi nhiều dòngescape \n và nối dòng bằng \|, >|-
Typemọi giá trị là String cho tới khi được converttype theo YAML 1.1: ontrue, 012383, 12:30750 nếu không quote
Chữ không phải ASCIIdecode bằng ISO-8859-1, nên Xin chàoXin chà o nếu không escapeđọc bằng UTF-8
Key trùngcái cuối cùng thắng, không báo gìstartup thất bại với found duplicate key
Khi gõ saimột dòng sai thường vẫn là dòng hợp lệmột tab hay một : lạc chỗ làm dừng startup
Tìm kiếmgrep app.owner.email tìm thấy keykey bị tách ra nhiều dòng, nên cùng lệnh grep không tìm thấy gì
Cùng key ở cả hai filethắngthua

Chọn .properties cho cấu hình nhỏ và phẳng, cho file được script chỉnh sửa, và khi muốn mọi key đều grep được. Chọn .yml cho cấu trúc phân cấp sâu và list, và cho mọi cấu hình có chữ tiếng Việt. Dù chọn cái nào, mỗi project hãy dùng một loại: hai file vẫn chạy chung được, nhưng một key khai báo ở cả hai nơi với một bên thắng âm thầm thì rất khó lần ra.

FAQ

Spring Boot có đọc application.properties bằng UTF-8 không?

Không. Trong Spring Boot 4.1.1, file cấu hình .properties được decode bằng ISO-8859-1 nếu không chỉ định encoding, nên chữ tiếng Việt UTF-8 như Xin chào bị đọc thành Xin chà o. Hãy dùng escape dạng \u00e0, chuyển các giá trị sang application.yml (được đọc bằng UTF-8), hoặc import một file riêng kèm [encoding=utf-8].

Dùng application.properties và application.yml cùng lúc được không?

Được. Cả hai đều được load và gộp theo từng key. Khi cùng một key có ở cả hai file trong cùng một vị trí, giá trị từ application.properties được dùng.

Vì sao field @Value của tôi bị null?

Hai nguyên nhân chiếm gần hết các trường hợp. Field là static: Spring bỏ qua nó và log đúng một dòng INFO.

Text
2026-09-11T14:47:25.774+07:00  INFO 48742 --- [demo] [           main] f.a.AutowiredAnnotationBeanPostProcessor : Autowired annotation is not supported on static fields: private static java.lang.String com.example.demo.StaticProbe.name

Hoặc object được tạo bằng new thay vì lấy từ container: new FieldStyle().name() trả về null, vì không có gì xử lý annotation của nó. Thiếu key thì không phải nguyên nhân — trường hợp đó làm startup thất bại luôn.

Làm sao inject một list từ application.yml bằng @Value?

List trong YAML được lưu thành các key có chỉ số, nên @Value("${app.servers}") thất bại với Could not resolve placeholder 'app.servers', còn @Value("${app.servers[0]}") thì chạy được. Muốn inject cả list bằng @Value, hãy viết nó thành một chuỗi phân cách bằng dấu phẩy — names: alice,bob,carol — và inject vào List<String>, kết quả là [alice, bob, carol]. Bind một list YAML thật sự vào List là việc của @ConfigurationProperties.

Vì sao 0123 biến thành 83 trong application.yml?

SnakeYAML 2.6 theo YAML 1.1, trong đó số không quote có số 0 ở đầu là số bát phân, nên 0123 là 83. Boot lưu Integer đó, và đọc ra dưới dạng chuỗi thì được 83. Hãy viết "0123". Cùng những quy tắc đó biến on thành true, 1.10 thành 1.112:30 thành 750.

${}#{} trong @Value khác nhau thế nào?

${key} tra một key trong Environment và thay giá trị của nó vào dưới dạng text. #{expression} tính một SpEL expression. Hai cú pháp kết hợp được, như #{'${app.names}'.split(',')}, và placeholder luôn được resolve trước khi expression được tính.

Làm sao viết ${ nguyên văn trong giá trị property?

Escape dấu đô la bằng một dấu gạch chéo ngược. Trong .properties, parser của file bỏ đi một dấu gạch chéo ngược trước, nên hãy viết \\${literal}; trong YAML, \${literal} không nháy hoặc nháy đơn là đủ. Cả hai đều đọc ra ${literal}.

Kết luận

Cả hai file đều đi về cùng một chỗ: một map phẳng gồm các key String nối bằng dấu chấm trong Environment, với application.properties thắng khi một key có mặt ở cả hai. Chúng khác nhau ở chỗ làm gì với text của bạn trên đường vào. .properties decode bằng ISO-8859-1, giữ dấu cách ở cuối và coi dấu gạch chéo ngược là escape; YAML đọc UTF-8 nhưng áp type theo YAML 1.1, nên NO, 0123, 1.1012:30 cần dấu nháy. Placeholder được resolve mỗi lần đọc, kể cả ${random.*}. @Value đọc key, resolve placeholder, tính SpEL, convert kết quả, và làm startup thất bại khi key không có giá trị lẫn default.

Như vậy là đủ để đọc vài giá trị. Nhưng chưa đủ cho một nhóm setting liên quan cần được validate, và đó là chỗ bài tiếp theo bắt đầu: @ConfigurationProperties — cấu hình type-safe kết hợp validation.

Bài viết liên quan

[Spring Boot Basics] Server-side rendering với Thymeleaf trong Spring Boot: template, form và validation

Server-side rendering với Thymeleaf 3.1.5 trong Spring Boot 4.1.1, kiểm chứng trên application đang chạy: khi nào SSR hợp hơn JSON API, view name trở thành classpath:/templates/products/list.html ra sao, năm loại standard expression, th:text và th:utext với XSS, th:each cùng #numbers và #temporals, form tạo mới có validation với th:field và th:errors, vị trí bắt buộc của BindingResult, Post/Redirect/Get với flash attribute, fragment, CSS tĩnh, spring.thymeleaf.cache thực sự thay đổi gì, và trang lỗi HTML.

[Spring Boot Basics] Gọi API bên ngoài với RestClient trong Spring Boot: GET, POST, xử lý lỗi và timeout

Gọi HTTP API bên ngoài từ Spring Boot 4.1.1 bằng RestClient, kiểm chứng với một stub chạy local: RestClient so với RestTemplate, WebClient và @HttpExchange, spring-boot-starter-restclient và RestClient.Builder được auto-configure, GET vào record và list, toEntity, encode query parameter, POST, PUT và DELETE, message thật của HttpClientErrorException, onStatus, defaultStatusHandler và exchange, connect timeout và read timeout mặc định lẫn khi cấu hình bằng spring.http.clients được đo thực tế, interceptor để log, và chuyển lỗi upstream thành 502, 503 và 504.

[Spring Boot Basics] @ConfigurationProperties trong Spring Boot: cấu hình type-safe kết hợp validation

Cấu hình type-safe trong Spring Boot 4.1.1 với @ConfigurationProperties, kiểm chứng bằng các lần chạy thật: bind vào record không cần @ConstructorBinding, JavaBean binding và @DefaultValue, ba cách đăng ký properties class, object lồng nhau, list, map, enum, chuyển đổi Duration và DataSize, relaxed binding và cách đặt tên environment variable, lỗi khởi động fail-fast với @Validated, metadata từ configuration processor, và bảng so sánh với @Value.

[Spring Boot Basics] Bean và ApplicationContext trong Spring: @Component, stereotype và component scanning

Bean trong Spring thực chất là gì và object nào không bao giờ nên là bean, chứng minh @Component là meta-annotation nằm dưới @Service, @Repository và @Controller, mỗi stereotype thật sự thêm gì lúc runtime, component scanning biến file .class thành BeanDefinition ra sao, quy tắc đặt tên bean kể cả trường hợp hai chữ hoa, API của ApplicationContext cùng số bean thật của một app Spring Boot 4.1.1, và ba kiểu đăng ký bean thất bại kèm thông báo lỗi thật.