Skip to main content
spring internals

The Auto-Configuration Mechanism: @EnableAutoConfiguration, ImportCandidates, and the Condition Evaluation Chain

7 min read Chapter 13 of 78

What @SpringBootApplication Actually Does

@SpringBootApplication is a composed annotation. Strip it apart and you find three annotations doing the real work:

@SpringBootConfiguration   // marks this as a @Configuration class
@EnableAutoConfiguration    // triggers auto-configuration discovery
@ComponentScan              // scans the package tree
public @interface SpringBootApplication { }

The one that matters for this chapter is @EnableAutoConfiguration. It carries an @Import(AutoConfigurationImportSelector.class) annotation. That import is the entire entry point into the auto-configuration mechanism. Every auto-configured DataSource, every auto-configured ObjectMapper, every auto-configured WebMvcConfigurer traces back to this single import.

Auto-configuration two-phase condition evaluation showing PARSE_CONFIGURATION and REGISTER_BEAN phases

The Discovery Pipeline

When ConfigurationClassPostProcessor processes the @Import on @EnableAutoConfiguration, it delegates to AutoConfigurationImportSelector. This class implements DeferredImportSelector, which means it runs after all user-defined @Configuration classes have been parsed. That ordering is deliberate: auto-configuration must yield to your explicit configuration.

AutoConfigurationImportSelector.selectImports() triggers a pipeline:

  1. Load candidates. ImportCandidates.load(AutoConfiguration.class, classLoader) reads every file at META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports on the classpath. Each file contains fully qualified class names, one per line.

  2. Remove duplicates. Multiple JARs can declare the same auto-configuration class. Duplicates are collapsed.

  3. Apply exclusions. Any class listed in @SpringBootApplication(exclude = ...) or in the spring.autoconfigure.exclude property is removed from the candidate set.

  4. Filter by conditions. The remaining candidates pass through AutoConfigurationImportFilter instances. The primary filter is OnBeanCondition, OnClassCondition, and OnWebApplicationCondition. These filters discard candidates early, before their class bytes are loaded.

  5. Sort. The surviving candidates are ordered according to @AutoConfigureBefore, @AutoConfigureAfter, and @AutoConfigureOrder.

The result is an ordered list of configuration classes that Spring will process as if you had declared @Import on each one.

The Imports File

Before Spring Boot 3.0, auto-configuration classes were registered in META-INF/spring.factories under the EnableAutoConfiguration key. That file was shared with other Spring extension points, which created confusion and startup cost. Spring Boot 3.0 moved to a dedicated file:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

The format is plain text. One class per line. Comments start with #:

# DataSource auto-configuration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

ImportCandidates reads these files using ClassLoader.getResources(), which means every JAR on the classpath contributes its own candidates. A typical Spring Boot web application starts with 150+ candidate classes.

AutoConfigurationImportSelector Internals

The selector does not implement ImportSelector.selectImports() directly. It implements DeferredImportSelector.Group, which batches imports for efficiency. The key class is AutoConfigurationImportSelector.AutoConfigurationGroup:

// Inside AutoConfigurationImportSelector
private static class AutoConfigurationGroup implements DeferredImportSelector.Group {

    @Override
    public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
        // 1. Load all candidates from imports files
        // 2. Apply exclusions
        // 3. Filter by conditions
        // 4. Sort
        this.autoConfigurationEntries.add(entry);
    }

    @Override
    public Iterable<Entry> selectImports() {
        // Return the sorted, filtered entries
        return sortedEntries;
    }
}

The process() method runs once per @EnableAutoConfiguration annotation (normally just one). The selectImports() method returns the final ordered set. Because this is a DeferredImportSelector, it executes after all regular @Import processing. Your explicit @Bean definitions exist before auto-configuration runs.

The Condition Evaluation Chain

The filtering step is where most auto-configuration classes get discarded. A typical Spring Boot application loads 20-30 auto-configuration classes out of 150+ candidates. The rest fail their conditions.

Spring Boot defines several @Conditional annotations. Each delegates to a Condition implementation:

AnnotationCondition ClassChecks
@ConditionalOnClassOnClassConditionClass present on classpath (ASM check, no loading)
@ConditionalOnMissingClassOnClassConditionClass absent from classpath
@ConditionalOnBeanOnBeanConditionBean of type/name exists in context
@ConditionalOnMissingBeanOnBeanConditionNo bean of type/name in context
@ConditionalOnPropertyOnPropertyConditionProperty has specific value
@ConditionalOnWebApplicationOnWebApplicationConditionServlet or reactive web context

These conditions evaluate in two phases:

Phase 1 (PARSE_CONFIGURATION): @ConditionalOnClass and @ConditionalOnWebApplication run before any bean definitions from the configuration class are registered. They act as a gate. If the required class is not on the classpath, the entire configuration class is skipped.

Phase 2 (REGISTER_BEAN): @ConditionalOnBean and @ConditionalOnMissingBean run when individual @Bean methods are being registered. At this point, bean definitions from previously processed configuration classes exist in the registry.

This two-phase model is why @ConditionalOnClass can appear at the class level but @ConditionalOnBean should appear at the method level. Phase 1 conditions gate entire classes. Phase 2 conditions gate individual beans.

Ordering: @AutoConfigureBefore, @AutoConfigureAfter

Auto-configuration ordering matters because @ConditionalOnBean depends on which bean definitions have already been registered. If JpaAutoConfiguration runs before DataSourceAutoConfiguration, it will not find the DataSource bean and will skip itself.

Spring Boot provides three ordering mechanisms:

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class JpaAutoConfiguration {
    // DataSource bean definition is guaranteed to exist when this is evaluated
}
@AutoConfigureBefore(JpaAutoConfiguration.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class CustomDataSourceAutoConfiguration { }
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class EarlyAutoConfiguration { }

AutoConfigurationSorter processes these annotations and produces a topological sort of the candidate list. Cycles cause an IllegalStateException at startup.

The ConditionEvaluationReport

Every condition evaluation is recorded. The ConditionEvaluationReport stores the result of every @Conditional check for every auto-configuration class. You can access it two ways:

1. The —debug flag:

java -jar app.jar --debug

This activates ConditionEvaluationReportLogger, which prints the full report at startup:

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required class 'javax.sql.DataSource' (OnClassCondition)
      - @ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition)

Negative matches:
-----------------
   MongoAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'com.mongodb.client.MongoClient' (OnClassCondition)

2. The /actuator/conditions endpoint:

GET /actuator/conditions

{
  "contexts": {
    "application": {
      "positiveMatches": {
        "DataSourceAutoConfiguration": [{
          "condition": "OnClassCondition",
          "message": "@ConditionalOnClass found required class 'javax.sql.DataSource'"
        }]
      },
      "negativeMatches": {
        "MongoAutoConfiguration": {
          "notMatched": [{
            "condition": "OnClassCondition",
            "message": "did not find required class 'com.mongodb.client.MongoClient'"
          }]
        }
      }
    }
  }
}

This report is the primary debugging tool for auto-configuration problems. When a bean you expect is missing, the condition evaluation report tells you exactly which condition failed and why.

The Failure Mode: Conflicting Configuration

Consider a multi-tenant SaaS backend where you define a custom DataSource:

// BROKEN: Defining a DataSource without guarding against auto-configuration
@Configuration
public class TenantDataSourceConfig {

    @Bean
    public DataSource dataSource(TenantRegistry registry) {
        return new TenantRoutingDataSource(registry);
    }
}

This appears to work, but it creates a conflict. DataSourceAutoConfiguration has a @ConditionalOnMissingBean(DataSource.class) check, so it backs off. However, DataSourceAutoConfiguration also registers DataSourceProperties, and the auto-configured DataSourceInitializer still runs. It tries to execute schema.sql against a data source that may not have a connection yet because the tenant context is not established.

The failure is subtle. In tests, tenant context is usually set up first, so this works. In production, the DataSourceInitializer fires during application startup before any tenant context exists, causing a NullPointerException deep inside the routing logic.

The Correct Pattern

// CORRECT: Exclude the auto-configuration that conflicts, declare what you need explicitly
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SaasApplication { }

@Configuration
public class TenantDataSourceConfig {

    @Bean
    @ConditionalOnMissingBean(DataSource.class)
    public DataSource dataSource(TenantRegistry registry) {
        return new TenantRoutingDataSource(registry);
    }

    @Bean
    public DataSourceProperties dataSourceProperties() {
        // Explicit control over which properties bind
        return new DataSourceProperties();
    }
}

The exclude removes the entire auto-configuration class, including its side effects. The @ConditionalOnMissingBean on your own bean allows test configurations to substitute a simpler data source. The explicit DataSourceProperties bean gives you control over property binding without the auto-configured initializer.

Verify the exclusion took effect:

java -jar app.jar --debug | grep DataSourceAutoConfiguration

You should see it in the “Exclusions” section, not in “Negative matches.” If it appears in “Negative matches,” the exclusion attribute has a typo or references the wrong class. That distinction matters: exclusions are removed before condition evaluation. Negative matches went through condition evaluation and failed. Both result in the class not being loaded, but only exclusions guarantee that no side effects from the class were processed.