Auto-Configuration Internals: From spring.factories to AutoConfiguration.imports

bee2026-10-0850 min read0 views
Why does adding a starter hand you a DataSource and a transaction manager? Unpack the loading pipeline of AutoConfigurationImportSelector, the ordering of @AutoConfiguration and the condition toolbox.
1 / 141
Section
0. The 30-second version
2 / 141

The last two articles taught you to run something and to pry open the three-in-one annotation. This one answers the question that stalls beginners most: why does one added dependency hand me a DataSource, a transaction manager and a JSON converter? There is no magic here — just a plain assembly line: jars ship with a candidate list, Boot reads it at startup, asks each entry a few questions ("did you configure this already? do you have that class?"), and registers only the ones that pass. The article splits that line into four stages, then teaches one more skill: when something does not take effect, how to make Boot tell you why.

3 / 141
类比|Analogy

a fully finished apartment comes pre-fitted with plumbing, wiring and furniture — but the moment you place your own sofa in the living room, the fit-out team will not put theirs there. Auto-configuration follows exactly that etiquette. And @ConditionalOnMissingBean is the umbrella rule at the hotel front desk: the shop lends an umbrella only because the guest brought none; show up with your own and the assistant puts the rack away before you even ask. That is why Section 2 keeps stressing "deferred import": Boot waits until you have finished speaking before deciding whether to configure anything for you.

4 / 141
Diagram
Figure · Two roads into the container: scanning vs a registration list
Figure · Two roads into the container: scanning vs a registration list
5 / 141

After this article you should be able to answer three questions:

6 / 141
  • Which file does the candidate list come from, and at which version did spring.factories become AutoConfiguration.imports?
  • What must an auto-configuration class satisfy before it really takes effect, and where do I read the reason when it is skipped?
  • What is the correct way to replace Boot's ObjectMapper, and why is the overriding switch the wrong tool?
7 / 141
Section
1. The phenomenon: one starter, and everything is there
8 / 141

You create a Spring Boot project and add a single dependency to pom.xml:

9 / 141
xml
<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
10 / 141

In application.yml you write only three or four lines of database connection info. You configure no DataSource, no EntityManagerFactory, no PlatformTransactionManager. Yet after startup all of these beans are sitting happily in the container — @Autowired a DataSource and it just works.

11 / 141

There is a question worth asking here: the Spring IoC container only builds what you tell it to build; it does not conjure a connection pool out of thin air. So who registered these beans?

12 / 141

The answer is auto-configuration. It is not magic: it is a set of configuration classes written into the jar ahead of time, which only take effect when their conditions are met. This article takes the whole pipeline apart — who reads which file, who evaluates the conditions, and who finally puts the beans into the container.

13 / 141

"One line of dependency, a roomful of beans" is best ticked through yourself. This generator follows the same rules as the lesson text: tick Data JPA alone and watch it drag in spring-orm, hibernate-core and spring-boot-starter-jdbc by itself; then stack the MySQL driver and H2 on top and notice what two drivers on one classpath produces — that is exactly the scene in the Section 13 sandbox and the trace at the end of Section 14. Finally add RabbitMQ, then untick it again: the second experiment below shows you which gate removed it.

14 / 141
Generator
GeneratorWhy one dependency buys you a roomful of beanspom.xml1 / 10
Every tick adds a batch of classes whose conditions get evaluated. Tick and untick once, then check Section 13: who is in Positive matches and who in Negative matches
Output
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Why each choice matters
parentInheriting 3.3.4 starter-parent means no spring-boot-starter-* needs a version; the moment someone adds an explicit version to one starter, that one wins — the most common source of dependency drift.
Data JPABrings spring-orm plus Hibernate: write a Repository interface and an implementation appears.
15 / 141
Section
2. The full loading pipeline: from switch to registration
16 / 141
Diagram
Figure 1 · The auto-configuration pipeline
Figure 1 · The auto-configuration pipeline
17 / 141

Everything starts from that one @Import on @EnableAutoConfiguration from the previous article:

18 / 141
java
@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration { }
19 / 141

The class that does the work is AutoConfigurationImportSelector. Its skeleton looks roughly like this:

20 / 141
Code
Codejava
public class AutoConfigurationImportSelector        implements DeferredImportSelector, BeanClassLoaderAware,                   ResourceLoaderAware, BeanFactoryAware, EnvironmentAware,                   Ordered {    @Override    public String[] selectImports(AnnotationMetadata annotationMetadata) {        if (!isEnabled(annotationMetadata)) {            return NO_IMPORTS;          // returns empty when spring.boot.enableautoconfiguration=false        }        AutoConfigurationEntry entry = getAutoConfigurationEntry(annotationMetadata);        return StringUtils.toStringArray(entry.getConfigurations());    }    @Override    public Class<? extends Group> getImportGroup() {        return AutoConfigurationGroup.class;   // group = deferred import, ordered after user config    }}
Notes
  • DeferredImportSelector: note the word Deferred. A plain ImportSelector is processed immediately, whereas this one is postponed until after all of the user's own @Configuration classes have been processed. That detail is critical, and Section 7 shows what it buys us
  • selectImports: the entry point. It first checks the master switch, then asks getAutoConfigurationEntry for the candidate list
  • getAutoConfigurationEntry: internally it does "gather candidates → deduplicate → filter exclusions → trim by conditions" and finally returns the array of configuration classes to import
21 / 141

Expanding getAutoConfigurationEntry, a full load is this sequence:

22 / 141
  1. getCandidateConfigurations() obtains all candidate auto-configuration classes (the list file inside the jars)
  2. Duplicates are removed (several jars may declare the same class) and anything the user excluded via exclude / excludeName / spring.autoconfigure.exclude is dropped
  3. AutoConfigurationImportFilter performs a fast preliminary filter (for example OnClassCondition reads the classpath directly and can eliminate a large batch of irrelevant classes, saving parsing cost)
  4. The survivors go to ConfigurationClassParser one by one
  5. During parsing, the @Conditional conditions on the class and its methods are evaluated by ConditionEvaluator
  6. Passers are registered as BeanDefinitions; failures are recorded in the ConditionEvaluationReport
  7. The container runs refresh(), instantiating real beans from those BeanDefinitions
23 / 141
Tip

notice there are two filters. The first is AutoConfigurationImportFilter, which does not parse classes and only inspects the classpath — it is a performance optimization that "cuts fast". The second is condition evaluation, which actually reads annotations and checks whether beans exist. Understanding this division explains why a project without RabbitMQ on its classpath pays no parsing cost for Rabbit's configuration classes at startup.

24 / 141

The two gates are not built the same and cannot do the same things — here they are side by side:

25 / 141
Diagram
Figure · The two gates on the chain: cheap sieve and deep sieve
Figure · The two gates on the chain: cheap sieve and deep sieve
26 / 141

Look at the last row of the right column: only the second gate leaves a "why" behind. That is why "why did it not apply" can only ever be answered from the --debug report and never from the candidate-list stage — classes removed by gate one do not even appear by name. Beginners read this as "the report is missing my class"; Section 13 meets the same trap again.

27 / 141
Section
3. Old and new mechanisms: spring.factories vs AutoConfiguration.imports
28 / 141

Where does the "candidate list" come from? There is a version watershed here. Before Spring Boot 2.7 it was spring.factories; 2.7 introduced the new style, and 3.0 removed the old mechanism entirely.

29 / 141
Table
Aspectspring.factories (≤ 2.7)AutoConfiguration.imports (2.7+)
File pathMETA-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Formatproperties: one key mapping to a list of class namesplain text: one class name per line
Lookupvia the key org.springframework.boot.autoconfigure.EnableAutoConfigurationthe file name itself is the key
Loading APISpringFactoriesLoader.loadFactoryNamesImportCandidates.load(AutoConfiguration.class, ...)
Statusdeprecated in 2.7, gone in 3.0the current standard
30 / 141

The old style looked like this — a comma-separated class list continued with backslashes:

31 / 141
properties
# META-INF/spring.factories (old)org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.example.demo.autoconfigure.MyServiceAutoConfiguration,\com.example.demo.autoconfigure.OtherAutoConfiguration
32 / 141

The new style is cleaner, one class per line, with no key to declare:

33 / 141
Code
Codetext
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (new)com.example.demo.autoconfigure.MyServiceAutoConfigurationcom.example.demo.autoconfigure.OtherAutoConfiguration
Notes

Trap: when upgrading from 2.7 to 3.0, if a custom starter still registers its auto-configuration via spring.factories, those entries silently stop working — no error, the classes simply are never loaded again. Search your upgrade checklist for spring.factories. As an aside, spring.factories was not fully deleted in 3.0; it still carries other extension points such as listeners and initializers. Only auto-configuration moved out.

34 / 141
Section
4. Inside a real auto-configuration: DataSourceAutoConfiguration
35 / 141

With the pipeline understood, let's look at a real player. Spring Boot's own DataSourceAutoConfiguration (lightly trimmed) uses nearly every key annotation in the toolbox:

36 / 141
java
@AutoConfiguration(before = HibernateJpaAutoConfiguration.class)@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")@EnableConfigurationProperties(DataSourceProperties.class)@Import({ DataSourcePoolMetadataProvidersConfiguration.class,          DataSourceCheckpointRestoreConfiguration.class })public class DataSourceAutoConfiguration {    @Configuration(proxyBeanMethods = false)    @Conditional(PooledDataSourceCondition.class)    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })    @Import({ DataSourceConfiguration.Hikari.class,              DataSourceConfiguration.Tomcat.class })    protected static class PooledDataSourceConfiguration {    }}
37 / 141

Annotation by annotation — this is how you should read auto-configuration source:

38 / 141
  • @AutoConfiguration: the new marker. It is equivalent to @Configuration(proxyBeanMethods = false) but additionally gains ordering (before / after). Legacy auto-configuration used @Configuration; new code should standardize on this
  • before = HibernateJpaAutoConfiguration.class: declares "I must run before the JPA auto-configuration". JPA needs a DataSource, so the wrong order makes JPA fail to find one — exactly the topic of Section 5
  • @ConditionalOnClass({ DataSource.class, ... }): these classes must exist on the classpath. Without spring-jdbc the whole configuration class is skipped and there will be no DataSource in the container
  • @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory"): if the project uses reactive R2DBC, let R2DBC handle it and get out of the way
  • @EnableConfigurationProperties(DataSourceProperties.class): binds spring.datasource.* into a Java object for the @Bean methods below
  • @Import(...): imports finer-grained configuration. Auto-configuration is nested and layered: the outer class decides "should we configure a DataSource", the inner one decides "which connection pool"
39 / 141
Section
5. @AutoConfiguration ordering: why order really matters
40 / 141

Auto-configuration classes are not peers — they have dependencies. The classic pair: DataSourceAutoConfiguration must run before HibernateJpaAutoConfiguration. If JPA runs first and looks for a DataSource while none is registered yet, startup fails with "no DataSource found".

41 / 141

Spring Boot offers three ways to express order:

42 / 141
Code
Codejava
// Way one: before / after on @AutoConfiguration (most common, declares a dependency)@AutoConfiguration(before = HibernateJpaAutoConfiguration.class)public class DataSourceAutoConfiguration { }// Way two: after on the class — wait for the other to finish@AutoConfiguration(after = DataSourceAutoConfiguration.class)@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class })public class HibernateJpaAutoConfiguration extends JpaBaseConfiguration {    // here we can safely assume the DataSource is ready}// Way three: @AutoConfigureOrder — an absolute number, lower runs first@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)public class MyEarlyAutoConfiguration { }
Notes
  • before / after are relative declarations meaning "I depend on so-and-so". Cycles are not allowed and fail at startup
  • @AutoConfigureOrder is an absolute declaration using a number. It is more forceful but drifts toward magic numbers, so before/after is preferred
  • Fallback rule: after resolving all before/after relations, AutoConfigurationSorter arranges the remaining classes with no dependency relation in alphabetical order (A→Z). This makes ordering deterministic — the same list yields the same order every startup, never jittering across JVMs

Key point: if asked "how is the order of auto-configuration classes decided", the standard answer has three layers — first resolve dependency order from before/after, then use @AutoConfigureOrder numbers for the ones without dependencies, and finally fall back to alphabetical order. Answer all three and you have full marks.

43 / 141

Those three layers are not parallel rules; they are a pipeline where each stage silences the ones below it. Click through it and pay attention to box 4 — it is the unglamorous alphabetical fallback that makes "the same pom behaves the same on every machine" true:

44 / 141
Diagram
FlowHow the auto-configuration order actually gets fixed (click through)1 / 5
Go from ① to ⑤. Box ③ is the one people forget to mention: a cycle is a startup failure, not a silent reshuffle
→
→
→
→
① Collect the candidates, order not yet considered
Whatever survives AutoConfigurationImportFilter enters the sorter as an unordered set — the merged result of list files from many jars, where nobody knows anybody.
All clearIn one line: dependency topology first, priority bands second, alphabetical fallback third — only after all three is the registration order reproducible.
45 / 141
Section
6. The condition matrix: the brain of auto-configuration
46 / 141

Auto-configuration decides "should I take effect" through conditional annotations. The table below collects the most common ones; the next article covers each in depth:

47 / 141
Table
Condition annotationWhat it checksTypical use
@ConditionalOnClassthe class is on the classpathwire only when the dependency is present (e.g. HikariCP)
@ConditionalOnMissingClassthe class is absentrule out one implementation
@ConditionalOnBeana bean of the type already existswire only when another bean exists
@ConditionalOnMissingBeanno bean of the type existsthe fallback when the user configured nothing
@ConditionalOnSingleCandidateexactly one candidate beana single DataSource or transaction manager
@ConditionalOnPropertya property equals / differs from a valuefeature toggles (like spring.aop.auto)
@ConditionalOnWebApplicationthe app is a web applicationweb-related auto-configuration
@ConditionalOnExpressiona SpEL expression is truecomplex combined conditions
48 / 141
Tip

treat this table as a dictionary for now. The genuinely hard part is their evaluation order — @ConditionalOnBean depends on someone else having registered already, so how is "someone else goes first" guaranteed? That is the core question the next article answers.

49 / 141
Section
7. The debugging weapon: --debug and the condition report
50 / 141

When auto-configuration "does not take effect", guessing is the worst move. Spring Boot ships a condition evaluation report that prints every auto-configuration class's decision and the reason. Enable it in one of three ways:

51 / 141
bash
# Way one: a startup argumentjava -jar app.jar --debug
52 / 141
yaml
# Way two: configuration filedebug: true# Way three: only turn on the auto-configuration package loglogging:  level:    org.springframework.boot.autoconfigure: DEBUG
53 / 141

Once on, the console prints a report whose heart is two sections — "who took effect" and "who was skipped, and why":

54 / 141
Code
Codetext
============================CONDITIONS EVALUATION REPORT============================Positive matches:-----------------   DataSourceAutoConfiguration matched:      - @ConditionalOnClass found required classes 'javax.sql.DataSource',        'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)      - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory) did not find any beans (OnBeanCondition)Negative matches:-----------------   RabbitAutoConfiguration matched:      - @ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel' (OnClassCondition)Exclusions:-----------   DataSourceAutoConfiguration (excluded by @SpringBootApplication exclude)
Notes
  • Positive matches: these classes passed their conditions and really took effect. Each line lists the checks passed, effectively telling you "these beans come from here"
  • Negative matches: these were skipped, with the reason in parentheses. When troubleshooting auto-configuration, about 80% of your time is spent reading this section — if you find RabbitAutoConfiguration skipped because it "did not find com.rabbitmq.client.Channel", the root cause is a missing dependency
  • Exclusions: entries you deliberately excluded are listed separately, confirming your exclude took effect

Note: the report comes from a ConditionEvaluationReport object that records every condition evaluation result; --debug merely renders it as readable text. Understand this and you know the report is not "a log" but a snapshot of the container's internal decision process.

55 / 141
Animation
Animation · From starter to live bean
Animation · From starter to live bean
56 / 141

Of the three ways to turn the report on, the first (--debug as a launch argument) is fine for one local glance. Real diagnosis needs one of the other two, because only those can vary per environment. Use the generator like this: tick logging first and turn on DEBUG for the auto-configuration package alone — far quieter than a blanket debug: true, and it does not drown the rest of the output; then add the datasource block, since those url / username / driver lines are the remedy for Section 8 and for the trace at the end of Section 14; finally add Profile to move both groups into a per-environment document and get the shape most teams actually keep: noisy in development, quiet in production.

57 / 141
Generator
GeneratorPut the debug switch and the datasource in the same ymlapplication.yml1 / 5
Tick logging first and open only the auto-configuration package; then add datasource and Profile, and compare against the three ways above — which of them can switch per environment?
Output
server:
  port: 8080

spring:
  application:
    name: demo-service

logging:
  level:
    root: INFO
    com.example.demoservice: DEBUG
    org.springframework.jdbc.core.JdbcTemplate: DEBUG   # 打 SQL 与参数
  file:
    name: logs/app.log
  logback:
    rollingpolicy: { max-file-size: 50MB, max-history: 14 }
Why each choice matters
loggingLevels work per package; root=DEBUG floods you with third-party output — never in production.
58 / 141
Section
8. Trap: the right way to override auto-configuration
59 / 141

Auto-configuration is a "fallback when you configured nothing", so what is the right way to take over? First, a bad example: some people turn on this switch to make their bean win —

60 / 141
properties
# ❌ not recommended: turn on BeanDefinition overridingspring.main.allow-bean-definition-overriding=true
61 / 141

Since Spring Boot 2.1 this defaults to false (overriding not allowed). Turning it on seems to "override everything", but it turns "who wins" into a question of registration order: whether the auto-configuration class or your class registers first is undetermined, so you may get your implementation or Boot's default — an extremely hard-to-reproduce latent bug.

62 / 141

The right approach uses the fact that auto-configuration steps aside on its own. Almost every auto-configuration class carries @ConditionalOnMissingBean:

63 / 141
java
@Configurationpublic class MyDataSourceConfig {    // As soon as you define a DataSource, DataSourceAutoConfiguration will step    // aside thanks to @ConditionalOnMissingBean and never conflict with your bean    @Bean    @ConfigurationProperties("spring.datasource.hikari")    public DataSource dataSource(DataSourceProperties props) {        return props.initializeDataSourceBuilder()                    .type(HikariDataSource.class)                    .build();    }}
64 / 141

Why does this work? The clue planted back in Section 2: AutoConfigurationImportSelector is a DeferredImportSelector, postponed until after all user configuration has been processed. So when the auto-configuration class evaluates @ConditionalOnMissingBean, your DataSource is already registered, the condition reports "already present", and auto-configuration steps aside. Deferred import is precisely the technical guarantee of "user first".

65 / 141
Trap

allow-bean-definition-overriding=true masks a conflict rather than resolving it. It makes you believe the override succeeded while planting an order-dependency landmine. To override auto-configuration with your own bean, rely on the @ConditionalOnMissingBean hand-off first; only when you cannot conveniently define a bean of the same type (say you want to change a Builder rather than the final object) should you switch to the official Customizer extension points.

66 / 141

"Deferred import → you register first → it evaluates afterwards → it steps aside" is exactly this six-frame sequence:

67 / 141
Animation
Animation · The yielding mechanism: why your bean wins
Animation · The yielding mechanism: why your bean wins
68 / 141

Now the same thing as a stepped run. Six lines on the left, and on the right the two cells that decide everything: whether the container already holds a dataSource, and who is evaluating. Press step through beats 3 and 4 — the half-second of ordering between them is the entire technical content of "user first":

69 / 141
Stepper
StepperFrame by frame: how your own @Bean pushes auto-configuration aside1 / 6
Six beats. Watch the cell 'DataSource definitions in the container' — its state at beat 4 decides the verdict
Code under debug
1// the container parses your MyDataSourceConfig (a plain @Configuration, never deferred)
2// the BeanDefinition for @Bean dataSource is registered; no object built yet
3// only now does DeferredImportSelector run: the auto-config candidates arrive
4// DataSourceAutoConfiguration evaluates @ConditionalOnMissingBean(DataSource.class)
5// the condition sees one already present -> false, the inner configuration is not registered
6// refresh() instantiates: the container's single DataSource is your HikariDataSource
Variables now
being processedMyDataSourceConfig
which batchthe user's own configuration
deferred queuenot started yet
Call stack
1ConfigurationClassParser.parse
2plain @Configuration branch
1Nothing special happens here, and that is the point: your class is parsed in the normal pass. ImportSelectors marked as deferred are collected during this pass but deliberately not executed — the whole mechanism hides in that one word, deferred.
70 / 141
Section
9. Hands-on: the scene of auto-configuration condition evaluation
71 / 141

The demo below turns auto-configuration condition evaluation into switchable knobs. Toggle the three condition switches and watch, per auto-configuration bean, when it is created and when it is skipped wholesale:

72 / 141
Kernel lab
73 / 141

Enough buttons — this article's central question ("did it apply, and if not why") has three direct answers on the command line: conditions prints the evaluation record, cond flips one condition by hand, and restart walks the whole assembly chain again.

74 / 141

Type them in this order; the contrast between steps 4 and 5 is exactly the contrast between the two gates in Section 2:

75 / 141
  1. boot — build the container, read the wiring log
  2. conditions — who is in, who is out, with the reason in brackets
  3. cond jdbcOnClasspath false — pretend JDBC is not on the classpath
  4. restart, then conditions again — is DataSourceAutoConfiguration still in the report at all?
  5. cond userDataSource true — simulate you defining your own DataSource, then restart
  6. lab autoconf filter versus lab autoconf report — how many the fast sieve removed, how many the report kept
76 / 141
Console
77 / 141
Note

steps 3 and 5 look similar and mean opposite things. jdbcOnClasspath false makes the configuration class vanish at the fast-sieve stage, so its name is absent from the report entirely; with userDataSource true it still appears under Negative matches, bracketed with OnBeanCondition. Those are the two testable predictions from the Section 2 double-gate picture.

78 / 141
Section
10. Decision: when you need to override auto-configuration, what should you actually do
79 / 141
Decision
Decisionyou want to give the auto-configured `ObjectMapper` a custom style (a different date format, and omit nulls on serialization). Which approach should you pick?
80 / 141
Section
11. Hands-on one: the four-stage assembly line, stage by stage
81 / 141

Section 2 gave you the sequence in prose. The demo below turns it into something you can advance stage by stage — the autoconf scenario simulates the auto-configuration chain, and its five buttons map onto "list → filter → sort → apply → report":

82 / 141
  • Where candidates come from (imports): sweep META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports across every jar on the classpath and merge the fully-qualified names into one big list. This stage only does IO and checks no condition, so the count looks alarming (the built-in list alone has 130+ entries)
  • Condition filtering (filter): AutoConfigurationImportFilter takes a rough cut first. You watch a large batch of configuration classes get eliminated because classes are missing from the classpath — this is the "fast knife" of Section 7; it never parses annotations
  • Sorting (sort): survivors go to AutoConfigurationSorter: resolve before/after dependencies first, then @AutoConfigureOrder, then alphabetical fallback. The order printed here is the order you will later read in the report
  • Getting applied (apply): full condition evaluation per class; passers become BeanDefinitions, failures are written into the report
  • The report (report): results rendered as Positive matches / Negative matches / Exclusions / Unconditional — byte for byte what --debug shows in your console
83 / 141
Kernel lab
TeaVMThe auto-configuration chain: list, filter, sort, apply, reportidle
Click imports, filter, sort, apply, report in order and note how the candidate count changes at each stage
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
84 / 141
Key point

the candidate count collapses hardest at filter, but what actually decides "does this bean exist" is apply. That gap between the two numbers is the answer to "I added the dependency and it still does nothing" — dependencies change the classpath, and the classpath drives condition evaluation.

85 / 141
Section
12. Hands-on two: three ways to make it work, three ways to silence it
86 / 141

Every stage of that line can be overridden by hand. These three demos each train one thing, and the last one is the practical version of Section 10's decision.

87 / 141

The first drills condition annotations. In cond, the missing argument demonstrates the umbrella rule — the shop lends an umbrella only because the guest brought none (no bean of that type exists, so auto-configuration supplies one); switch to report to see each verdict in the same wording --debug uses. Walk all four conditions once and you will never need to guess "why isn't it active" again:

88 / 141
Kernel lab
TeaVMWhat each condition interrogates: onclass, onbean, onprop, missingidle
Focus on missing: define your own bean first, then watch it shut the auto-configuration out
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
89 / 141

The second runs the pipeline backwards: write a starter yourself and traverse the whole chain end to end. starter's four arguments are meta (register the auto-configuration class in the list), props (bind prefixed properties with @ConfigurationProperties), bean (create it once conditions pass), off (disable via spring.xxx.enabled=false). After this, Section 8's "step aside" mechanism stops being abstract — it is something you wrote in four lines:

90 / 141
Kernel lab
TeaVMAssemble one auto-configuration yourself: list, properties, bean, switch offidle
After meta, props and bean, always click off to compare the property switch against exclude
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
91 / 141

The third trains "when several places declare the same property, who wins". Auto-configuration leans heavily on properties to open and close features, so precedence is mandatory knowledge: command line > JNDI > system properties > application-{profile}.yml > application.yml > defaults. The prop scenario demonstrates the overriding live:

92 / 141
Kernel lab
TeaVMWho overrides whom: auto-configuration switches are decided this way tooidle
After order, switch to profile to see one auto-configuration behave differently per environment
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
93 / 141

The fourth experiment stitches this article back onto the previous one with a single picture: there are four roads an object can take into the container, and beanin's auto mode is the one this article is about — no scanning involved, no package name consulted, only the list file inside the jar. Flip to scan right afterwards and you have seen both fates side by side, which is exactly what the contrast figure below is trying to say:

94 / 141
Kernel lab
TeaVMOf the four roads into the container, auto-configuration is the last oneidle
Use scan first to see a road that only understands package paths, then auto to see the road that does not care about them at all
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
95 / 141

And keep this contrast picture in mind — last article's "bean not found" and this article's "auto-configuration not taking effect" are two faces of one coin: your classes enter through the scanning door, framework classes through the registration door. Use the wrong door and they simply never arrive:

96 / 141
Diagram
Figure · Two roads into the container: scanning vs a registration list
Figure · Two roads into the container: scanning vs a registration list
97 / 141

By now you actually hold six wrenches, but they do not grip the same bolt: some delete candidates, some change a verdict, one only matters after registration is finished. Pair each wrench with the stage it acts on, and the next outage stops being a guessing game:

98 / 141
Match
MatchSix wrenches, six different boltsMatched 0/6 · Missed 0
Left is what you write in code or config, right is the stage where it really bites. A wrong pair explains which stage it skipped
Pick a card on the left first
99 / 141
Section
13. Sandbox: same complaint, four cells to check
100 / 141

The --debug report is long, but only four cells matter. Watch this animation first — it shows how the report gets written, which is exactly why we call it a decision snapshot rather than a log — then switch scenarios on the left and let the right tell you which cell to read:

101 / 141
Animation
Animation · How the --debug report gets written
Animation · How the --debug report gets written
102 / 141
Sandbox
SandboxAuto-configuration not working? Check these four cells
Result
Negative matches:
DataSourceAutoConfiguration:
- @ConditionalOnClass did not find required class 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)
#go to Negative matches: the OnClassCondition in brackets already names the missing class
Classic missing dependency: spring-jdbc alone, without a starter that brings a pool. Add spring-boot-starter-data-jpa or HikariCP and re-read.
103 / 141
Note

the reading order of the four cells matters. First confirm whether it is in Positive matches (yes → go inspect your properties; no → continue), then read Negative matches for the reason (the Condition class in brackets is the root cause), and finally check Exclusions to be sure you did not accidentally switch it off yourself. This order eliminates most "let me just tweak something and see".

104 / 141
Section
14. Common errors, quick reference
105 / 141

Every fragment below pastes straight into a search box:

106 / 141
Table
Error fragmentReal cause30-second fixDig deeper in
java.lang.NoClassDefFoundError: org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseTypeA class required by an auto-configuration's @ConditionalOnClass is not on the classpath — one dependency shortRun --debug and search this class name in Negative matches; it tells you which auto-configuration got skipped, then add the owning artifactThis article, Sections 7 and 13
ClassNotFoundException: com.rabbitmq.client.Channel while RabbitAutoConfiguration stays completely silentSame mechanism: class missing, condition unmet, whole config class skipped. Boot never errors for a feature you did not enableThat is exactly the - @ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel' (OnClassCondition) line in the report; add spring-boot-starter-amqpThis article, Section 6
A custom starter silently stops working after upgrading to Spring Boot 3, with no error at allIt still registers auto-configuration under META-INF/spring.factories, whose auto-configuration entry point was removed in 3.0Move the class names, one per line, into META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports; do not delete spring.factories wholesale — it still serves listeners and initializersThis article, Section 3
Field dataSource in ... required a bean of type 'javax.sql.DataSource' that could not be foundThe data-source auto-configuration was skipped or excluded, or spring.datasource.url is absent so a condition failsCheck Exclusions first, then the reason in Negative matches; if it appears in neither, the starter was never addedArticle #17 Section 4 · Article #28
Parameter 0 of method jpaVendorAdapter in ...HibernateJpaAutoConfiguration required a bean of type '...' that could not be foundOrdering between auto-configuration classes went wrong: a bean that should register first had not arrived yetDeclare the relation with @AutoConfiguration(before/after) instead of tuning magic numbers in @AutoConfigureOrderThis article, Section 5
The bean 'objectMapper', defined in class path resource [...] could not be registered. A bean with that name has already been defined [...] and overriding is disabled.Same-name bean clash, and overriding has been disabled by default since Boot 2.1Do not reach for allow-bean-definition-overriding; rename yours or delete the definition and let @ConditionalOnMissingBean yieldThis article, Section 8 · quiz two below
spring.autoconfigure.exclude lists a class but startup reports No auto-configuration classes foundThe class name in the property is misspelled, or that class is not on the classpath at all (starter missing)Paste the fully-qualified name from your IDE rather than typing it, and confirm the corresponding starter is in the pomArticle #17 Section 6
107 / 141
Trap

auto-configuration failing to take effect almost never throws — it simply stays absent from the container in silence. So the first move is always --debug, not editing code and hoping. Absence is not an error; absence means a condition was unmet.

108 / 141

That rule has one common exception: auto-configuration did apply, but it could not produce a usable bean. Then it does throw, and it puts its own authorship into the stack. Below is what a fresh project with spring-boot-starter-data-jpa and no application.yml looks like — do not read the analysis, just click the frame you think is guilty:

109 / 141
Triage
Error triageDataSourceBeanCreationException: Failed to determine a suitable driver class
Auto-configuration applied but could not build the DataSource: the error names its own author

A new project with Data JPA and H2 ticked, application.yml left completely empty. Startup aborts with 'Failed to configure a DataSource' in the middle of the screen.

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userController' defined in file [D:\demo\target\classes\com\example\demo\web\UserController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1212)
at org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari.dataSource(DataSourceConfiguration.java:49)
at org.springframework.boot.autoconfigure.jdbc.DataSourceProperties.determineDriverClassName(DataSourceProperties.java:186)
Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
110 / 141
Section
15. Quick self-tests
111 / 141
Quiz
Check yourselfAfter upgrading to Spring Boot 3.0, every auto-configuration inside your own starter stops working — and startup prints no error at all. Most likely cause?
Pick one — you get feedback right away
112 / 141
Quiz
Check yourselfYou want to replace Boot's auto-configured ObjectMapper. Which action is the worst choice?
Pick one — you get feedback right away
113 / 141
Section
16. Hands-on exercises
114 / 141
Section
Tier one · Follow along
115 / 141

Goal: turn on --debug, read one real auto-configuration chain, and locate the origin of five beans inside the report.

116 / 141

pom.xml:

117 / 141
xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>3.2.5</version>        <relativePath/>    </parent>    <groupId>com.example</groupId>    <artifactId>autolab</artifactId>    <version>0.0.1-SNAPSHOT</version>    <properties>        <java.version>17</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <!-- H2 triggers DataSourceAutoConfiguration on its own; no datasource config needed -->        <dependency>            <groupId>com.h2database</groupId>            <artifactId>h2</artifactId>            <scope>runtime</scope>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
118 / 141

The main class (src/main/java/com/example/autolab/AutolabApplication.java, note the root package):

119 / 141
java
package com.example.autolab;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class AutolabApplication {    public static void main(String[] args) {        SpringApplication.run(AutolabApplication.class, args);    }}
120 / 141

One line is enough in src/main/resources/application.properties:

121 / 141
properties
debug=true
122 / 141

Run mvn spring-boot:run. Expected key fragment of the startup log (ordering and exact entries vary slightly with dependencies):

123 / 141
text
 :: Spring Boot ::                (v3.2.5)============================CONDITIONS EVALUATION REPORT============================Positive matches:-----------------   DataSourceAutoConfiguration matched:      - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)      - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory) did not find any beans (OnBeanCondition)   DataSourceAutoConfiguration$EmbeddedDataSourceConfiguration matched:      - @ConditionalOnMissingBean (types: javax.sql.DataSource,javax.sql.XADataSource) did not find any beans (OnBeanCondition)   DispatcherServletAutoConfiguration matched:      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)      - found 'session' scope (OnWebApplicationCondition)Negative matches:-----------------   RabbitAutoConfiguration:      - @ConditionalOnClass did not find required classes 'org.springframework.amqp.rabbit.core.RabbitTemplate', 'com.rabbitmq.client.Channel' (OnClassCondition)   QuartzAutoConfiguration:      - @ConditionalOnClass did not find required class 'org.quartz.Scheduler' (OnClassCondition)Exclusions:-----------    None.Unconditional:--------------    None.... Tomcat started on port 8080 (http) with context path ''... Started AutolabApplication in 2.1 seconds
124 / 141

Now play "find things" three times, quoting the exact line each time:

125 / 141
  1. Search DispatcherServletAutoConfiguration in Positive matches and say who handed you the DispatcherServlet bean
  2. Pick any three entries in Negative matches, copy the Condition class name from the brackets, and say which class is missing in each
  3. Remove debug=true and start with the --debug argument instead; confirm the report is identical (and feel why Section 13 says the three enabling styles are interchangeable)
126 / 141

Checklist: ① you can explain Positive versus Negative matches without notes; ② for at least one negative entry you can state "adding which dependency would promote it"; ③ you can say why the line Exclusions: None. deserves a deliberate glance.

127 / 141
Section
Tier two · Variants
128 / 141

Each variant changes one thing:

129 / 141
  1. Promote a negative entry: add spring-boot-starter-amqp and restart. You will observe RabbitAutoConfiguration moving from Negative matches into Positive matches — the most direct proof that conditions are driven by the classpath.
  2. Watch the yielding mechanism live: create MyJsonConfig with a @Bean ObjectMapper (set any date format), then hit an endpoint returning an object. You will observe the @ConditionalOnMissingBean verdict for JacksonAutoConfiguration flip, and your formatting winning. That is option B of Section 10 executed for real.
  3. Manufacture the bad pattern deliberately: on top of step 2, set spring.main.allow-bean-definition-overriding=true and name your bean objectMapper. You will observe the conflict warning vanish while which implementation lives depends on registration order — keep both logs side by side in your notes and you will never forget why this is wrong.
  4. Silence one auto-configuration by property: put spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration in application.properties. You will observe it listed under Exclusions, and EmbeddedDataSourceConfiguration disappearing from Positive matches with it.
130 / 141
Section
Tier three · Build one
131 / 141

Build a minimal starter named greeting-spring-boot-starter and walk the entire chain of this article.

132 / 141
  • Two Maven modules: greeting-spring-boot-starter (dependency aggregation only) and greeting-spring-boot-autoconfigure (the code)
  • The auto-configuration class must carry @AutoConfiguration, @ConditionalOnClass(GreetingService.class) and @ConditionalOnMissingBean, plus @EnableConfigurationProperties(GreetingProperties.class) binding the prefix acme.greeting
  • GreetingProperties has at least two fields: prefix (default [hello]) and enabled (default true); add @ConditionalOnProperty(prefix = "acme.greeting", name = "enabled", havingValue = "true", matchIfMissing = true) on the @Bean method
  • Register the class in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • A separate demo project depends only on the starter, sets nothing about component scanning, injects GreetingService directly and exposes GET /greet
133 / 141

Checklist: ① the demo starts with zero configuration and /greet returns text carrying the default prefix; ② changing acme.greeting.prefix takes effect immediately; ③ setting acme.greeting.enabled=false makes startup fail with "required a bean of type GreetingService", and the Negative matches section of --debug names the property condition that blocked it; ④ remove the optional dependency and watch @ConditionalOnClass erase the whole configuration class; ⑤ your README draws the four stages "list → filter → sort → apply" and marks which stage each of your files participates in.

134 / 141
Section
17. Key-point self-check
135 / 141
Self-check

from memory, name the four stages of the auto-configuration pipeline and roughly what each does.

136 / 141
Self-check

what separates AutoConfigurationImportFilter from ConditionEvaluator? Why call the first a fast knife and the second a fine sieve?

137 / 141
Self-check

at which version did spring.factories give way to AutoConfiguration.imports? Which kind of starter silently dies on the jump to 3.0?

138 / 141
Self-check

which three layers decide auto-configuration ordering? Why do the docs prefer before/after over @AutoConfigureOrder?

139 / 141
Self-check

what is the correct way to replace an auto-configured bean? Explain it twice — once with the finished-apartment analogy, once with the umbrella analogy.

140 / 141

Mantra: **the list sets the candidates, conditions decide life and death, ordering decides sequence, deferral decides who yields** — one line for the whole pipeline.

141 / 141
Summary

the full auto-configuration pipeline is @EnableAutoConfiguration → AutoConfigurationImportSelector (deferred import) → read the AutoConfiguration.imports list → fast filter + condition evaluation → register BeanDefinition → refresh() instantiation. The list format moved from spring.factories to AutoConfiguration.imports in 3.0; ordering is decided in three layers by before/after, @AutoConfigureOrder, and alphabetical fallback; whether a class takes effect depends entirely on conditions. Two practical rules to remember: when troubleshooting auto-configuration, start with --debug and read the condition report, and override auto-configuration by defining your own bean and letting @ConditionalOnMissingBean step aside, not by turning on the overriding switch.