Hình dung @Value như những mẩu giấy nhãn rời ghim khắp văn phòng, mỗi cái giữ đúng một chuỗi thô: "30s", "8080", "10MB". Tiện lúc dán, nhưng mỗi lần cần dùng bạn phải gỡ xuống, đọc, và tự đoán nó nghĩa là gì — và nếu một nhãn ghi sai, bạn chỉ phát hiện lúc có người tình cờ gỡ nó ra vào ba giờ sáng. @ConfigurationProperties là thay cả đống nhãn rời đó bằng một tờ khai có ô định sẵn kiểu: ô "thời gian" chỉ nhận thời lượng, ô "kích thước" chỉ nhận dung lượng, và một nhân viên đứng quầy từ chối nguyên tờ khai ngay lúc nộp nếu có ô nào sai. @Value đủ cho vài thuộc tính; với cấu hình thật, có cách tốt hơn nhiều.
Gom thành một record
@ConfigurationProperties(prefix = "app")
record AppProps(
@NotBlank String ten,
@Min(1) @Max(65535) int cong,
Duration hanCho,
DataSize kichThuoc,
List<String> danhSach,
Map<String, Integer> bang) {}
app:
ten: "dịch vụ đơn hàng"
cong: 8080
han-cho: 30s
kich-thuoc: 10MB
danh-sach: [a, b, c]
bang: {x: 1, y: 2}
ten=dịch vụ đơn hàng cong=8080
hanCho=PT30S (Duration, parse từ '30s')
kichThuoc=10485760B (DataSize, parse từ '10MB')
danhSach=[a, b, c] bang={x=1, y=2}
Bốn thứ đáng chú ý.
record dùng được từ Boot 2.6 — bất biến, gọn, không cần getter/setter. Đây là cách tôi khuyên cho mã mới.
Chuyển kiểu tự động. 30s thành Duration, 10MB thành DataSize với 10.485.760 byte. Không phải tự parse, không phải nhớ đơn vị — ô nào tự điền kiểu của ô đó.
Duration nhận 30s, 5m, 2h, PT30S. DataSize nhận 10MB, 512KB, 1GB.
List và Map ánh xạ thẳng từ YAML.
Tên gạch nối thành camelCase. han-cho thành hanCho nhờ relaxed binding.
Bật nó lên
@SpringBootApplication
@EnableConfigurationProperties(AppProps.class)
public class App { }
Hoặc gắn @ConfigurationPropertiesScan để quét tự động, hoặc thêm @Component vào lớp props.
Với record, @EnableConfigurationProperties là cách rõ ràng nhất.
Ba lý do tốt hơn @Value
Kiểu mạnh và chuyển kiểu. @Value("${app.han-cho}") String hanCho cho bạn một chuỗi "30s" mà bạn phải tự parse. @ConfigurationProperties cho Duration.
Validation lúc khởi động:
@ConfigurationProperties(prefix = "app")
@Validated
record AppProps(@NotBlank String ten, @Min(1) @Max(65535) int cong) {}
Cấu hình sai làm ứng dụng không khởi động, kèm thông báo rõ ràng chỉ đúng thuộc tính nào sai — chính là nhân viên từ chối tờ khai ngay tại quầy. So với @Value, nơi giá trị sai chỉ lộ ra khi có ai đó dùng tới, thường là vào lúc tệ nhất.
Cần spring-boot-starter-validation trong pom.xml.
Nhóm lại được và test được. Một đối tượng truyền vào hàm khởi tạo, thay vì mười @Value rải rác. Test thì new AppProps("x", 8080, ...) là xong — không cần Spring.
Tiêm vào bean
@Service
class DichVu {
private final AppProps props;
DichVu(AppProps props) { this.props = props; }
}
Như mọi bean khác, đúng nguyên tắc tiêm qua hàm khởi tạo ở bài 4.
Lồng nhau
@ConfigurationProperties(prefix = "app")
record AppProps(String ten, Csdl csdl, Mail mail) {
record Csdl(String url, int pool) {}
record Mail(String host, int cong) {}
}
app:
ten: x
csdl:
url: jdbc:...
pool: 20
Cấu trúc phản chiếu đúng cấu trúc YAML, và người đọc nhìn một lớp là biết ứng dụng cần gì.
Giá trị mặc định
Với record, dùng hàm khởi tạo rút gọn:
record AppProps(String ten, Integer cong) {
AppProps {
if (cong == null) cong = 8080;
}
}
Hoặc đơn giản hơn: để mặc định trong application.yml. Cách thứ hai rõ hơn vì mọi giá trị nằm cùng chỗ.
Nhớ nguyên tắc ở bài 6: mặc định cho thứ vô hại, bắt buộc cho bí mật.
Gợi ý trong IDE
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
Nó sinh metadata để IDE tự động hoàn thành tên thuộc tính trong application.yml và cảnh báo khi bạn gõ sai. Rẻ và rất đáng thêm.
Cấu hình đổi lúc chạy
@ConfigurationProperties đọc một lần lúc khởi động. Đổi tệp không có tác dụng.
Cần đổi lúc chạy thì có @RefreshScope của Spring Cloud, nhưng nó kéo theo cả một hệ sinh thái. Với đa số dịch vụ, khởi động lại là cách đơn giản và đúng đắn hơn — nhất là khi bạn đã triển khai cuốn chiếu.
Ngoại lệ đáng làm: mức log. Actuator cho đổi lúc chạy qua /actuator/loggers, và bài 10 sẽ nói.
Nếu muốn biết dự án mình còn bao nhiêu nhãn rời, đếm một con số:
grep -rn '@Value' --include='*.java' src/main | wc -l
Lớn hơn năm thì gom chúng vào một hoặc vài record @ConfigurationProperties. Đổi lại bạn được kiểu mạnh, validation lúc khởi động, và một chỗ duy nhất để nhìn thấy dịch vụ cần gì để chạy.
Mẫu số chung
Biến cấu hình từ "một túi chuỗi rải khắp mã" thành "một đối tượng có kiểu, kiểm ở cửa" là nước đi mà mọi framework trưởng thành đều hội tụ về, chỉ khác tên.
- .NET là bản song sinh gần như một-một: mẫu Options với
IOptions<T>gom cấu hình vào một lớp có kiểu, cộng DataAnnotations ([Range],[Required]) để kiểm — thậm chí cóValidateOnStart()để app từ chối khởi động khi cấu hình sai, đúng@Validatedcủa Spring. - Node đi theo
zodhayenvalid: phân tích biến môi trường vào một đối tượng có lược đồ, sai thì ném ngay lúc nạp. Rust dùngserdecộngconfig/figmentđể dựng thẳng vào mộtstruct. - Và cái tiện nhỏ "chuỗi thành kiểu giàu" (
30s→Duration,10MB→DataSize) thì hệ nào cũng phải tự dựng lại — Go cótime.ParseDuration, Rust cóhumantime— vì không ai muốn tự nhớ30 * 1000hay10 * 1024 * 1024ở rải rác trong mã.
Sợi chỉ chung đáng mang theo: cấu hình xứng đáng là một đối tượng miền có kiểu, không phải một túi chuỗi vương vãi. Khoảnh khắc nó trở thành một đối tượng có kiểu, ba trợ thủ cùng lúc vào cuộc giúp bạn: trình biên dịch (sai kiểu là đỏ ngay), IDE (tự hoàn thành tên, gạch đỏ lỗi gõ), và bộ kiểm lúc khởi động (app không chạy nổi nếu giá trị sai). Cả ba đều dịch lỗi từ lúc chạy — nơi một @Value sai nằm im tới 2 giờ sáng — về lúc biên dịch và lúc khởi động, nơi nó rẻ và ồn. Ở Spring, .NET, Node hay Rust, quy tắc giống nhau: nâng cấu hình từ chuỗi lên kiểu, rồi để máy móc canh giúp.
Ngày mai: auto-configuration — cách Boot tạo ra 263 bean mà bạn không khai.