DEV Community

Silver_dev
Silver_dev

Posted on

Spring Boot Under the Hood. Part 5: The Heart of Spring Boot — How Auto-configuration Works

Auto-configuration is the heart of Spring Boot. It's what lets you add spring-boot-starter-web to your pom.xml and instantly get Tomcat, DispatcherServlet, Jackson, and a bunch of other beans — without a single line of configuration.

Let's break down how it works, down to the last detail.

5.1. Where the Entry Point Lives

You already know that @SpringBootApplication includes @EnableAutoConfiguration. Let's look at it:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}
Enter fullscreen mode Exit fullscreen mode

Two key points:

  • @AutoConfigurationPackage — remembers the main class's package (we covered this in Part 4).
  • @Import(AutoConfigurationImportSelector.class) — imports the selector that does all the magic.

@Import is a Spring Framework mechanism that lets you add BeanDefinitions to the context programmatically, bypassing scanning. A selector implements ImportSelector and returns an array of class names to register.

5.2. AutoConfigurationImportSelector — the Master Conductor

public class AutoConfigurationImportSelector 
        implements DeferredImportSelector, BeanClassLoaderAware, 
                   ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
Enter fullscreen mode Exit fullscreen mode

Note: this is a DeferredImportSelector, not a plain ImportSelector. The difference is fundamental.

5.2.1. DeferredImportSelector vs ImportSelector

Type When it's processed
ImportSelector Immediately, while parsing @Configuration
DeferredImportSelector After all regular @Configuration classes have been processed

This guarantees that auto-configurations are processed after user-defined beans. That's exactly why @ConditionalOnMissingBean in auto-configurations works correctly — by the time they're processed, all of your @Bean methods are already registered.

5.2.2. selectImports() — the entry point

@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    AutoConfigurationEntry autoConfigurationEntry = 
        getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
Enter fullscreen mode Exit fullscreen mode

The getAutoConfigurationEntry() method is the central one. Let's break it down.

5.3. getAutoConfigurationEntry() — a Step-by-Step Breakdown

protected AutoConfigurationEntry getAutoConfigurationEntry(
        AnnotationMetadata annotationMetadata) {

    // 1. Check: is auto-configuration enabled at all?
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }

    // 2. Get the @EnableAutoConfiguration attributes (exclude, excludeName)
    AnnotationAttributes attributes = getAttributes(annotationMetadata);

    // 3. Load ALL candidates from the classpath
    List<String> configurations = getCandidateConfigurations(
        annotationMetadata, attributes);

    // 4. Deduplicate
    configurations = removeDuplicates(configurations);

    // 5. Apply exclusions
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);

    // 6. Filter by conditions (ConfigurationClassFilter)
    configurations = getConfigurationClassFilter().filter(configurations);

    // 7. Publish the AutoConfigurationImportEvent
    fireAutoConfigurationImportEvents(configurations, exclusions);

    return new AutoConfigurationEntry(configurations, exclusions);
}
Enter fullscreen mode Exit fullscreen mode

The key steps are 3 and 6.

5.4. Loading the Candidates — .imports Instead of spring.factories

5.4.1. A Bit of History

Version Mechanism
Spring Boot 1.x – 2.6 META-INF/spring.factories (the EnableAutoConfiguration key)
Spring Boot 2.7 Both mechanisms in parallel (backward compatibility)
Spring Boot 3.0+ .imports only; spring.factories registration for auto-configurations removed

This is confirmed by the official documentation and release notes: "Spring Boot 2.7 introduced a new META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file for registering auto-configurations, while maintaining backwards compatibility with registration in spring.factories. With this release, support for registering auto-configurations in spring.factories has been removed in favor of the imports file."

5.4.2. What the .imports File Looks Like

The file path:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Enter fullscreen mode Exit fullscreen mode

The format — one line = one class:

org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
...
Enter fullscreen mode Exit fullscreen mode

For comparison, the old spring.factories used a key-value format:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
Enter fullscreen mode Exit fullscreen mode

The advantages of .imports: simpler parsing, no escaping issues, better AOT compatibility.

5.4.3. ImportCandidates — the loader

Candidates are loaded via ImportCandidates.load():

public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
    String location = String.format(
        "META-INF/spring/%s.imports", annotation.getName());
    List<String> candidates = new ArrayList<>();
    // ... reads all files at this location from all JARs
    return new ImportCandidates(candidates);
}
Enter fullscreen mode Exit fullscreen mode

Key point: ImportCandidates does not use SpringFactoriesLoader (which reads spring.factories). It's a separate mechanism optimized for AOT and native images.

5.4.4. @AutoConfiguration — the new annotation

Since Spring Boot 2.7, auto-configuration classes are marked with @AutoConfiguration instead of @Configuration:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore
@AutoConfigureAfter
public @interface AutoConfiguration {
    @AliasFor(annotation = AutoConfigureBefore.class, attribute = "value")
    Class<?>[] before() default {};

    @AliasFor(annotation = AutoConfigureAfter.class, attribute = "value")
    Class<?>[] after() default {};
}
Enter fullscreen mode Exit fullscreen mode

Three key differences from @Configuration:

  1. proxyBeanMethods = false — auto-configurations are not proxied with CGLIB. It's faster and avoids circular dependencies.
  2. It carries @AutoConfigureBefore / @AutoConfigureAfter — the ordering can be specified right in the annotation.
  3. It's semantically separated from user-defined @Configuration classes.

5.5. Conditional Annotations (@Conditional*) — Filtering the Candidates

After all the candidates are loaded (there are 150+ of them in Spring Boot 3.x), filtering begins. This is where the conditional annotations come into play.

5.5.1. How It's Wired

The filtering happens in ConfigurationClassFilter:

class ConfigurationClassFilter {
    private final List<AutoConfigurationImportFilter> filters;

    List<String> filter(List<String> configurations) {
        String[] candidates = configurations.toArray(new String[0]);
        boolean[] skip = new boolean[candidates.length];
        boolean skipped = false;
        for (AutoConfigurationImportFilter filter : this.filters) {
            boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
            for (int i = 0; i < match.length; i++) {
                if (!match[i]) {
                    skip[i] = true;
                    skipped = true;
                }
            }
        }
        // ... returns the filtered list
    }
}
Enter fullscreen mode Exit fullscreen mode

AutoConfigurationImportFilter is an SPI interface whose implementations are loaded via spring.factories (yes, spring.factories is still used for internal SPIs — just not for auto-configuration registration). Spring Boot ships three filters:

Filter What it checks
OnClassCondition @ConditionalOnClass / @ConditionalOnMissingClass
OnBeanCondition @ConditionalOnBean / @ConditionalOnMissingBean
OnWebApplicationCondition @ConditionalOnWebApplication / @ConditionalOnNotWebApplication

Important: this is the first stage of filtering — a coarse-grained pass. It only checks for the presence of classes and beans, without loading the auto-configurations themselves. Finer-grained checks (properties, resources, expressions) happen later, when ConfigurationClassParser processes each specific class.

5.5.2. The Main Conditional Annotations

@ConditionalOnClass / @ConditionalOnMissingClass

@AutoConfiguration
@ConditionalOnClass(DataSource.class)   // activates only if DataSource is on the classpath
public class DataSourceAutoConfiguration { }
Enter fullscreen mode Exit fullscreen mode

The check happens via ASM — the class is not loaded into the JVM; only the .class file's metadata is read. That's why it's safe to reference classes that may not be present at runtime.

@ConditionalOnBean / @ConditionalOnMissingBean

@Bean
@ConditionalOnMissingBean
public DataSource dataSource() {
    return new HikariDataSource();
}
Enter fullscreen mode Exit fullscreen mode

The key customization mechanism. If you've declared your own DataSource, the auto-configuration backs off and doesn't create its own. This is exactly why auto-configurations are processed as a DeferredImportSelector — so that all of your beans are already registered by the time the checks run.

Caveat: @ConditionalOnMissingBean only works reliably for @Bean methods and @Configuration classes that are processed after yours. If you put it on a regular @Configuration, the result can be unpredictable — @conditionals are evaluated while parsing the @Configuration, and the parsing order may differ from the bean registration order.

@ConditionalOnProperty

@AutoConfiguration
@ConditionalOnProperty(
    prefix = "spring.datasource", 
    name = "url", 
    matchIfMissing = false
)
public class DataSourceAutoConfiguration { }
Enter fullscreen mode Exit fullscreen mode

Checks the value of a property. matchIfMissing = true means "activate if the property is missing entirely."

@ConditionalOnResource

@ConditionalOnResource(resources = "classpath:my-config.properties")
Enter fullscreen mode Exit fullscreen mode

@ConditionalOnWebApplication / @ConditionalOnNotWebApplication

@ConditionalOnWebApplication(type = Type.SERVLET)
Enter fullscreen mode Exit fullscreen mode

@ConditionalOnExpression

@ConditionalOnExpression("${my.feature.enabled:false} and '${my.mode}' == 'advanced'")
Enter fullscreen mode Exit fullscreen mode

Uses SpEL. Not recommended for auto-configurations — it's slow and plays poorly with AOT.

5.5.3. ConfigurationPhase — a Subtlety

A @Conditional can be evaluated on two phases:

public enum ConfigurationPhase {
    PARSE_CONFIGURATION,  // while parsing @Configuration
    REGISTER_BEAN         // while registering beans
}
Enter fullscreen mode Exit fullscreen mode

@ConditionalOnBean and @ConditionalOnMissingBean use REGISTER_BEAN, because they need to know which beans are already registered. The rest (@ConditionalOnClass, @ConditionalOnProperty) use PARSE_CONFIGURATION.

5.6. Auto-configuration Ordering

When you have 150+ auto-configurations, the order in which they're applied is critical. For example, DataSourceAutoConfiguration must come before JpaRepositoriesAutoConfiguration.

5.6.1. @AutoConfigureBefore / @AutoConfigureAfter

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class JpaRepositoriesAutoConfiguration { }
Enter fullscreen mode Exit fullscreen mode

Or via the standalone annotations:

@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class JpaRepositoriesAutoConfiguration { }
Enter fullscreen mode Exit fullscreen mode

These annotations don't guarantee an absolute order — they only define a relative one. Spring Boot sorts the dependency graph via AutoConfigurationSorter.

5.6.2. @AutoConfigureOrder

@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class MyEarlyAutoConfiguration { }
Enter fullscreen mode Exit fullscreen mode

The semantics are the same as @Order, but for auto-configurations. It's used when auto-configurations know nothing about each other and can't reference one another via before/after.

5.6.3. AutoConfigurationSorter — how the sorting works

class AutoConfigurationSorter {
    void sort(List<String> classNames) {
        // 1. Reads each class's metadata (ASM)
        // 2. Builds the graph: A before B, A after C
        // 3. Topological sort
        // 4. Resolves conflicts via @AutoConfigureOrder
    }
}
Enter fullscreen mode Exit fullscreen mode

Priority: @AutoConfigureBefore / @AutoConfigureAfter > @AutoConfigureOrder.

5.7. Creating Your Own Auto-configuration

5.7.1. The auto-configuration class

@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService(MyServiceProperties properties) {
        return new MyService(properties.getEndpoint(), properties.getTimeout());
    }
}
Enter fullscreen mode Exit fullscreen mode

5.7.2. Properties

@ConfigurationProperties(prefix = "my.service")
public class MyServiceProperties {
    private String endpoint = "http://localhost:8080";
    private Duration timeout = Duration.ofSeconds(5);
    // getters/setters
}
Enter fullscreen mode Exit fullscreen mode

5.7.3. Registration in .imports

The file:

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Enter fullscreen mode Exit fullscreen mode

The content:

com.example.autoconfigure.MyServiceAutoConfiguration
Enter fullscreen mode Exit fullscreen mode

5.7.4. The starter

So that users can add a single dependency, you ship a starter — an empty JAR that pulls in:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>my-service-spring-boot-starter</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Inside the starter's pom.xml:

<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-service</artifactId>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-service-spring-boot-autoconfigure</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

Naming convention: xxx-spring-boot-starter (for users) and xxx-spring-boot-autoconfigure (for the auto-configuration code).

5.8. Spring Boot 3 vs Spring Boot 4: Key Differences

5.8.1. Modularization of spring-boot-autoconfigure

This is the biggest change in Boot 4.

Aspect Spring Boot 3.x Spring Boot 4.x
Artifact A single monolithic spring-boot-autoconfigure (2 MiB in 3.5) 47 modular JARs, one per technology
Size Everything in one place Only the modules you need end up on the classpath
Web auto-configurations In spring-boot-autoconfigure In spring-boot-webmvc / spring-boot-webflux
JPA auto-configurations In spring-boot-autoconfigure In spring-boot-data-jpa
IDE hints All classes of all technologies Only the ones actually in use

The official announcement explains the motivation: "Instead of a single, monolithic spring-boot-autoconfigure jar, we are now splitting functionality into small and more focused modules. This change is motivated by maintainability, clarity, and a leaner runtime footprint."

5.8.2. What Hasn't Changed

  • @EnableAutoConfiguration — works the same way.
  • AutoConfigurationImportSelector — the same mechanism.
  • .imports files — the same format.
  • @Conditional* — the same annotations.
  • @AutoConfigureBefore / @AutoConfigureAfter — the same semantics.

5.8.3. BeanRegistrar — the new alternative

In Spring Framework 7 / Boot 4, BeanRegistrar arrived — programmatic bean registration without @Bean methods:

public class MyBeanRegistrar implements BeanRegistrar {
    @Override
    public void register(BeanRegistry registry, Environment env) {
        if (env.acceptsProfiles(Profiles.of("dev"))) {
            registry.registerBean("myService", MyService.class, 
                spec -> spec.supplier(ctx -> new MyService("dev")));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

It's registered via @Import(MyBeanRegistrar.class). This is closer to the AOT approach: beans are registered programmatically rather than via reflection.

5.8.4. Starters

Boot 3 Boot 4
spring-boot-starter-web spring-boot-starter-webmvc
spring-boot-starter-webflux spring-boot-starter-webflux (unchanged)
spring-boot-starter-data-jpa spring-boot-starter-data-jpa (unchanged)

5.9. Diagram: the Full Path of Auto-configuration

@SpringBootApplication
  └─ @EnableAutoConfiguration
        └─ @Import(AutoConfigurationImportSelector.class)
              │
              └─ refresh() → invokeBeanFactoryPostProcessors()
                    └─ ConfigurationClassPostProcessor
                          └─ ConfigurationClassParser
                                └─ DeferredImportSelectorGroupingHandler
                                      └─ AutoConfigurationImportSelector.selectImports()
                                            │
                                            ├─ 1. isEnabled()?
                                            ├─ 2. getAttributes() → exclude/excludeName
                                            ├─ 3. getCandidateConfigurations()
                                            │     └─ ImportCandidates.load()
                                            │           └─ META-INF/spring/...AutoConfiguration.imports
                                            │                 (all JARs)
                                            ├─ 4. removeDuplicates()
                                            ├─ 5. getExclusions() → removeAll()
                                            ├─ 6. ConfigurationClassFilter.filter()
                                            │     ├─ OnClassCondition
                                            │     ├─ OnBeanCondition
                                            │     └─ OnWebApplicationCondition
                                            └─ 7. fireAutoConfigurationImportEvents()
                                                  │
                                                  ▼
                                            A list of class names
                                                  │
                                                  ▼
                                            ConfigurationClassParser processes
                                            each class as a @Configuration
                                                  │
                                                  ▼
                                            @Bean methods → BeanDefinitions
                                            @Conditional* → final filtering
Enter fullscreen mode Exit fullscreen mode

5.10. Key Takeaways

  1. @EnableAutoConfigurationAutoConfigurationImportSelector is the single entry point. The selector is a DeferredImportSelector, so auto-configurations are processed after user-defined beans.
  2. .imports files (Boot 3+) replaced spring.factories for auto-configurations. The path: META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
  3. @AutoConfiguration is the new annotation for auto-configuration classes: proxyBeanMethods = false, carries @AutoConfigureBefore/After.
  4. @Conditional* annotations work in two passes: coarse-grained filtering via AutoConfigurationImportFilter (by classes/beans), then fine-grained via ConfigurationClassParser.
  5. @ConditionalOnMissingBean is the "back-off" mechanism: if you've declared your own bean, the auto-configuration backs off.
  6. Ordering is defined by @AutoConfigureBefore / @AutoConfigureAfter (relative) and @AutoConfigureOrder (absolute). Sorting is done by AutoConfigurationSorter.
  7. Boot 4 — modularization: 47 modules instead of a single spring-boot-autoconfigure. The auto-configuration mechanism itself hasn't changed, only the packaging.
  8. Your own auto-configuration = @AutoConfiguration + @ConditionalOnClass + @ConditionalOnMissingBean + an .imports file + a starter.

Top comments (0)