How Spring Boot’s Conditional Annotations Dynamically Control Bean Registration

Spring Boot provides a set of conditional annotations such as @ConditionalOnBean, @ConditionalOnClass, and @ConditionalOnMissingBean that evaluate classpath presence, bean existence, and other criteria at runtime, allowing auto‑configuration classes to be registered only when their required conditions are satisfied, with detailed implementation details and activation mechanisms explained.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How Spring Boot’s Conditional Annotations Dynamically Control Bean Registration

Spring Boot includes special conditional annotations that enable dynamic bean registration based on runtime conditions.

@ConditionalOnBean

@ConditionalOnClass

@ConditionalOnExpression

@ConditionalOnMissingBean

The purpose of these annotations is to automatically decide whether a configuration class should be processed by checking the presence of classes, beans, or other criteria. If the condition is not met, the class is skipped.

Example: FreemarkerAutoConfiguration

@Configuration
@ConditionalOnClass({ freemarker.template.Configuration.class,
    FreeMarkerConfigurationFactory.class })
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(FreeMarkerProperties.class)
public class FreeMarkerAutoConfiguration

This auto‑configuration class is guarded by @ConditionalOnClass, which checks whether freemarker.template.Configuration and FreeMarkerConfigurationFactory are present on the classpath before registering the configuration.

Definition of @ConditionalOnClass

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
  Class<?>[] value() default {}; // classes to match
  String[] name() default {}; // class names to match
}

The annotation has two attributes—an array of Class objects and an array of class name strings—both serving the same purpose. It is itself meta‑annotated with @Conditional, linking it to a Condition implementation.

Condition Interface

public interface Condition {
  // ConditionContext stores Spring container, environment, resource loader, class loader
  boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

All conditional annotations rely on implementations of this interface to decide if the condition is satisfied.

ConfigurationCondition Sub‑interface

public interface ConfigurationCondition extends Condition {
  ConfigurationPhase getConfigurationPhase();
  enum ConfigurationPhase {
    PARSE_CONFIGURATION,
    REGISTER_BEAN
  }
}

This sub‑interface adds a phase concept, indicating whether the condition should be evaluated during configuration parsing or bean registration.

SpringBootCondition Abstract Class

@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  String classOrMethodName = getClassOrMethodName(metadata);
  try {
    ConditionOutcome outcome = getMatchOutcome(context, metadata);
    logOutcome(classOrMethodName, outcome);
    recordEvaluation(context, classOrMethodName, outcome);
    return outcome.isMatch();
  } catch (NoClassDefFoundError ex) {
    throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + ex.getMessage() + " not found. ...", ex);
  } catch (RuntimeException ex) {
    throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
  }
}

All Spring Boot condition classes extend this abstract class, which implements the core matching logic.

Class‑Based Conditional Annotation: 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)));
    }
    MultiValueMap<String, Object> onMissingClasses = getAttributes(metadata, ConditionalOnMissingClass.class);
    if (onMissingClasses != null) {
      List<String> present = getMatchingClasses(onMissingClasses, MatchType.PRESENT, context);
      if (!present.isEmpty()) {
        return ConditionOutcome.noMatch("@ConditionalOnMissing classes found: " + StringUtils.collectionToCommaDelimitedString(present));
      }
      matchMessage.append(matchMessage.length() == 0 ? "" : " ");
      matchMessage.append("@ConditionalOnMissing classes not found: " + StringUtils.collectionToCommaDelimitedString(getMatchingClasses(onMissingClasses, MatchType.MISSING, context)));
    }
    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);
  }
}

OnClassCondition evaluates whether the specified classes are present or missing on the class loader, producing a ConditionOutcome that records the match result and a log message.

Bean‑Based Conditional Annotation: @ConditionalOnBean

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
  Class<?>[] value() default {}; // bean types to match
  String[] type() default {}; // bean type class names
  Class<? extends Annotation>[] annotation() default {}; // bean annotations
  String[] name() default {}; // bean names
  SearchStrategy search() default SearchStrategy.ALL; // search strategy
}

The corresponding condition class OnBeanCondition checks the Spring bean factory for matching beans.

@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);
  }
  if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
    BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata, ConditionalOnSingleCandidate.class);
    List<String> matching = getMatchingBeans(context, spec);
    if (matching.isEmpty()) {
      return ConditionOutcome.noMatch("@ConditionalOnSingleCandidate " + spec + " found no beans");
    } else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matching)) {
      return ConditionOutcome.noMatch("@ConditionalOnSingleCandidate " + spec + " found no primary candidate amongst the following " + matching);
    }
    matchMessage.append("@ConditionalOnSingleCandidate " + spec + " found a primary candidate amongst the following " + matching);
  }
  if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
    BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnMissingBean.class);
    List<String> matching = getMatchingBeans(context, spec);
    if (!matching.isEmpty()) {
      return ConditionOutcome.noMatch("@ConditionalOnMissingBean " + spec + " found the following " + matching);
    }
    matchMessage.append(matchMessage.length() == 0 ? "" : " ");
    matchMessage.append("@ConditionalOnMissingBean " + spec + " found no beans");
  }
  return ConditionOutcome.match(matchMessage.toString());
}

Spring Boot also provides other conditional annotations such as @ConditionalOnJava, @ConditionalOnWebApplication, @ConditionalOnResource, @ConditionalOnProperty, and @ConditionalOnExpression.

Activation Mechanism of Conditional Annotations

During the Spring container refresh, the internal ConditionEvaluator parses and evaluates each conditional annotation. Classes involved in bean registration or configuration parsing invoke this evaluator, and those that do not satisfy their conditions are skipped.

public class ConfigurationClassParser {
  private final ConditionEvaluator conditionEvaluator;
  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 handling conditional annotations
    this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
  }
  // ... when parsing each configuration class:
  if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
    return;
  }
}
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 or reporting purposes.

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

Overall, Spring Boot’s conditional annotations provide a powerful mechanism for building modular, environment‑aware auto‑configuration.

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-configurationConditional Annotation
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.