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.

Programmer DD
Programmer DD
Programmer DD
How Spring Boot’s Conditional Annotations Dynamically Control Bean Registration

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.FreeMarkerConfigurationFactory

Bean‑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)
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring Bootauto-configurationSpring FrameworkConditional Annotation
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.