How Spring Boot Auto-Configuration Works

Introduction

This tutorial explains auto-configuration, the feature that makes Spring Boot feel like magic - add a library to the build, and the beans it needs appear in your application with no configuration code. We will start with what auto-configuration is and the annotation that switches it on. We will then find the auto-configuration classes themselves, in the AutoConfiguration.imports files of Spring Boot's jars, and see how Spring Boot 4 splits them across modules. Next we will look at the conditions that decide which of them apply, print the condition evaluation report for a real application, and watch auto-configuration back off when we define our own bean. Lastly, we will exclude an auto-configuration, and write an auto-configuration of our own.

(This tutorial assumes you know conditional beansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans and @Configuration classesClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration from the Spring Core module - auto-configuration is built from both.)

What is auto-configuration?

In the first Spring Boot applicationClick to read tutorial: Your First Spring Boot Application with Spring Initializr, adding one dependency - the web starter - was enough to get an embedded Tomcat server, Spring MVC and JSON conversion, all configured and running. Nobody wrote a @Bean method for Tomcat. Those beans came from auto-configuration: ordinary @Configuration classes, shipped inside Spring Boot's jars, that Spring Boot loads automatically. Each one declares the beans for one technology, guarded by conditions -
  • Is the technology on the classpath? Tomcat's configuration applies only if Tomcat's classes are present.
  • Has the application already defined this bean? If you declare your own bean of the same type, Boot's version backs off.
  • Do the properties ask for it? Some configurations are switched on or off by properties such as spring.jmx.enabled.
That is the whole idea: sensible defaults that give way to your own configuration.

Where it starts: @SpringBootApplication

The annotation on every Spring Boot main class is a combination of three others -
What @SpringBootApplication is made of (simplified)
@SpringBootConfiguration    // a @Configuration: the class can declare @Bean methods
@EnableAutoConfiguration    // load the auto-configuration classes
@ComponentScan              // scan this class's package and its sub-packages
public @interface SpringBootApplication {
  Class<?>[] exclude() default {};
  // ...
}
@EnableAutoConfiguration imports a special selector class, AutoConfigurationImportSelector, which collects the names of all auto-configuration classes from the classpath and hands them to the container to process like any other configuration - after the application's own configuration, so that conditions such as "has the application defined this bean?" can be answered.

Finding the auto-configuration classes

Every jar that contributes auto-configuration lists its classes in a text file at META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, one class name per line. The program below finds every such file in the jars on the classpath and counts the entries in each -
Listing the AutoConfiguration.imports files on the classpath
//ImportsApp.java
package com.javabrahman.boot.autoconfig.imports;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;

public class ImportsApp {
  static final String IMPORTS = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";

  public static void main(String[] args) throws IOException {
    Enumeration<URL> files = ImportsApp.class.getClassLoader().getResources(IMPORTS);
    int total = 0;
    while (files.hasMoreElements()) {
      URL url = files.nextElement();
      if (!url.getProtocol().equals("jar")) {
        continue;   // list the jars only, not this tutorial's own classes folder
      }
      List<String> classes;
      try (InputStream in = url.openStream()) {
        classes = new String(in.readAllBytes()).lines()
            .map(String::strip).filter(line -> !line.isEmpty() && !line.startsWith("#")).toList();
      }
      String jar = url.toString().replaceAll(".*/([^/!]+\\.jar)!.*", "$1");
      System.out.printf("%-48s %3d%n", jar, classes.size());
      total += classes.size();
    }
    System.out.println("Auto-configuration classes in total: " + total);
  }
}
Run on the classpath of a basic Spring Boot application - the core spring-boot-starter plus the validation starter - it prints -  OUTPUT of the above code
spring-boot-autoconfigure-4.1.2.jar               12
spring-boot-validation-4.1.2.jar                   1
Auto-configuration classes in total: 13
Run on the classpath of the web application from the previous tutorial, with spring-boot-starter-webmvc, the same program prints -  OUTPUT on the web application's classpath
spring-boot-autoconfigure-4.1.2.jar               12
spring-boot-http-converter-4.1.2.jar               1
spring-boot-jackson-4.1.2.jar                      1
spring-boot-servlet-4.1.2.jar                      5
spring-boot-tomcat-4.1.2.jar                       5
spring-boot-webmvc-4.1.2.jar                       6
Auto-configuration classes in total: 30

Explanation of the output

  • Up to Spring Boot 3, almost all auto-configuration lived in one big jar, spring-boot-autoconfigure. Spring Boot 4 split it into modules: spring-boot-autoconfigure now keeps only the general-purpose configurations (AOP, task execution and scheduling, SSL, JMX and so on), and each technology brings its own - spring-boot-validation, spring-boot-tomcat, spring-boot-webmvc, spring-boot-jackson and the rest.
  • So the classpath itself decides what can be auto-configured: the web starter pulled in the web modules, and their auto-configurations came with them.
  • Being listed does not mean being applied - each class is still checked against its conditions, as the next sections show.

The conditions

An auto-configuration class is a @Configuration class annotated with @AutoConfiguration, and its class and @Bean methods carry condition annotations. A simplified version of the auto-configuration for Spring's JMX support shows the typical shape -
The shape of an auto-configuration class (simplified)
@AutoConfiguration
@ConditionalOnClass(MBeanExporter.class)                                   // the library is present
@ConditionalOnBooleanProperty(name = "spring.jmx.enabled")                 // and switched on
public final class JmxAutoConfiguration {

  @Bean
  @ConditionalOnMissingBean(value = MBeanExporter.class, search = SearchStrategy.CURRENT)
  MBeanExporter mbeanExporter(...) {                                       // unless the application has one
    ...
  }
}
The most common conditions are -
  • @ConditionalOnClass / @ConditionalOnMissingClass: a class is or is not on the classpath.
  • @ConditionalOnBean / @ConditionalOnMissingBean: a bean of a type or name exists, or does not.
  • @ConditionalOnProperty and @ConditionalOnBooleanProperty: a property has a value.
  • @ConditionalOnResource: a file such as messages.properties exists.
  • @ConditionalOnWebApplication, @ConditionalOnThreading (platform or virtual threads), and several more.

The condition evaluation report

Spring Boot records every condition it evaluates in a ConditionEvaluationReport. Starting an application with the --debug argument (or debug=true in application.properties) prints the full report at startup, and Spring Boot Actuator exposes it at the /actuator/conditions endpoint. The program below reads the report in code, so that we can print a compact version of it -
Printing the condition evaluation report of a running application
//ReportApp.java
package com.javabrahman.boot.autoconfig.report;

import java.util.Map;
import java.util.TreeMap;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;

@SpringBootApplication
public class ReportApp {
  public static void main(String[] args) {
    try (var context = SpringApplication.run(ReportApp.class, args)) {
      ConditionEvaluationReport report = ConditionEvaluationReport.get(context.getBeanFactory());
      Map<String, ConditionAndOutcomes> results = new TreeMap<>(report.getConditionAndOutcomesBySource());

      System.out.println("MATCHED (applied):");
      results.keySet().removeIf(source -> !source.startsWith("org.springframework"));   // Spring Boot's own only
      results.forEach((source, outcomes) -> {
        if (outcomes.isFullMatch()) {
          System.out.println("  " + shorten(source));
        }
      });
      System.out.println("NOT MATCHED (skipped), with the reason:");
      results.forEach((source, outcomes) -> {
        if (!outcomes.isFullMatch()) {
          System.out.println("  " + shorten(source));
          outcomes.forEach(o -> {
            if (!o.getOutcome().isMatch()) {
              System.out.println("      " + o.getOutcome().getMessage());
            }
          });
        }
      });
    }
  }

  static String shorten(String className) {
    return className.replace("org.springframework.boot.", "").replace("autoconfigure.", "");
  }
}
 OUTPUT of the above code
MATCHED (applied):
  aop.AopAutoConfiguration
  aop.AopAutoConfiguration$ClassProxyingConfiguration
  availability.ApplicationAvailabilityAutoConfiguration#applicationAvailability
  context.LifecycleAutoConfiguration#defaultLifecycleProcessor
  context.PropertyPlaceholderAutoConfiguration#propertySourcesPlaceholderConfigurer
  ssl.SslAutoConfiguration#sslBundleRegistry
  task.TaskExecutionAutoConfiguration
  task.TaskExecutorConfigurations$AsyncConfigurerConfiguration
  task.TaskExecutorConfigurations$SimpleAsyncTaskExecutorBuilderConfiguration#simpleAsyncTaskExecutorBuilder
  task.TaskExecutorConfigurations$TaskExecutorConfiguration
  task.TaskExecutorConfigurations$TaskExecutorConfiguration#applicationTaskExecutor
  task.TaskExecutorConfigurations$ThreadPoolTaskExecutorBuilderConfiguration#threadPoolTaskExecutorBuilder
  task.TaskSchedulingAutoConfiguration
  task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration#simpleAsyncTaskSchedulerBuilder
  task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration#threadPoolTaskSchedulerBuilder
  validation.ValidationAutoConfiguration
  validation.ValidationAutoConfiguration#defaultValidator
  validation.ValidationAutoConfiguration#methodValidationPostProcessor
NOT MATCHED (skipped), with the reason:
  admin.SpringApplicationAdminJmxAutoConfiguration
      @ConditionalOnBooleanProperty (spring.application.admin.enabled=true) did not find property 'spring.application.admin.enabled'
  aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration
      @ConditionalOnClass did not find required class 'org.aspectj.weaver.Advice'
  context.MessageSourceAutoConfiguration
      ResourceBundle did not find bundle with basename messages
  info.ProjectInfoAutoConfiguration#buildProperties
      @ConditionalOnResource did not find resource '${spring.info.build.location:classpath:META-INF/build-info.properties}'
  info.ProjectInfoAutoConfiguration#gitProperties
      GitResource did not find git info at classpath:git.properties
  jmx.JmxAutoConfiguration
      @ConditionalOnBooleanProperty (spring.jmx.enabled=true) did not find property 'spring.jmx.enabled'
  task.TaskExecutorConfigurations$AsyncConfigurerWrapperConfiguration
      @ConditionalOnBean (types: org.springframework.scheduling.annotation.AsyncConfigurer; SearchStrategy: all) did not find any beans of type org.springframework.scheduling.annotation.AsyncConfigurer
  task.TaskExecutorConfigurations$SimpleAsyncTaskExecutorBuilderConfiguration#simpleAsyncTaskExecutorBuilderVirtualThreads
      @ConditionalOnMissingBean (types: org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder; SearchStrategy: all) found beans of type 'org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder' simpleAsyncTaskExecutorBuilder
  task.TaskExecutorConfigurations$TaskExecutorConfiguration#applicationTaskExecutorVirtualThreads
      @ConditionalOnThreading did not find VIRTUAL
  task.TaskExecutorConfigurations$TaskExecutorContextPropagationConfiguration
      @ConditionalOnClass did not find required class 'io.micrometer.context.ContextSnapshot'
  task.TaskSchedulingAutoConfiguration#scheduledBeanLazyInitializationExcludeFilter
      @ConditionalOnBean (names: org.springframework.scheduling.config.internalScheduledAnnotationProcessor; SearchStrategy: all) did not find any beans named org.springframework.scheduling.config.internalScheduledAnnotationProcessor
  task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration#simpleAsyncTaskSchedulerBuilderVirtualThreads
      @ConditionalOnMissingBean (types: org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; SearchStrategy: all) found beans of type 'org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder' simpleAsyncTaskSchedulerBuilder
  task.TaskSchedulingConfigurations$TaskSchedulerConfiguration
      @ConditionalOnBean (names: org.springframework.scheduling.config.internalScheduledAnnotationProcessor; SearchStrategy: all) did not find any beans named org.springframework.scheduling.config.internalScheduledAnnotationProcessor

Explanation of the output

  • Each line names a configuration class, or a @Bean method after a #. The program keeps only Spring Boot's own entries and shortens their names by dropping org.springframework.boot..
  • The matched entries are the defaults this small application received without asking: a task executor and builders for executors and schedulers, a PropertySourcesPlaceholderConfigurer for ${...} placeholders, AOP proxying, an SSL bundle registry, and a Bean Validation validator.
  • The skipped entries show the reasons in plain words: AspectJAutoProxyingConfiguration because AspectJ is not on the classpath; MessageSourceAutoConfiguration because there is no messages.properties; JMX because the spring.jmx.enabled property is not set; the virtual-thread executors because virtual threads are not switched on (spring.threads.virtual.enabled=true would switch them on); the build and git information beans because those files do not exist.
  • When a bean you expect is missing, or an unexpected one appears, this report is the first place to look.

Backing off: defining your own bean

The matched list above includes applicationTaskExecutor, a thread pool that Spring Boot creates for @Async methods and other background work. Its configuration is conditional on the application not having an Executor bean of its own. The program below runs the same check twice - once with Boot's default, once with an executor of our own -
com.javabrahman.boot.extra - a configuration class declaring our own Executor
//OwnExecutorConfig.java
package com.javabrahman.boot.extra;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OwnExecutorConfig {
  @Bean
  Executor reportExecutor() {
    return Executors.newFixedThreadPool(2);
  }
}
Spring Boot's applicationTaskExecutor backs off when the application defines an Executor
//BackOffApp.java
package com.javabrahman.boot.autoconfig.backoff;

import java.util.Arrays;
import java.util.concurrent.Executor;
import com.javabrahman.boot.extra.OwnExecutorConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class BackOffApp {
  public static void main(String[] args) {
    show("Boot's default:", SpringApplication.run(BackOffApp.class));
    // OwnExecutorConfig is outside the scanned package, so it is added as a second configuration source
    show("With our own Executor bean:", SpringApplication.run(new Class<?>[] {BackOffApp.class, OwnExecutorConfig.class}, args));
  }

  static void show(String label, ConfigurableApplicationContext context) {
    try (context) {
      System.out.println(label);
      Arrays.stream(context.getBeanNamesForType(Executor.class))
          .forEach(name -> System.out.println("  Executor bean '" + name + "' of type " + context.getType(name).getSimpleName()));
    }
  }
}
 OUTPUT of the above code
Boot's default:
  Executor bean 'applicationTaskExecutor' of type ThreadPoolTaskExecutor
With our own Executor bean:
  Executor bean 'reportExecutor' of type ThreadPoolExecutor

Explanation of the code

  • In the first run, the only Executor was Boot's applicationTaskExecutor, a ThreadPoolTaskExecutor.
  • In the second run, OwnExecutorConfig was added as an extra configuration source, so the application defined reportExecutor. Boot's @ConditionalOnMissingBean found it and skipped its own executor entirely - the application now has exactly the one executor we declared.
  • OwnExecutorConfig lives outside the package that BackOffApp scans, so the first run does not see it; the second run passes it to SpringApplication.run() as an extra configuration source. In a real application you would simply declare the @Bean in a scanned configuration class.
  • This is the everyday way to customise Spring Boot: declare the bean you want, and the default steps aside. Many defaults can also be tuned through properties without replacing the bean, such as spring.task.execution.pool.max-size.

Excluding an auto-configuration

Occasionally you want an auto-configuration not to run at all, even though its conditions match. There are two ways to exclude it -
Excluding auto-configuration classes
// 1. On the main class
@SpringBootApplication(exclude = TaskSchedulingAutoConfiguration.class)
public class ShopApplication { ... }

// 2. In application.properties (useful per environment, without a code change)
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
Excluded classes appear in the condition evaluation report under Exclusions, so it is always clear that they were switched off on purpose.

Writing your own auto-configuration

Auto-configuration is not reserved for Spring Boot itself - any library can ship its own, and Spring Boot treats it the same way. Writing one also makes the mechanism concrete. Our "library" provides a Greeter bean, configurable by a property, that applications get automatically and can replace -
com.javabrahman.greeting - the library's class
//Greeter.java
package com.javabrahman.greeting;

public class Greeter {
  private final String salutation;

  public Greeter(String salutation) {
    this.salutation = salutation;
  }

  public String greet(String name) {
    return salutation + ", " + name + "!";
  }
}
com.javabrahman.greeting - the library's auto-configuration
//GreetingAutoConfiguration.java
package com.javabrahman.greeting;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
@ConditionalOnBooleanProperty(name = "greeting.enabled", matchIfMissing = true)
public class GreetingAutoConfiguration {

  @Bean
  @ConditionalOnMissingBean
  Greeter greeter(@Value("${greeting.salutation:Hello}") String salutation) {
    return new Greeter(salutation);
  }
}
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - registers the auto-configuration
com.javabrahman.greeting.GreetingAutoConfiguration
An application in another package that uses the library
//GreetingApp.java
package com.javabrahman.boot.autoconfig.custom;

import com.javabrahman.greeting.Greeter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GreetingApp {
  public static void main(String[] args) {
    try (var context = SpringApplication.run(GreetingApp.class, args)) {
      var greeter = context.getBeanProvider(Greeter.class).getIfAvailable();
      System.out.println(String.join(" ", args) + " -> "
          + (greeter == null ? "no Greeter bean" : greeter.greet("Deborah Sprightly")));
    }
  }
}
The application is run three times - with no properties, with a different salutation, and with the feature switched off -  OUTPUT of the above code
(no arguments) -> Hello, Deborah Sprightly!
 OUTPUT of the above code
--greeting.salutation=Namaste -> Namaste, Deborah Sprightly!
 OUTPUT of the above code
--greeting.enabled=false -> no Greeter bean

Explanation of the code

  • GreetingApp is in com.javabrahman.boot.autoconfig.custom, so component scanning never sees the com.javabrahman.greeting package. The Greeter bean came only through the AutoConfiguration.imports file - exactly how Spring Boot's own auto-configurations reach your application.
  • @AutoConfiguration marks the class as an auto-configuration (it is a @Configuration with proxyBeanMethods = false, and it lets you order auto-configurations relative to each other with before and after).
  • The property greeting.salutation changed the greeting with no code change. With greeting.enabled=false, @ConditionalOnBooleanProperty skipped the whole class and there was no Greeter bean; matchIfMissing = true makes the feature enabled by default.
  • @ConditionalOnMissingBean on the @Bean method means an application that declares its own Greeter gets that one instead, just as with the executor earlier.
  • The first run's arguments are the literal words "(no arguments)", which Spring Boot ignores because they are not --name=value options; they only label the output line.
  • In a real library, the auto-configuration would go in its own module, usually with a matching starter that pulls in the library and its dependencies - the subject of the next tutorialClick to read tutorial: Spring Boot Starters Explained with Examples.

Summary

In this tutorial we first saw what auto-configuration is - conditional configuration shipped in Spring Boot's jars - and that @SpringBootApplication switches it on through @EnableAutoConfiguration. We then found the auto-configuration classes in the AutoConfiguration.imports files and saw how Spring Boot 4 spreads them across technology modules, so the classpath decides what can be configured. Next we looked at the condition annotations and printed the condition evaluation report of a real application, with the reason for every skipped configuration. We then watched Boot's task executor back off when we declared our own, saw two ways to exclude an auto-configuration, and lastly wrote an auto-configuration of our own, controlled by properties and replaceable by the application. The next tutorial covers the dependencies that bring auto-configurations onto the classpath: Spring Boot startersClick to read tutorial: Spring Boot Starters Explained with Examples.