@Conditional — đăng ký bean có điều kiện và nền tảng auto-configuration
Bài này bóc đúng một thứ: @Conditional và family @ConditionalOn* hoạt động ra sao bên dưới — condition evaluate lúc đăng ký BeanDefinition (BFPP phase), bean chỉ register khi condition đúng. Hiểu cơ chế này giải thích tại sao Spring Boot auto-configuration 'back off' khi user tự khai báo bean.
TL;DR: @Conditional là annotation gắn vào @Bean method hoặc @Configuration class, yêu cầu container evaluate một điều kiện trước khi đăng ký BeanDefinition vào beanDefinitionMap. Điều kiện được kiểm tra trong pha BeanFactoryPostProcessor — trước khi bất kỳ bean nào được tạo. Nếu điều kiện sai, BeanDefinition không được thêm vào; bean không tồn tại trong container. Family @ConditionalOnClass/@ConditionalOnMissingBean/@ConditionalOnProperty là specialization của cùng cơ chế đó. Spring Boot dùng @ConditionalOnMissingBean để "back off" — chỉ cung cấp bean mặc định khi user chưa tự khai báo.
1. Vấn đề @Conditional giải quyết
Hãy hình dung một library cung cấp integration với Redis: nếu spring-boot-starter-data-redis có trong classpath, tạo bean CacheManager dùng Redis. Nếu không, tạo bean fallback dùng in-memory. Không có @Conditional, bạn phải tự viết if/else trong code, hoặc yêu cầu user bật/tắt config thủ công — cồng kềnh và dễ lỗi.
@Conditional giải quyết đúng điều đó: khai báo bean kèm điều kiện, container tự quyết định đăng ký hay bỏ qua lúc khởi động.
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(factory),
RedisCacheConfiguration.defaultCacheConfig());
}
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager fallbackCacheManager() {
return new ConcurrentMapCacheManager("default");
}
}
Nếu Redis không có trong classpath: redisCacheManager bị skip, fallbackCacheManager được đăng ký. Nếu Redis có, redisCacheManager được đăng ký, fallbackCacheManager thấy "đã có CacheManager rồi" nên cũng bị skip.
2. Cơ chế bên dưới — condition evaluate ở pha nào?
Điểm cốt lõi mà nhiều dev bỏ qua: @Conditional không hoạt động khi bean được tạo (instantiation phase) — nó hoạt động khi BeanDefinition được đăng ký vào beanDefinitionMap.
Nhớ lại từ bài 01 — BeanFactory vs ApplicationContext: container tách làm 2 pha rõ ràng:
- Registration phase: scan
@Component, parse@Beanmethod → điền metadata vàobeanDefinitionMap. - Instantiation phase: duyệt
beanDefinitionMap→createBean→ điềnsingletonObjects.
@Conditional can thiệp ở pha 1. Class chịu trách nhiệm là ConfigurationClassPostProcessor — một BeanFactoryPostProcessor (viết tắt BFPP) chạy sau khi Spring hoàn tất scan nhưng trước khi instantiate bất kỳ bean nào.
flowchart TB
Scan["Component scan<br/>+ parse @Bean method"] --> BFPP
subgraph BFPP["ConfigurationClassPostProcessor (BeanFactoryPostProcessor)"]
direction TB
Eval["evaluate Condition<br/>cho tung @Bean method"]
Eval -->|"condition = true"| Reg["register BeanDefinition<br/>vao beanDefinitionMap"]
Eval -->|"condition = false"| Skip["skip -- khong register<br/>bean khong ton tai"]
end
BFPP --> Inst["Instantiation phase<br/>createBean cho tung definition"]
Reg --> InstHệ quả thực tế: khi Spring Boot gặp @ConditionalOnMissingBean(CacheManager.class), nó tra beanDefinitionMap tại thời điểm đó. Nếu đã có entry CacheManager, condition sai, bean bị skip. Nếu chưa có, condition đúng, bean được đăng ký.
Đây chính xác là lý do thứ tự khai báo config có thể ảnh hưởng đến conditional: nếu autoconfig của Spring Boot evaluate trước user config, @ConditionalOnMissingBean sẽ thấy map còn trống. Spring Boot giải quyết bằng @AutoConfigureAfter/@AutoConfigureBefore — đảm bảo user config chạy trước, autoconfig chạy sau để "back off" đúng lúc.
3. Interface Condition — tự viết condition
Mọi annotation @ConditionalOn* đều là specialization của interface core:
// File: org/springframework/context/annotation/Condition.java
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
ConditionContext cung cấp quyền truy cập vào:
getBeanFactory()— trabeanDefinitionMaphiện tại.getEnvironment()— đọc property từapplication.properties/env var/system property.getClassLoader()— kiểm tra class có trong classpath không.getResourceLoader()— kiểm tra file/resource tồn tại không.
Ví dụ condition tự định nghĩa: chỉ load bean khi đang chạy trên Linux:
public class OnLinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String osName = System.getProperty("os.name", "").toLowerCase();
return osName.contains("linux");
}
}
@Configuration
public class FilesystemConfig {
@Bean
@Conditional(OnLinuxCondition.class)
public FileSystemWatcher inotifyWatcher() {
return new InotifyFileSystemWatcher();
}
}
@Conditional nhận class implement Condition. Có thể kết hợp nhiều condition trên cùng một bean — tất cả phải đúng (AND logic):
@Bean
@Conditional(OnLinuxCondition.class)
@ConditionalOnProperty(name = "fs.watch.enabled", havingValue = "true")
public FileSystemWatcher inotifyWatcher() { ... }
4. Family @ConditionalOn* — bộ condition dựng sẵn
Spring Boot cung cấp 12+ specialization của @Conditional, mỗi cái evaluate một loại điều kiện phổ biến:
| Annotation | Condition | Use case điển hình |
|---|---|---|
@ConditionalOnClass | Class có trong classpath | Auto-config bật khi user thêm starter |
@ConditionalOnMissingClass | Class không có trong classpath | Fallback khi không có driver |
@ConditionalOnBean | Bean kiểu X đã được register | Bean phụ thuộc bean khác phải tồn tại trước |
@ConditionalOnMissingBean | Bean kiểu X chưa được register | Default bean — user override được |
@ConditionalOnProperty | Property có giá trị nhất định | Feature flag, env-specific bean |
@ConditionalOnExpression | SpEL expression trả về true | Logic phức tạp kết hợp nhiều property |
@ConditionalOnWebApplication | App là web (servlet hoặc reactive) | Bean chỉ dành cho web context |
@ConditionalOnNotWebApplication | App không phải web | Bean chỉ dành cho CLI/batch |
@ConditionalOnResource | File/resource tồn tại trên classpath | License file, schema file cần có |
@ConditionalOnJava | Java version match range | Compatibility với nhiều Java version |
@Profile | Active Spring profile match | Dev/staging/prod config khác nhau |
4.1 @ConditionalOnProperty — chi tiết
@ConditionalOnProperty là annotation được dùng nhiều nhất cho feature flag:
@Bean
@ConditionalOnProperty(
name = "feature.email-notification.enabled",
havingValue = "true",
matchIfMissing = false // skip neu property khong ton tai
)
public EmailNotificationService emailNotificationService(MailSender sender) {
return new DefaultEmailNotificationService(sender);
}
Giải thích từng attribute:
name: tên property trongapplication.properties(hoặc env var theo conventionFEATURE_EMAIL_NOTIFICATION_ENABLED).havingValue: giá trị mong muốn. Mặc định là"true"nếu không khai báo.matchIfMissing: nếutrue, condition đúng khi property không tồn tại (ngược lại:false= skip khi thiếu property).
4.2 @ConditionalOnMissingBean — cơ chế "back off"
Đây là annotation quan trọng nhất trong hệ sinh thái Spring Boot auto-configuration. Nguyên tắc:
Spring Boot chỉ cung cấp bean mặc định khi user chưa tự khai báo bean cùng kiểu.
// Trong DataSourceAutoConfiguration của Spring Boot (rut gon)
@Bean
@ConditionalOnMissingBean(DataSource.class)
public DataSource embeddedDatabase(EmbeddedDatabaseBuilder builder) {
return builder.setType(H2).build();
}
Khi user tự khai báo DataSource bean trong code của mình, @ConditionalOnMissingBean thấy đã có DataSource trong beanDefinitionMap → bean H2 của Spring Boot bị skip. User không cần tắt autoconfig thủ công — chỉ cần khai báo bean của mình.
Đây là convention over configuration thực thi ở tầng container: Spring Boot cung cấp cấu hình mặc định, user override bằng cách khai báo bean của mình. Hành vi "back off" là kết quả trực tiếp của @ConditionalOnMissingBean.
5. @Conditional trên cả @Configuration class
@Conditional không chỉ đặt được trên @Bean method mà còn đặt được trên cả class @Configuration. Khi đặt trên class, toàn bộ class — kể cả mọi @Bean method bên trong — bị bỏ qua nếu condition sai:
@Configuration
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisOperations")
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
}
Trong ví dụ trên: nếu RedisOperations không có trong classpath, cả class RedisAutoConfiguration bị skip — cả 2 @Bean method không bao giờ được process. Không cần đặt @ConditionalOnClass lặp lại trên mỗi method.
Pattern này rất phổ biến trong Spring Boot: @ConditionalOnClass ở class level để guard toàn bộ config, @ConditionalOnMissingBean ở method level để cho phép user override từng bean.
6. Pitfall thường gặp
Pitfall 1 — @ConditionalOnMissingBean evaluate sai khi thứ tự config không đúng:
// SAI: neu autoconfig cua Spring Boot chay TRUOC user config,
// @ConditionalOnMissingBean thay "map trong" -> dang ky bean mac dinh
// User config chay SAU -> co 2 bean cung kieu -> BeanDefinitionOverrideException
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager defaultCache() { ... }
Nguyên nhân: trong cùng @Configuration class, thứ tự method theo source order. Giữa các class, Spring Boot dùng @AutoConfigureAfter/@AutoConfigureBefore. Khi viết config library, phải khai báo thứ tự evaluate rõ ràng.
Cách fix: nếu bạn là tác giả library, dùng @AutoConfigureAfter(UserConfig.class) để đảm bảo user config được process trước.
Pitfall 2 — Nhầm @ConditionalOnClass với @ConditionalOnBean:
// SAI: kiem tra CLASS co trong classpath - khong dam bao co BEAN duoc dang ky
@ConditionalOnClass(RedisTemplate.class) // class co nhung bean chua cau hinh
// DUNG: kiem tra BEAN da duoc register
@ConditionalOnBean(RedisConnectionFactory.class) // dam bao bean ton tai
@ConditionalOnClass chỉ kiểm tra class có trong classpath không (compile-time dependency). Nó không đảm bảo bean của class đó tồn tại. Dùng @ConditionalOnBean khi cần đảm bảo dependency bean đã được đăng ký.
Pitfall 3 — @ConditionalOnMissingBean không kiểm tra subtype:
// ConcreteMailSender implements MailSender
@Bean
@ConditionalOnMissingBean(MailSender.class) // chi kiem tra EXACT type MailSender
public MailSender defaultMailSender() { ... }
// Neu user khai bao:
@Bean
public ConcreteMailSender myMailSender() { ... } // subtype, KHONG bi detect
@ConditionalOnMissingBean mặc định kiểm tra theo type hierarchy (bao gồm subtype). Nhưng cần xác nhận kỹ với interface và abstract class — đặc biệt khi type hierarchy phức tạp.
7. Tại sao @Conditional là nền của auto-configuration
Bài 01 — @Configuration & @Bean giới thiệu @Bean và @Configuration. Bài đó mention @ConditionalOn* là "cốt lõi của Spring Boot auto-configuration". Bài này giải thích cơ chế đó.
Spring Boot auto-configuration hoạt động theo một pattern đơn giản nhưng mạnh:
Classpath detect (ConditionalOnClass)
→ Register default bean (nếu chưa có: ConditionalOnMissingBean)
→ User override bằng cách tự khai báo bean
→ Auto-config "back off" vì ConditionalOnMissingBean thấy bean đã có
Toàn bộ spring-boot-autoconfigure jar chứa hàng trăm @Configuration class theo pattern này. Khi bạn thêm spring-boot-starter-web, Spring Boot tự cấu hình DispatcherServlet, HttpMessageConverter, Jackson... vì các @ConditionalOnClass tương ứng đúng. Khi bạn khai báo custom ObjectMapper bean, @ConditionalOnMissingBean của Spring Boot tự skip bean ObjectMapper mặc định.
Đây là lý do Spring Boot app "tự cấu hình" mà không cần XML dài dòng: mọi bean mặc định đều có điều kiện, user override đơn giản bằng khai báo bean.
Auto-configuration deep dive sẽ đào sâu cách Spring Boot load file AutoConfiguration.imports, thứ tự evaluate, và cách debug autoconfig nào được áp dụng.
Liên hệ các bài khác
- @Bean & @Configuration:
@Conditionalgắn lên@Beanmethod hoặc@Configurationclass — phải hiểu cơ chế@Beantrước để biết@Conditionalcan thiệp ở bước nào trong quá trình đăng ký. - BeanFactory vs ApplicationContext:
@Conditionalhoạt động ở pha đăng kýBeanDefinitionvàobeanDefinitionMap— bài kia giải thích tại sao 2 map (beanDefinitionMap+singletonObjects) tách biệt nhau, và vai trò của BFPP phase. - Auto-configuration deep dive: autoconfig của Spring Boot xây hoàn toàn trên
@Conditional— bài đó cho thấy bức tranh đầy đủ: fileAutoConfiguration.imports,@AutoConfigureAfter, debug bằngConditionEvaluationReport.
Tóm tắt
@Conditionalđặt trên@Beanmethod hoặc@Configurationclass, yêu cầu container evaluate mộtConditiontrước khi đăng kýBeanDefinition.- Condition được evaluate trong pha
ConfigurationClassPostProcessor(BFPP) — sau scan, trước instantiation. Bean không được đăng ký nếu condition sai; bean không tồn tại trong container. - Interface
Conditioncó 1 method:matches(ConditionContext, AnnotatedTypeMetadata).ConditionContextcho phép trabeanDefinitionMap, đọc property, kiểm tra classpath. @ConditionalOnClass: class có trong classpath.@ConditionalOnMissingBean: bean chưa được register.@ConditionalOnProperty: property có giá trị nhất định.@ConditionalOnMissingBeanlà cơ chế "back off" của Spring Boot auto-configuration: chỉ đăng ký bean mặc định khi user chưa tự khai báo bean cùng kiểu.- Đặt
@ConditionalOnClassở class level để guard toàn bộ@Configuration,@ConditionalOnMissingBeanở method level để user override từng bean. - Thứ tự evaluate quan trọng: Spring Boot dùng
@AutoConfigureAfter/@AutoConfigureBefoređể đảm bảo user config chạy trước autoconfig.
Tự kiểm tra
Q1Ở pha nào trong vòng đời container Spring, @Conditional được evaluate? Giải thích tại sao nó phải xảy ra trước instantiation phase.▸
@Conditional được evaluate? Giải thích tại sao nó phải xảy ra trước instantiation phase.@Conditional được evaluate trong pha BeanFactoryPostProcessor, cụ thể là trong ConfigurationClassPostProcessor — sau khi Spring hoàn tất component scan và parse @Bean method, nhưng trước khi bất kỳ bean nào được khởi tạo (createBean).
Lý do phải xảy ra trước instantiation: @Conditional kiểm soát việc đăng ký BeanDefinition vào beanDefinitionMap. Nếu condition sai, BeanDefinition không được thêm vào map — bean không tồn tại về mặt metadata. Instantiation phase đọc beanDefinitionMap để tạo bean; nếu entry không có trong map, không có gì để instantiate.
Nếu evaluate sau instantiation, bean đã được tạo rồi — không còn ý nghĩa "có điều kiện" nữa, chỉ là destroy sau khi tạo — vừa lãng phí, vừa không clean.
Q2Giải thích tại sao Spring Boot dùng @ConditionalOnMissingBean để "back off". Khi user khai báo DataSource bean của mình, Spring Boot auto-configuration biết thế nào để không đăng ký DataSource mặc định?▸
@ConditionalOnMissingBean để "back off". Khi user khai báo DataSource bean của mình, Spring Boot auto-configuration biết thế nào để không đăng ký DataSource mặc định?Cơ chế như sau: Spring Boot auto-configuration class (vd DataSourceAutoConfiguration) được đánh dấu để chạy sau user config nhờ @AutoConfigureAfter. Khi ConfigurationClassPostProcessor process class autoconfig này, nó gặp @ConditionalOnMissingBean(DataSource.class).
Tại thời điểm đó, beanDefinitionMap đã có entry cho DataSource bean mà user khai báo (vì user config đã được process trước). @ConditionalOnMissingBean tra map, thấy đã có DataSource → condition sai → bean mặc định của Spring Boot bị skip, không được đăng ký.
Đây là "back off": Spring Boot chỉ cung cấp bean khi không ai khác đã cung cấp. User override bằng cách khai báo bean của mình — không cần tắt autoconfig thủ công hay chỉnh thêm config nào. Convention over configuration thực thi ở tầng container.
Q3Phân biệt @ConditionalOnClass và @ConditionalOnBean. Cho ví dụ tình huống cần dùng mỗi loại.▸
@ConditionalOnClass và @ConditionalOnBean. Cho ví dụ tình huống cần dùng mỗi loại.@ConditionalOnClass(Foo.class): condition đúng nếu class Foo tồn tại trên classpath — tức là dependency đã được thêm vào pom.xml hoặc build.gradle. Nó không quan tâm Foo có được khai báo là bean hay không.
@ConditionalOnBean(Foo.class): condition đúng nếu có bean kiểu Foo đã được đăng ký vào beanDefinitionMap. Class có thể có trong classpath nhưng chưa tạo bean.
Ví dụ cần @ConditionalOnClass: guard toàn bộ Redis config chỉ khi user thêm spring-boot-starter-data-redis. Nếu starter không có, class RedisTemplate không tồn tại trên classpath → bỏ qua toàn bộ config, tránh ClassNotFoundException.
Ví dụ cần @ConditionalOnBean: tạo TransactionTemplate chỉ khi có bean PlatformTransactionManager đã được đăng ký. Class PlatformTransactionManager luôn có trên classpath (spring-tx), nhưng bean của nó chỉ tồn tại khi user cấu hình transaction. Dùng @ConditionalOnClass sẽ luôn đúng và không capture đúng điều kiện cần.
Q4Đoạn code sau có vấn đề gì? Bean nào sẽ thực sự được đăng ký?@Configuration
public class NotificationConfig {
@Bean
@ConditionalOnMissingBean(NotificationService.class)
public NotificationService emailService() {
return new EmailNotificationService();
}
@Bean
public NotificationService smsService() {
return new SmsNotificationService();
}
}
▸
@Configuration
public class NotificationConfig {
@Bean
@ConditionalOnMissingBean(NotificationService.class)
public NotificationService emailService() {
return new EmailNotificationService();
}
@Bean
public NotificationService smsService() {
return new SmsNotificationService();
}
}Vấn đề nằm ở thứ tự method trong cùng @Configuration class.
Spring process @Bean method theo source order. emailService() khai báo trước, smsService() khai báo sau. Khi evaluate @ConditionalOnMissingBean(NotificationService.class) cho emailService(), beanDefinitionMap chưa có NotificationService nào → condition đúng → emailService được đăng ký.
Tiếp theo smsService() không có condition → cũng được đăng ký. Kết quả: có 2 bean kiểu NotificationService trong container. Từ Spring Boot 2.1+, nếu không có @Primary hoặc @Qualifier, inject NotificationService sẽ throw NoUniqueBeanDefinitionException.
Cách fix ý định đúng: hoặc thêm @ConditionalOnMissingBean cho cả smsService(), hoặc khai báo smsService() trước và emailService() sau (để @ConditionalOnMissingBean thấy sms đã có và skip email), hoặc tách thành 2 class config riêng với thứ tự rõ ràng qua @AutoConfigureAfter.
Q5Viết một custom Condition kiểm tra property app.mode có giá trị "demo" VÀ class com.example.DemoFeature tồn tại trong classpath. Không dùng @ConditionalOnProperty + @ConditionalOnClass mà tự implement Condition interface.▸
Condition kiểm tra property app.mode có giá trị "demo" VÀ class com.example.DemoFeature tồn tại trong classpath. Không dùng @ConditionalOnProperty + @ConditionalOnClass mà tự implement Condition interface.public class OnDemoModeWithFeatureCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Kiem tra property
String mode = context.getEnvironment()
.getProperty("app.mode", "");
if (!"demo".equals(mode)) {
return false;
}
// Kiem tra class trong classpath
try {
context.getClassLoader().loadClass("com.example.DemoFeature");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
// Su dung:
@Bean
@Conditional(OnDemoModeWithFeatureCondition.class)
public DemoFeatureService demoFeatureService() {
return new DemoFeatureService();
}Giải thích: ConditionContext.getEnvironment() đọc từ toàn bộ property source (file application.properties, env var, system property). ConditionContext.getClassLoader() trả về classloader của application — dùng để kiểm tra class tồn tại.
Cả 2 điều kiện phải đúng (short-circuit AND). Nếu property sai, trả về false ngay, không check classpath nữa.
Trên thực tế, dùng @ConditionalOnProperty + @ConditionalOnClass đặt cùng nhau sẽ clean hơn. Tự implement Condition phù hợp khi logic phức tạp hơn (vd kiểm tra giá trị trong DB, hoặc điều kiện runtime không thể diễn đạt bằng annotation sẵn có).
Bài tiếp theo: Tổng kết module
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