Mastering SpringBoot @Conditional Annotations for On‑Demand Bean Loading
The article explains how SpringBoot’s @Conditional meta‑annotation enables dynamic, zero‑intrusion bean creation by replacing hard‑coded if‑logic, details the underlying Condition and ConditionContext APIs, execution order, performance benefits, built‑in derived annotations across six categories, and shows practical code examples and custom condition implementations.
In large‑scale enterprise projects, custom middleware and internal starters often need to create beans dynamically. Traditional hard‑coded if checks lead to severe coupling, require code changes and restarts, lack standardized control, and cannot prioritize user‑defined beans over framework defaults.
SpringBoot’s @Conditional meta‑annotation
SpringBoot builds a family of ready‑to‑use conditional annotations on top of the top‑level meta‑annotation @Conditional, providing a zero‑intrusion, configurable, extensible and environment‑isolated standard for bean loading, which is the core of the starter auto‑configuration mechanism.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
// Pass an array of Condition implementations; multiple conditions are AND‑ed
Class<? extends Condition>[] value();
}Core interfaces
The Condition interface defines the single method that decides whether a bean definition should be registered:
public interface Condition {
/**
* @param context provides access to environment, bean factory, class loader, etc.
* @param metadata annotation metadata of the class or method
* @return true to keep the bean definition, false to discard it
*/
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
} ConditionContextsupplies the data source for the decision:
public interface ConditionContext {
// 1. Environment (properties, system vars, command‑line args)
Environment getEnvironment();
// 2. Configurable bean factory (query existing BeanDefinitions)
ConfigurableListableBeanFactory getBeanFactory();
// 3. Class loader (check classpath presence)
ClassLoader getClassLoader();
// 4. Resource loader (read classpath resources)
ResourceLoader getResourceLoader();
} AnnotatedTypeMetadatalets a Condition read attributes of the annotation it decorates, e.g. @ConditionalOnProperty can obtain prefix, name and havingValue values.
Effective scope and execution order
When placed on a @Configuration class, all @Bean methods inside are governed by the condition.
When placed directly on a @Bean method, only that bean is affected.
It cannot be used on plain component stereotypes ( @Service, @Component, @Repository) because component scanning occurs before condition evaluation, rendering the annotation ineffective.
During container startup the sequence is:
Scan all configuration classes and resolve their @Bean definitions.
Read every @Conditional (or derived) annotation attached to the class or method.
Instantiate the specified Condition implementation(s) and invoke matches().
If any condition returns false, discard the BeanDefinition; otherwise keep it for later instantiation.
Performance advantage
The condition is evaluated only once during the bean‑definition scanning phase; no runtime re‑checks occur. Beans that fail the condition are never loaded into the bean factory, eliminating memory overhead and avoiding any performance penalty in high‑concurrency scenarios.
Built‑in derived annotations (six categories)
Class‑presence checks : @ConditionalOnClass, @ConditionalOnMissingClass – activate only when a specific class is (or is not) on the classpath.
Bean‑existence checks : @ConditionalOnBean, @ConditionalOnMissingBean, @ConditionalOnSingleCandidate – depend on the presence or absence of a bean type or name.
Property‑based checks : @ConditionalOnProperty – match configuration keys, supporting prefixes, multiple values and matchIfMissing.
Web‑environment checks : @ConditionalOnWebApplication, @ConditionalOnServletWebApplication, @ConditionalOnReactiveWebApplication, @ConditionalOnNotWebApplication – discriminate between servlet, reactive, or non‑web contexts.
System/JDK/resource checks : @ConditionalOnResource, @ConditionalOnJava, @ConditionalOnSystemProperty – evaluate classpath resources, JDK version ranges, or system properties.
Development/Test‑only checks (SpringBoot 2.7+) : @ConditionalOnDevProperty, @ConditionalOnTestContainer – activate beans only in dev or test container environments.
Practical code examples
Below are representative snippets that demonstrate each category.
@ConditionalOnClass – two ways to declare the target class
@Configuration
@ConditionalOnClass(RedisTemplate.class)
public class RedisAutoConfig {
@Bean
public RedisUtil redisUtil() {
return new RedisUtil();
}
}
@Configuration
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
public class RedisAutoConfig {
@Bean
public RedisUtil redisUtil() {
return new RedisUtil();
}
}@ConditionalOnMissingClass – fallback mock implementation
@Configuration
@ConditionalOnMissingClass("co.elastic.clients.elasticsearch.ElasticsearchClient")
public class MockSearchConfig {
@Bean
public SearchService searchService() {
return new MockSearchService();
}
}@ConditionalOnMissingBean – user‑defined bean overrides default
@Configuration
@ConditionalOnClass(RedisTemplate.class)
public class RedisAutoConfig {
// Type‑based match: if a RedisUtil bean already exists, this bean is skipped
@Bean
@ConditionalOnMissingBean
public RedisUtil redisUtil() {
return new RedisUtil();
}
// Name‑based match for precise control
@Bean
@ConditionalOnMissingBean(name = "redisUtil")
public RedisUtil redisUtil() {
return new RedisUtil();
}
}When a custom CustomRedisUtil component is present, the framework detects the bean and does not instantiate the default RedisUtil.
@ConditionalOnBean – dependent component creation
@Configuration
public class RedisTaskConfig {
@Bean
@ConditionalOnBean(RedisConnectionFactory.class)
public RedisCacheRefreshTask cacheRefreshTask() {
return new RedisCacheRefreshTask();
}
}@ConditionalOnProperty – global switch and multi‑environment loading
// Global rate‑limit switch (default off)
@Configuration
@ConditionalOnProperty(prefix = "component.limit", name = "enable", havingValue = "true", matchIfMissing = false)
public class RateLimitConfig {
@Bean
public RateLimitAspect rateLimitAspect() {
return new RateLimitAspect();
}
}
// Production vs development monitoring beans
@Configuration
@ConditionalOnProperty(prefix = "component.monitor", name = "env", havingValue = "prod")
public class ProdMonitorConfig {
@Bean
public MonitorReport monitorReport() {
return new ProdMonitorReport();
}
}
@Configuration
@ConditionalOnProperty(prefix = "component.monitor", name = "env", havingValue = {"dev", "test"})
public class DevMockConfig {
@Bean
public MonitorReport monitorReport() {
return new MockMonitorReport();
}
}Web‑environment annotations
// Servlet MVC interceptor
@Configuration
@ConditionalOnServletWebApplication
public class MvcLogConfig implements WebMvcConfigurer {
@Bean
public GlobalLogInterceptor logInterceptor() {
return new GlobalLogInterceptor();
}
}
// Reactive WebFlux filter
@Configuration
@ConditionalOnReactiveWebApplication
public class FluxTraceConfig {
@Bean
public GlobalTraceFilter traceFilter() {
return new GlobalTraceFilter();
}
}
// Non‑web batch task
@Configuration
@ConditionalOnNotWebApplication
public class BatchCleanConfig {
@Bean
public DataCleanTask dataCleanTask() {
return new DataCleanTask();
}
}@ConditionalOnResource – load a datasource only when a custom properties file exists
@Configuration
@ConditionalOnResource(resources = "classpath:custom-druid.properties")
public class CustomDataSourceConfig {
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
}@ConditionalOnJava – JDK version isolation
@Configuration
@ConditionalOnJava(range = ConditionalOnJava.Range.EQUAL_OR_NEWER, value = JavaVersion.SEVENTEEN)
public class VirtualThreadConfig {
@Bean
public Executor virtualThreadPool() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}Custom conditional annotation
Built‑in annotations support only AND logic. For more complex rules (e.g., internal‑network IP + development profile) you can create a custom Condition and a meta‑annotation.
Step 1 – implement the Condition
public class InnerNetCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
String ip = env.getProperty("server.bind.ip", "127.0.0.1");
return ip.startsWith("127.") || ip.startsWith("192.168.");
}
}Step 2 – define the meta‑annotation
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(InnerNetCondition.class)
public @interface ConditionalOnInnerNet {}Step 3 – use it in configuration
@Configuration
@ConditionalOnInnerNet
public class DevDebugConfig {
@Bean
public DebugApiTool debugApiTool() {
return new DebugApiTool();
}
}For OR logic you implement a Condition that checks multiple independent predicates, e.g. development profile OR internal network:
public class DevOrInnerCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
boolean isDev = "dev".equals(env.getProperty("component.monitor.env"));
String ip = env.getProperty("server.bind.ip");
boolean isInner = ip != null && ip.startsWith("192.168");
return isDev || isInner;
}
}Composite example – multiple built‑in and custom conditions
@Configuration
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnProperty(prefix = "component.limit", name = "enable", havingValue = "true")
@ConditionalOnWebApplication
@ConditionalOnInnerNet
public class AdvancedLimitConfig {
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter();
}
}This bean is created only when Redis is on the classpath, the rate‑limit switch is on, the application runs in a web environment, and the server IP belongs to the internal network.
Conclusion
Conditional annotations form the backbone of SpringBoot’s auto‑configuration, component plug‑in, multi‑environment isolation and framework extensibility. Mastering the built‑in suite and learning to craft custom Condition implementations empowers developers to build clean, modular starters, replace hard‑coded logic with declarative configuration, and keep large codebases maintainable.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
