@ConditionalOn* family + back-off pattern + autoconfig ordering
Bóc tách 12 annotation @ConditionalOn* trong auto-configuration: cơ chế điều kiện, back-off pattern qua @ConditionalOnMissingBean, thứ tự evaluate qua @AutoConfigureBefore/After, và debug bằng Actuator /conditions.
TL;DR: @ConditionalOn* là cơ chế tất cả-hoặc-không-có-gì của autoconfig — mỗi annotation đặt một câu hỏi về môi trường runtime (classpath? bean đã có chưa? property có không?), chỉ register bean khi mọi điều kiện pass. Trong đó @ConditionalOnMissingBean là chìa khoá "back-off": autoconfig đăng ký bean mặc định nhưng kiểm tra trước xem user đã khai rồi chưa — nếu rồi thì nhường. @AutoConfigureBefore/After xác định thứ tự evaluate giữa các autoconfig: cần thiết vì @ConditionalOnMissingBean phụ thuộc thứ tự — autoconfig đến trước sẽ win. Debug bằng /actuator/conditions thay vì đoán.
Bài 03 — Auto-configuration deep dive bóc toàn bộ pipeline AutoConfigurationImportSelector → file AutoConfiguration.imports → 143 candidate → filter. Bài này đào sâu một bước của filter đó: các annotation điều kiện và lý do thứ tự evaluate quan trọng. Bài xây trên nền @Conditional ở bài @Conditional bean.
1. Bài toán: autoconfig register bean không điều kiện là thảm hoạ
Giả sử Spring Boot không có điều kiện nào — mọi autoconfig luôn register bean:
// Gia su khong co @Conditional
@AutoConfiguration
public class CacheAutoConfiguration {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(); // luon tao, khong hoi gi
}
}
Và team bạn muốn dùng Redis cho cache:
@Configuration
public class AppConfig {
@Bean
public CacheManager cacheManager() {
return new RedisCacheManager(...); // custom
}
}
Kết quả: Spring context có 2 bean CacheManager — conflict → NoUniqueBeanDefinitionException ngay lúc startup.
Đây là lý do mọi @Bean trong autoconfig đều cần điều kiện: autoconfig phải biết khi nào nên register, khi nào nên nhường. Hai nhóm điều kiện giải quyết 2 câu hỏi khác nhau.
2. Family 12 annotation @ConditionalOn*
Spring Boot cung cấp 12 annotation điều kiện sẵn, mỗi cái hỏi một thứ về môi trường:
| Annotation | Câu hỏi |
|---|---|
@ConditionalOnClass | Class này có trong classpath không? |
@ConditionalOnMissingClass | Class này không có trong classpath? |
@ConditionalOnBean | Bean kiểu/tên này đã register trong context chưa? |
@ConditionalOnMissingBean | Bean kiểu/tên này chưa register? |
@ConditionalOnProperty | Property có giá trị mong đợi không? |
@ConditionalOnExpression | SpEL expression có true không? |
@ConditionalOnWebApplication | App đang chạy trong web context? |
@ConditionalOnNotWebApplication | App không phải web context? |
@ConditionalOnResource | Resource (classpath:/…) tồn tại không? |
@ConditionalOnJava | Java version match không? |
@ConditionalOnCloudPlatform | Cloud platform detect (K8s, CF, …)? |
@ConditionalOnJndi | JNDI resource available? |
Nhiều annotation cùng class = AND logic: tất cả phải pass, thiếu một là skip.
@AutoConfiguration
@ConditionalOnClass(KafkaTemplate.class) // (1) co Kafka classpath
@ConditionalOnProperty("spring.kafka.bootstrap-servers") // (2) co property
@ConditionalOnMissingBean(KafkaTemplate.class) // (3) user chua khai
public class KafkaAutoConfiguration { ... }
Ba điều kiện song song — fail bất kỳ cái nào → autoconfig toàn bộ skip.
2.1 @ConditionalOnClass — đo classpath
@AutoConfiguration
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
public class RedisAutoConfiguration { ... }
Lý do dùng name (string) thay value (class literal): autoconfig phải compile được kể cả khi RedisTemplate không có classpath. Nếu dùng value = RedisTemplate.class và class không có → compile fail. String name tránh vấn đề này — runtime evaluate qua ClassLoader.getResource().
Bài 03 đã giải thích Boot dùng ASM bytecode reader thay Class.forName() để check condition này mà không load class.
2.2 @ConditionalOnProperty — feature flag
@AutoConfiguration
@ConditionalOnProperty(
prefix = "spring.jpa",
name = "open-in-view",
havingValue = "true",
matchIfMissing = true // bat default neu khong co property
)
public class OpenEntityManagerInViewAutoConfiguration { ... }
matchIfMissing = true nghĩa là "nếu property vắng mặt thì coi như điều kiện pass". Dùng để bật tính năng theo mặc định, cho user tắt qua spring.jpa.open-in-view=false.
3. Back-off pattern — @ConditionalOnMissingBean
Đây là cơ chế cốt lõi của "opinionated but not lock-in". Một autoconfig điển hình:
@AutoConfiguration
public class CacheAutoConfiguration {
@Bean
@ConditionalOnMissingBean // chi tao neu user CHUA co CacheManager bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(); // default
}
}
Luồng evaluation:
flowchart TB
Q{"CacheManager bean<br/>da co trong context?"}
Skip["Boot skip<br/>dung bean cua user"]
Register["Boot tao ConcurrentMapCacheManager<br/>lam default"]
Q -->|"Co roi"| Skip
Q -->|"Chua co"| RegisterVì sao đây là "chìa khoá override": user chỉ cần khai bean với đúng kiểu, Boot tự nhận ra và nhường. Không cần annotation đặc biệt, không cần file config nào — chỉ cần @Bean:
@Configuration
public class AppConfig {
@Bean
public CacheManager cacheManager() {
// Boot thay CacheManager da ton tai -> skip CacheAutoConfiguration.cacheManager()
return new RedisCacheManager(redisConnectionFactory());
}
}
Tính chất này cho phép "đúng là default ra của hộp, sai thì override một chỗ" — không phải sửa source của Boot hay exclude autoconfig toàn bộ.
3.1 Ba attribute của @ConditionalOnMissingBean
@ConditionalOnMissingBean // check theo kieu cua @Bean return type
@ConditionalOnMissingBean(CacheManager.class) // check theo class cu the
@ConditionalOnMissingBean(name = "cacheManager") // check theo bean name
@ConditionalOnMissingBean(
type = "org.springframework.cache.CacheManager" // string -- tranh ClassNotFound
)
Dùng type (string) khi interface/class có thể không có classpath — tương tự lý do dùng name trong @ConditionalOnClass.
3.2 Pitfall back-off: annotation thiếu → bean conflict
// SAI -- boot autoconfig khong co @ConditionalOnMissingBean:
@AutoConfiguration
public class TracingAutoConfiguration {
@Bean
public Tracer tracer() { return new DefaultTracer(); }
}
// User cung khai Tracer:
@Configuration
public class AppConfig {
@Bean
public Tracer tracer() { return new JaegerTracer(...); }
}
Spring context có 2 bean Tracer → NoUniqueBeanDefinitionException lúc startup. Fix:
@Bean
@ConditionalOnMissingBean // bat buoc
public Tracer tracer() { return new DefaultTracer(); }
Quy tắc vàng: mọi @Bean trong autoconfig public nên có @ConditionalOnMissingBean — không có ngoại lệ trừ khi bạn chủ ý forbid override.
4. Ordering — @AutoConfigureBefore/After
4.1 Vì sao thứ tự quan trọng
@ConditionalOnMissingBean phụ thuộc vào bean nào đã có trong context tại thời điểm evaluate. Nếu hai autoconfig không có thứ tự rõ ràng, kết quả không xác định:
// A1 va A2 khong co ordering
@AutoConfiguration
public class A1 {
@Bean
public Foo foo() { return new Foo("from-A1"); }
}
@AutoConfiguration
public class A2 {
@Bean
@ConditionalOnMissingBean // neu A2 chay truoc A1 -> Foo chua co -> register
public Foo foo() { return new Foo("from-A2"); }
}
Nếu A2 evaluate trước A1: A2 register Foo("from-A2"). Sau đó A1 evaluate — không có @ConditionalOnMissingBean → cố tạo thêm Foo → BeanDefinitionOverrideException.
Nếu A1 evaluate trước A2: A1 register Foo("from-A1"). A2 thấy Foo đã có → skip. Ổn.
Kết quả phụ thuộc vào thứ tự không xác định → bug không tái hiện được.
4.2 @AutoConfigureBefore/After — đặt ordering rõ ràng
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class HibernateJpaAutoConfiguration { ... }
@AutoConfiguration(after = HibernateJpaAutoConfiguration.class)
public class JpaRepositoriesAutoConfiguration { ... }
Chuỗi phụ thuộc:
flowchart LR
DS["DataSourceAutoConfiguration<br/>tao DataSource"]
HJpa["HibernateJpaAutoConfiguration<br/>after = DS<br/>tao EntityManagerFactory"]
JpaRepo["JpaRepositoriesAutoConfiguration<br/>after = HJpa<br/>tao Repository proxy"]
DS --> HJpa --> JpaRepoMỗi bước xây trên kết quả của bước trước. Nếu JpaRepositoriesAutoConfiguration chạy trước HibernateJpaAutoConfiguration, @ConditionalOnBean(EntityManagerFactory.class) của nó sẽ false → skip → repository không tạo → @Autowired fail lúc runtime.
4.3 Cú pháp — 3 cách
// Cach 1: annotation attribute (Boot 2.7+, uu tien)
@AutoConfiguration(after = HibernateJpaAutoConfiguration.class)
@AutoConfiguration(before = WebMvcAutoConfiguration.class)
// Cach 2: annotation rieng (tuong duong, cu hon)
@AutoConfiguration
@AutoConfigureAfter(HibernateJpaAutoConfiguration.class)
@AutoConfiguration
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// Cach 3: fine-grained so nguyen
@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfigureOrder dùng khi cần ép một autoconfig chạy rất sớm hoặc rất muộn so với toàn bộ tập. @AutoConfigureBefore/After ưu tiên hơn vì express ý định rõ hơn.
4.4 Cơ chế bên dưới — tại sao ordering không phải ép chạy sequential
@AutoConfigureBefore/After không ép Spring chạy autoconfig theo đúng thứ tự đó. Thực ra:
AutoConfigurationSorterthu thập tất cả@AutoConfigureBefore/Afterrelationship từ metadata.- Build DAG (đồ thị có hướng không chu trình) từ các relationship.
- Topological sort → danh sách thứ tự an toàn.
DeferredImportSelectorimport autoconfig theo thứ tự đó.
Hệ quả: nếu bạn khai @AutoConfigureAfter(NonExistentAutoConfig.class) → dependency đó ignored (không crash). Boot chỉ tôn trọng dependency tồn tại. Circular dependency (A after B, B after A) → Boot throws IllegalStateException tại startup.
5. Debug bằng Actuator /conditions
Khi autoconfig không ra kết quả như mong đợi — đừng đoán. Bật endpoint:
# application.yml
management:
endpoints:
web:
exposure:
include: health,conditions
GET /actuator/conditions trả về:
{
"contexts": {
"application": {
"positiveMatches": {
"CacheAutoConfiguration#cacheManager": [
{
"condition": "OnMissingBeanCondition",
"message": "No qualifying bean of type 'CacheManager' found"
}
]
},
"negativeMatches": {
"RedisAutoConfiguration": {
"notMatched": [
{
"condition": "OnClassCondition",
"message": "did not find required class 'org.springframework.data.redis.core.RedisTemplate'"
}
]
}
}
}
}
}
Đọc output:
positiveMatches: autoconfig đã register + lý do condition pass.negativeMatches: autoconfig skip + lý do fail. Đây là chỗ debug "tại sao bean không tạo".exclusions: autoconfig bị user loại bỏ qua@SpringBootApplication(exclude = ...).
Ví dụ debug thực tế:
Bug: Redis cache không work dù đã thêm starter.
1. GET /actuator/conditions
2. Search "Redis" trong negativeMatches
3. Thay vì: "RedisAutoConfiguration" ở positiveMatches
Thay vào đó: "RedisAutoConfiguration" ở negativeMatches
Reason: "did not find required class RedisTemplate"
4. Kiểm tra pom.xml -- quên add spring-boot-starter-data-redis
5. Fix: them dependency, restart, check positiveMatches
Thay thế cho production: chạy java -jar app.jar --debug in autoconfig report ra console lúc startup — tiện cho dev local.
Cơ chế bên dưới — ConditionEvaluationReport
Toàn bộ thông tin /actuator/conditions đến từ ConditionEvaluationReport — một object Spring build trong quá trình ConfigurationClassPostProcessor evaluate condition. Mỗi lần một condition được đánh giá, kết quả được ghi vào report này với lý do chi tiết (ConditionMessage).
Đây cũng là lý do --debug flag in được report đầy đủ: Boot đăng ký ConditionEvaluationReportLoggingListener lắng nghe event ContextRefreshedEvent, đọc report từ context, in ra log ở level DEBUG.
flowchart TB
CPP["ConfigurationClassPostProcessor<br/>evaluate @Conditional*"]
CER["ConditionEvaluationReport<br/>ghi result + reason"]
AE["/actuator/conditions<br/>doc report, serialize JSON"]
Log["--debug flag<br/>ConditionEvaluationReportLoggingListener<br/>in console"]
CPP -->|"ghi"| CER
CER --> AE
CER --> LogHiểu cơ chế này giúp debug sâu hơn: nếu condition evaluate sai kết quả, bạn có thể breakpoint trong OnClassCondition.getMatchOutcome() hoặc OnMissingBeanCondition.getMatchOutcome() để xem chính xác Spring nhìn thấy gì.
Pitfall
Pitfall 1 — @ConditionalOnMissingBean thiếu trên default bean:
// SAI:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.build(); // luon register, conflict voi app config
}
// DUNG:
@Bean
@ConditionalOnMissingBean(SecurityFilterChain.class)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.build();
}
Pitfall 2 — @AutoConfigureAfter trên một class không phải @AutoConfiguration:
@AutoConfigureAfter/Before chỉ có nghĩa khi đặt trên @AutoConfiguration class. Đặt trên @Configuration thường → Spring bỏ qua hoàn toàn, không có ordering effect.
Pitfall 3 — Ordering giữa @ConditionalOnBean và bean tạo bởi user config:
@AutoConfiguration
@ConditionalOnBean(DataSource.class)
public class MyAutoConfig { ... }
Nếu DataSource được tạo trong @Configuration của user — liệu condition có pass không? Có, vì DeferredImportSelector (được dùng bởi autoconfig) evaluate sau tất cả @Configuration thông thường. Đây là lý do autoconfig dùng DeferredImportSelector thay ImportSelector thường: đảm bảo user bean đã có trước khi condition đánh giá.
Pitfall 4 — Đoán thay debug:
SAI: "Thử exclude DataSourceAutoConfiguration xem sao..."
DUNG: GET /actuator/conditions -> tim lý do -> fix root cause
Liên hệ các bài khác
- @Conditional bean: bài nền —
@ConditionalOn*của Boot xây trên@Conditionalcore Spring. HiểuConditioninterface giúp đọc source của mọi@ConditionalOn*annotation. - 03 — Auto-configuration deep dive: bài nguồn — pipeline đầy đủ từ
AutoConfigurationImportSelectorđến bean register; bài này đào sâu một bước trong pipeline đó (condition + ordering). - @EnableAutoConfiguration: entry point kích hoạt cả hệ thống —
@AutoConfiguration(after = ...)chỉ có nghĩa khi đượcAutoConfigurationImportSelectorload.
Tóm tắt
@ConditionalOn*đặt điều kiện cho bean/autoconfig — 12 annotation, AND logic khi combine.@ConditionalOnMissingBeanlà cơ chế back-off: autoconfig khai bean mặc định nhưng nhường khi user đã có bean cùng kiểu — "opinionated but not lock-in".- Mọi
@Beandefault trong autoconfig bắt buộc có@ConditionalOnMissingBean— thiếu → conflict khi user override. @AutoConfigureBefore/Afterxác định thứ tự evaluate.@ConditionalOnMissingBeanphụ thuộc thứ tự — autoconfig đến trước win.- Cơ chế ordering:
AutoConfigurationSorterbuild DAG từ relationship rồi topological sort — không phải ép sequential run. DeferredImportSelectorđảm bảo autoconfig evaluate sau mọi user@Configuration—@ConditionalOnBeanvới bean user khai sẽ thấy đúng.- Debug:
/actuator/conditions(production) hoặc--debug(dev). Đừng đoán.
Tự kiểm tra
Q1Tại sao autoconfig dùng @ConditionalOnClass(name = "com.example.Foo") thay vì @ConditionalOnClass(Foo.class)? Điều gì xảy ra nếu dùng class literal khi class không có classpath?▸
@ConditionalOnClass(name = "com.example.Foo") thay vì @ConditionalOnClass(Foo.class)? Điều gì xảy ra nếu dùng class literal khi class không có classpath?Autoconfig phải compile được kể cả khi class đích không có classpath của dự án autoconfig. Nếu dùng @ConditionalOnClass(Foo.class), trình biên dịch cần import được Foo — nếu Foo không có dependency của module autoconfig thì compile fail, ngay cả khi ý định là "chỉ load khi Foo có".
Dùng name = "com.example.Foo" là string thuần — compile không phân giải gì. Runtime, Spring Boot dùng ClassLoader.getResource("com/example/Foo.class") để check sự tồn tại mà không cần load class. Nếu không tìm thấy → condition false → autoconfig skip.
Thực tế: mọi autoconfig core của Boot dùng name attribute cho @ConditionalOnClass khi class đến từ thư viện ngoài. Chỉ dùng class literal khi class đó chắc chắn có trong classpath vô điều kiện (vd class của chính module autoconfig).
Q2Mô tả back-off pattern bằng ví dụ cụ thể. Tại sao đây là cơ chế "override được" mà không cần exclude autoconfig toàn bộ?▸
Back-off pattern: autoconfig khai một bean mặc định có @ConditionalOnMissingBean. Khi user khai bean cùng kiểu trong @Configuration của mình, condition OnMissingBean fail → autoconfig skip bean đó, dùng bean của user.
Ví dụ: CacheAutoConfiguration khai ConcurrentMapCacheManager với @ConditionalOnMissingBean. User khai RedisCacheManager trong AppConfig. Boot thấy CacheManager đã có → skip default. User không cần viết @SpringBootApplication(exclude = CacheAutoConfiguration.class).
Sức mạnh: override một bean cụ thể mà không mất phần còn lại của autoconfig. Nếu exclude toàn bộ CacheAutoConfiguration, bạn mất luôn binding @EnableConfigurationProperties, event listener, và các bean phụ trong autoconfig đó. Back-off chính xác hơn: chỉ nhường đúng bean user muốn override.
Q3Có 2 autoconfig A và B. B khai bean Foo với @ConditionalOnMissingBean. A khai bean Foo không có điều kiện. Không có ordering. Điều gì xảy ra? Fix ra sao?▸
A và B. B khai bean Foo với @ConditionalOnMissingBean. A khai bean Foo không có điều kiện. Không có ordering. Điều gì xảy ra? Fix ra sao?Thứ tự evaluate không xác định — phụ thuộc vào classpath scan order. Hai tình huống:
Nếu B evaluate trước A: B thấy chưa có Foo → register Foo từ B. A evaluate sau, không có @ConditionalOnMissingBean → cố register thêm Foo → BeanDefinitionOverrideException (Boot 2.1+ mặc định cấm override).
Nếu A evaluate trước B: A register Foo. B thấy Foo đã có → skip. Ổn.
Fix: hai hướng:
1. Thêm @AutoConfiguration(after = A.class) trên B — đảm bảo A luôn chạy trước, B luôn skip.
2. Thêm @ConditionalOnMissingBean vào Foo bean của A — cả hai nhường nhau, chỉ bean nào đến trước win.
Quy tắc: nếu hai autoconfig có thể produce cùng bean type, ít nhất một phải có ordering rõ ràng, và bean default phải có @ConditionalOnMissingBean.
Q4Tại sao @ConditionalOnBean(DataSource.class) trong autoconfig thấy được DataSource do user khai trong @Configuration? Cơ chế kỹ thuật nào đảm bảo điều này?▸
@ConditionalOnBean(DataSource.class) trong autoconfig thấy được DataSource do user khai trong @Configuration? Cơ chế kỹ thuật nào đảm bảo điều này?Autoconfig được load qua AutoConfigurationImportSelector, implement DeferredImportSelector — interface Spring dùng để delay import đến sau khi mọi @Configuration thông thường đã được process.
Thứ tự trong ConfigurationClassPostProcessor:
1. Parse và process tất cả @Configuration user (khai trong app).
2. Sau khi user config xong, mới gọi DeferredImportSelector.selectImports() để load autoconfig.
3. Autoconfig evaluate condition — lúc này bean definition của user đã có trong beanDefinitionMap.
Nếu autoconfig dùng ImportSelector thường (không Deferred) — evaluate cùng lúc user config — có thể thấy chưa đủ bean. DeferredImportSelector là lý do autoconfig luôn thua user config về thứ tự — thiết kế có chủ đích.
Hệ quả thực tế: bạn luôn có thể override autoconfig bằng cách khai bean trong @Configuration thông thường — không cần @Order, không cần @Primary. Autoconfig tự nhận ra và nhường.
Q5Bạn thêm spring-boot-starter-data-redis nhưng bean RedisTemplate không tạo. Quy trình debug chính xác bằng /actuator/conditions?▸
spring-boot-starter-data-redis nhưng bean RedisTemplate không tạo. Quy trình debug chính xác bằng /actuator/conditions?Bước 1 — Bật endpoint: thêm management.endpoints.web.exposure.include: health,conditions vào application.yml. Restart app.
Bước 2 — Fetch và đọc: GET /actuator/conditions. Tìm RedisAutoConfiguration.
Bước 3 — Đọc negativeMatches: nếu RedisAutoConfiguration nằm trong negativeMatches, đọc trường notMatched[].message. Ba lý do phổ biến:
- did not find required class RedisTemplate → starter chưa kéo đúng jar (kiểm tra mvn dependency:tree).
- spring.data.redis.host not set → thiếu property host.
- RedisTemplate bean already defined → đang ở positiveMatches nhưng bean của bạn là custom — check positiveMatches lần nữa.
Bước 4 — Nếu trong positiveMatches nhưng vẫn không work: GET /actuator/beans search redisTemplate — verify bean tồn tại. Nếu có, vấn đề không phải autoconfig mà là connection (Redis chưa chạy, host sai).
Quy tắc: /actuator/conditions chỉ answer câu hỏi "bean có được register không và tại sao". Câu hỏi runtime (connection, data) cần log và /actuator/health.
Bài tiếp theo: Mini-challenge — trace 1 request
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
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