@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 chỉ có một câu hỏi: đã có bean kiểu CacheManager trong context chưa? Có rồi thì Boot bỏ qua @Bean của mình và dùng bean của user; chưa có thì tạo ConcurrentMapCacheManager làm default.
Vì 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ộ. Hai thao tác đó nghe như thay thế nhau, nhưng thứ bạn mất kèm thì khác hẳn:

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 chạy theo đúng thứ tự khai báo: DataSourceAutoConfiguration tạo DataSource → HibernateJpaAutoConfiguration tạo EntityManagerFactory → JpaRepositoriesAutoConfiguration tạo repository proxy. Mỗ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. Hai đường ra — endpoint Actuator và console log — đọc chung một object trong context, nên chúng không bao giờ lệch nhau.
Hiể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? - 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ộ?
- Q3Có 2 autoconfig
AvàB.Bkhai beanFoovới@ConditionalOnMissingBean.Akhai beanFookhông có điều kiện. Không có ordering. Điều gì xảy ra? Fix ra sao? - Q4Tại sao
@ConditionalOnBean(DataSource.class)trong autoconfig thấy đượcDataSourcedo user khai trong@Configuration? Cơ chế kỹ thuật nào đảm bảo điều này? - Q5Bạn thêm
spring-boot-starter-data-redisnhưng beanRedisTemplatekhông tạo. Quy trình debug chính xác bằng/actuator/conditions?
Bài tiếp theo: Mini-challenge — trace 1 request
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
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