How Spring Boot’s Conditional Annotations Dynamically Control Bean Registration
This article explains Spring Boot’s conditional annotations, such as @ConditionalOnClass and @ConditionalOnBean, detailing their definitions, underlying Condition interfaces, evaluation process, and activation mechanism, with code examples and diagrams illustrating how beans are conditionally loaded at runtime.
Spring Boot provides a set of conditional annotations (Conditional Annotation) that enable dynamic bean registration based on the runtime environment. Common examples include @ConditionalOnBean, @ConditionalOnClass, @ConditionalOnExpression, and @ConditionalOnMissingBean.
@ConditionalOnBean
@ConditionalOnClass
@ConditionalOnExpression
@ConditionalOnMissingBean
The purpose of these annotations is to automatically decide whether a component should be registered. For instance, @ConditionalOnClass checks whether the specified classes are present in the class loader; if they are, the annotated class is eligible for registration, otherwise it is skipped.
Example: the auto‑configuration class FreemarkerAutoConfiguration is defined as follows:
@Configuration
@ConditionalOnClass({ freemarker.template.Configuration.class, FreeMarkerConfigurationFactory.class })
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(FreeMarkerProperties.class)
public class FreeMarkerAutoConfiguration {
// ...
}This class is only loaded when both freemarker.template.Configuration and FreeMarkerConfigurationFactory are present.
Fundamental definitions of conditional annotations
Taking @ConditionalOnClass as an example, its definition is:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}The annotation has two attributes—an array of classes and an array of class names—both serving the same purpose. It is itself meta‑annotated with @Conditional, which references a Condition implementation.
The Condition interface is defined as:
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}Spring Boot provides an abstract class SpringBootCondition that implements Condition and supplies the common matching logic.
Class‑based conditional annotations
Spring Boot offers two class‑based annotations: @ConditionalOnClass (requires the class to be present) and @ConditionalOnMissingClass (requires the class to be absent). Their underlying condition class is OnClassCondition:
@Order(Ordered.HIGHEST_PRECEDENCE)
class OnClassCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
StringBuffer matchMessage = new StringBuffer();
MultiValueMap<String, Object> onClasses = getAttributes(metadata, ConditionalOnClass.class);
if (onClasses != null) {
List<String> missing = getMatchingClasses(onClasses, MatchType.MISSING, context);
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch("required @ConditionalOnClass classes not found: "
+ StringUtils.collectionToCommaDelimitedString(missing));
}
matchMessage.append("@ConditionalOnClass classes found: "
+ StringUtils.collectionToCommaDelimitedString(
getMatchingClasses(onClasses, MatchType.PRESENT, context)));
}
// similar handling for @ConditionalOnMissingClass
// ...
return ConditionOutcome.match(matchMessage.toString());
}
private enum MatchType {
PRESENT {
@Override
public boolean matches(String className, ConditionContext context) {
return ClassUtils.isPresent(className, context.getClassLoader());
}
},
MISSING {
@Override
public boolean matches(String className, ConditionContext context) {
return !ClassUtils.isPresent(className, context.getClassLoader());
}
};
public abstract boolean matches(String className, ConditionContext context);
}
}When @ConditionalOnClass is applied to FreemarkerAutoConfiguration, the condition outcome log looks like:
1 @ConditionalOnClass classes found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean‑based conditional annotations
Annotations such as @ConditionalOnBean, @ConditionalOnMissingBean, and @ConditionalOnSingleCandidate evaluate the presence of specific beans. Their definition is:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
Class<?>[] value() default {};
String[] type() default {};
Class<? extends Annotation>[] annotation() default {};
String[] name() default {};
SearchStrategy search() default SearchStrategy.ALL;
}The matching logic in OnBeanCondition is:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
StringBuffer matchMessage = new StringBuffer();
if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnBean.class);
List<String> matching = getMatchingBeans(context, spec);
if (matching.isEmpty()) {
return ConditionOutcome.noMatch("@ConditionalOnBean " + spec + " found no beans");
}
matchMessage.append("@ConditionalOnBean " + spec + " found the following " + matching);
}
// similar handling for @ConditionalOnSingleCandidate and @ConditionalOnMissingBean
return ConditionOutcome.match(matchMessage.toString());
}Spring Boot also provides other conditional annotations such as @ConditionalOnJava, @ConditionalOnWebApplication, @ConditionalOnProperty, etc.
Summary of various conditional annotations
Activation mechanism of Spring Boot conditional annotations
During the container refresh, Spring Boot uses the internal ConditionEvaluator to parse and evaluate conditional annotations. Classes involved in bean registration, such as AnnotatedBeanDefinitionReader, ConfigurationClassParser, and ClassPathScanningCandidateComponentProvider, invoke the evaluator, and those that do not satisfy the conditions are skipped.
For example, ConfigurationClassParser creates a ConditionEvaluator instance:
public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,
ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader,
BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) {
this.metadataReaderFactory = metadataReaderFactory;
this.problemReporter = problemReporter;
this.environment = environment;
this.resourceLoader = resourceLoader;
this.registry = registry;
this.componentScanParser = new ComponentScanAnnotationParser(
resourceLoader, environment, componentScanBeanNameGenerator, registry);
// construct ConditionEvaluator for conditional processing
this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
}
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}The shouldSkip method checks whether the metadata is annotated with @Conditional and evaluates each condition according to its required phase:
public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase) {
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
if (phase == null) {
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}
List<Condition> conditions = new ArrayList<>();
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
AnnotationAwareOrderComparator.sort(conditions);
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
if (requiredPhase == null || requiredPhase == phase) {
if (!condition.matches(this.context, metadata)) {
return true;
}
}
}
return false;
}The evaluation results are stored in ConditionEvaluationReport, which can be retrieved from the bean factory for debugging:
ConditionEvaluationReport report = beanFactory.getBean("autoConfigurationReport", ConditionEvaluationReport.class);
Map<String, ConditionEvaluationReport.ConditionAndOutcomes> result = report.getConditionAndOutcomesBySource();
for (String key : result.keySet()) {
ConditionEvaluationReport.ConditionAndOutcomes outcomes = result.get(key);
for (ConditionEvaluationReport.ConditionAndOutcome outcome : outcomes) {
System.out.println(key + " -- " + outcome.getCondition().getClass().getSimpleName() + " -- " + outcome.getOutcome());
}
}Sample log output shows which auto‑configuration classes were skipped because required classes were missing:
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: freemarker.template.Configuration,org.springframework.ui.freemarker.FreeMarkerConfigurationFactory
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration -- OnClassCondition -- required @ConditionalOnClass classes not found: groovy.text.markup.MarkupTemplateEngine
... (other entries omitted for brevity)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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
