Auto-configuration, starters and conditions that back off
How Spring Boot decides which beans to create for you — candidates from the classpath, conditions on each, and your own beans always winning.
After this lesson you can
- Explain what a starter is and what auto-configuration does with it
- Read @ConditionalOnClass, @ConditionalOnMissingBean and @ConditionalOnProperty
- Find out why an auto-configuration did or did not apply, and switch one off
Add spring-boot-starter-web to a build and, with no configuration at
all, the application starts an embedded Tomcat, serves JSON through
Jackson and routes requests through Spring MVC. Two separate mechanisms
make that happen.
A starter is only a dependency descriptor: a POM that pulls in a
curated, version-aligned set of libraries. spring-boot-starter-web
brings Spring MVC, an embedded Tomcat and Jackson; it contains almost
no code of its own.
Auto-configuration is the code. @SpringBootApplication combines
@SpringBootConfiguration, @ComponentScan and
@EnableAutoConfiguration. The last one loads a list of candidate
configuration classes — in Spring Boot 3, every class named in
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
on the classpath — and evaluates the conditions on each one before
registering any of its beans.
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
public class MyJacksonAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ObjectMapper objectMapper() { return new ObjectMapper(); }
}
The conditions you will meet
@ConditionalOnClass— apply only if a class is on the classpath. This is how adding a dependency switches a feature on.@ConditionalOnMissingBean— create this bean only if the context does not already contain a bean of that type. This is how your own bean replaces Boot's.@ConditionalOnProperty— apply depending on a property's value, for a feature flag such as@ConditionalOnProperty(name = "acme.audit.enabled", havingValue = "true").- Others:
@ConditionalOnBean,@ConditionalOnWebApplication,@ConditionalOnResource.
Why your bean wins
Auto-configuration classes are processed after your own
configuration and scanned components. By the time Boot evaluates
@ConditionalOnMissingBean on its ObjectMapper, yours is already
registered, so Boot's backs off. The model below evaluates three
candidates against a classpath and the user's own beans, the way the
conditions report describes them.
import java.util.*; public class Solution { record Candidate(String bean, String type, String onClass) {} static List<String> evaluate(Set<String> classpath, Set<String> userBeanTypes, List<Candidate> candidates) { Set<String> registeredTypes = new HashSet<>(userBeanTypes); // user config is processed first List<String> report = new ArrayList<>(); for (Candidate c : candidates) { if (!classpath.contains(c.onClass())) { report.add(c.bean() + ": did not match, @ConditionalOnClass did not find " + c.onClass()); } else if (registeredTypes.contains(c.type())) { report.add(c.bean() + ": did not match, @ConditionalOnMissingBean found existing " + c.type()); } else { registeredTypes.add(c.type()); report.add(c.bean() + ": matched, bean created"); } } return report; } public static List<String> run() { Set<String> classpath = Set.of("com.fasterxml.jackson.databind.ObjectMapper", "com.zaxxer.hikari.HikariDataSource"); Set<String> userBeans = Set.of("ObjectMapper"); // the application defined its own List<Candidate> candidates = List.of( new Candidate("objectMapper", "ObjectMapper", "com.fasterxml.jackson.databind.ObjectMapper"), new Candidate("dataSource", "DataSource", "com.zaxxer.hikari.HikariDataSource"), new Candidate("mongoClient", "MongoClient", "com.mongodb.client.MongoClient")); return evaluate(classpath, userBeans, candidates); }}The same rule explains why the order matters between auto-configurations:
a missing-bean check sees only what has been registered so far. Boot
orders its own classes with @AutoConfiguration(after = ...), and its
documentation recommends using the bean conditions only in
auto-configuration classes, where that ordering is controlled.
Seeing and changing the decisions
Start the application with --debug (or debug=true in
application.properties) and Boot logs a conditions evaluation
report: every candidate, which conditions matched, and why the
others did not. With Actuator, /actuator/conditions exposes the same
data. To switch a candidate off entirely use
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) or
the spring.autoconfigure.exclude property.
A custom starter follows the same pattern: an autoconfigure module with
an @AutoConfiguration class listed in the imports file, guarded by
conditions, and a starter POM that depends on it. Third-party starters
are named acme-spring-boot-starter; the spring-boot-starter-* prefix
is reserved for Spring Boot's own.
Common mistakes
- Declaring your own
ObjectMapperbean to change one setting: Boot's backs off completely, and with it go Boot's defaults, thespring.jackson.*properties and every builder customizer. Customise through the properties or aJackson2ObjectMapperBuilderCustomizerbean instead; customizers without an run after the properties have been applied.
Try it yourself
2 visible tests · 4 hidden testsModel where the application's ObjectMapper gets its settings.
Implement
effectiveSettings(Map<String, String> defaults, Map<String, String> properties, List<Map<String, String>> customizers, boolean userDefinedMapper, Map<String, String> userSettings).
defaults: the settings Boot's auto-configured builder starts from.properties: every application property. Only keys that start withspring.jackson.(including the final dot) apply; the setting name is the rest of the key, sospring.jackson.time-zonesetstime-zone.customizers: the settings eachJackson2ObjectMapperBuilderCustomizerbean sets, in the order they run (they run after the properties have been applied).userDefinedMapper: whether the application declared its ownObjectMapperbean;userSettingsare that bean's settings.
If userDefinedMapper is true, Boot's mapper backs off
(@ConditionalOnMissingBean) and the result is exactly
userSettings: the defaults, the spring.jackson.* properties and the
customizers all belong to the bean that was not created. Otherwise
start from defaults, apply the spring.jackson.* properties, then
each customizer in order; a later write to a setting replaces an
earlier one.
Return the settings as a map sorted by setting name (use a TreeMap).
effectiveSettings({"write-dates-as-timestamps":"false"}, {"server.port":"8080","spring.jackson.default-property-inclusion":"non_null"}, [], false, {})effectiveSettings({"write-dates-as-timestamps":"false"}, {"spring.jackson.default-property-inclusion":"non_null"}, [], true, {"fail-on-unknown-properties":"true"})
Sign up to run the tests, check the hidden ones and save your progress. Sign up