How Spring Boot Auto-Configuration Finds and Loads Classes: A Deep Dive
This article explains Spring Boot's auto-configuration mechanism, covering how @EnableAutoConfiguration triggers AutoConfigurationImportSelector, loads candidate classes from META-INF imports files, evaluates conditional annotations like @ConditionalOnClass and @ConditionalOnMissingBean at different phases, orders configurations via @AutoConfigureBefore/After, binds properties with @ConfigurationProperties, and troubleshoots silent failures using --debug and ConditionEvaluationReport.
Auto-Configuration Trigger Entry Point
The @SpringBootApplication annotation is a composite of three annotations: @SpringBootConfiguration marks a configuration class, @ComponentScan scans the current package and sub-packages, and @EnableAutoConfiguration is the actual switch for auto-configuration. These three annotations handle separate concerns; many developers mistakenly believe component scanning picks up auto-configuration classes, but those classes reside outside the user's package path. @EnableAutoConfiguration does two things: imports auto-configuration classes via @Import(AutoConfigurationImportSelector.class) and provides exclusion hooks through exclude() and excludeName() attributes. The real workhorse is AutoConfigurationImportSelector, which implements DeferredImportSelector — the "Deferred" prefix means these configuration classes are processed after all user-defined @Configuration classes have been parsed. This ordering is critical: user configurations register first, so when auto-configuration classes later evaluate @ConditionalOnMissingBean, they can see the beans already registered by the user.
Auto-Configuration Class Loading Sources
Dozens or hundreds of auto-configuration classes are not hard-coded; they are read from configuration files. Before Spring Boot 2.7, the file was META-INF/spring.factories with the key
org.springframework.boot.autoconfigure.EnableAutoConfigurationlisting class names as a comma-separated, line-continued value. From 2.7 onward, a dedicated file
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsis used, with one fully-qualified class name per line. This file lives inside the starter JAR and can be inspected with unzip. Spring Boot 3.0 stops reading the spring.factories key for auto-configuration entirely; old files become ineffective without any error, a common upgrade pitfall where third-party starters silently fail on 3.x.
The simplified getCandidateConfigurations method in AutoConfigurationImportSelector uses
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).getCandidates()to read the list. The raw class names then undergo three processing steps: deduplication (multiple starters may declare the same class), sorting by @AutoConfigureOrder, @AutoConfigureBefore, and @AutoConfigureAfter, and filtering out classes listed in exclude or excludeName (from either the annotation or spring.autoconfigure.exclude in configuration files — both use the same filtering logic, but the file-based approach allows multi-environment switching without code changes).
Conditional Annotation Evaluation Timing
Far more auto-configuration classes are loaded as candidates than actually take effect. In a typical Web + JPA project, over 200 candidate classes are loaded but fewer than 20 become beans; the rest are filtered out by conditional annotations. The most common are @ConditionalOnClass (checks classpath for a class) and @ConditionalOnMissingBean (checks container for a bean), but their evaluation times differ drastically. @ConditionalOnClass operates at the class level and is evaluated early. Spring Boot uses ASM to read bytecode without actually loading the class, so missing dependencies do not throw NoClassDefFoundError. Using Class.forName directly would break this safety. @ConditionalOnMissingBean evaluates much later, during bean definition registration, after all user configuration classes have been processed. Its evaluation result forms a layered tree: if an outer condition fails, inner conditions are never evaluated, so the report often shows only the outermost reason. This delayed evaluation is precisely why auto-configuration classes must be imported after user configurations.
Other conditionals include @ConditionalOnBean (checks for a bean in the container), @ConditionalOnProperty (checks a configuration property's value), and @ConditionalOnWebApplication (checks Servlet vs Reactive type). @ConditionalOnProperty has a tricky matchIfMissing parameter (default false): when false, a missing property makes the condition fail; when true, the condition passes even if the property is absent — suitable for features enabled by default. The author once spent over two hours debugging a reversed matchIfMissing setting.
Auto-Configuration Ordering
Ordering is not cosmetic; it directly affects conditional evaluation results. Some classes depend on others being registered first — for example, DataSourceAutoConfiguration must precede JdbcTemplateAutoConfiguration, otherwise the latter's dependency check fails even when a data source is configured. Three declaration styles exist, chosen by dependency tightness: the after attribute on @AutoConfiguration for simple cases, @AutoConfigureBefore / @AutoConfigureAfter for finer-grained multi-class dependencies, and @AutoConfigureOrder with an integer (lower values run first, negative values can go to the very front; ties break by class name). @AutoConfiguration (introduced in 2.7) combines @Configuration(proxyBeanMethods=false) with ordering semantics and is now recommended for all auto-configuration classes.
Ordering only affects relative class order, not conditional logic itself. Correct ordering won't make a non-matching class match, but wrong ordering can cause a matching class to miss its dependency during evaluation, resulting in a mysteriously missing bean. Circular after declarations fall back to alphabetical order without deadlocking, but the outcome becomes unpredictable.
@ConfigurationProperties Binding Process
Auto-configuration parameters live in XxxProperties classes bound via @ConfigurationProperties. The annotation itself does not register a bean; ConfigurationPropertiesBindingPostProcessor handles that. Binding uses a prefix (e.g., demo.alert), and relaxed binding maps demo.alert.batch-size=100 in YAML to a batchSize field, also supporting batchSize and environment variable DEMO_ALERT_BATCH_SIZE (heavily used in containerized deployments). Type conversion delegates to Spring's ConversionService; Boot adds converters for Duration (accepting 3s, 500ms) and DataSize (accepting 10MB), far more readable than raw long values. Custom enums or value objects require only a Converter bean — no extra annotations on the property class.
Three ways to register the properties bean: (1) @EnableConfigurationProperties(AlertProperties.class) on the auto-configuration class — most robust, binding the properties object to the auto-configuration lifecycle; (2) @ConfigurationProperties + @Component for component scanning; (3) @ConfigurationPropertiesScan on the main class (since Boot 2.2) to scan a package. The first approach is safest for starters because starter and business package names rarely overlap.
User Configuration vs Auto-Configuration Priority
Priority is simple: user-defined beans win. This is enforced by @ConditionalOnMissingBean on auto-configuration @Bean methods combined with deferred import ordering. If the business project declares its own AlertClient bean, the auto-configuration's bean is skipped, and --debug logs show AlertAutoConfiguration#alertClient in Negative matches with the reason on the indented second line. A trickier case: business code declares an implementation class while auto-configuration checks for an interface — the type mismatch makes the condition think no bean exists, resulting in two beans of the same type in the container.
Property priority follows a separate, fixed chain in PropertySource order: command-line args > environment variables > application-{profile}.yml > application.yml > @ConfigurationProperties field defaults. This cannot be changed. When debugging, call Environment.getProperty() to see the effective value; mismatches usually mean an environment variable or command-line argument overrode the config file.
A subtle pitfall: @ConditionalOnMissingBean is only reliable across different configuration classes. Within the same class, method declaration order affects the result, and the official docs explicitly warn against this usage. Split into separate classes or use @ConditionalOnBean with explicit ordering instead.
DataSource Auto-Configuration Decision Flow
DataSourceAutoConfigurationexemplifies layered conditional logic. At the class level, it requires both DataSource.class and EmbeddedDatabaseType.class on the classpath (provided by spring-boot-starter-jdbc); missing either triggers a silent OnClassCondition mismatch — the app starts but the bean is absent, only surfacing later when a service tries to inject it. Inside, two mutually exclusive paths exist: PooledDataSourceConfiguration activates when spring.datasource.type is set or a pool (Hikari, Tomcat, DBCP2) is on the classpath; EmbeddedDataSourceConfiguration activates when no DataSource bean exists and H2 or Derby is present. Both paths carry
@ConditionalOnMissingBean({DataSource.class, XADataSource.class}), so a user-defined DataSource disables both. Deeper still, DataSourceConfiguration.Hikari checks for the Hikari class via @ConditionalOnClass and then for an existing bean via @ConditionalOnMissingBean. Any unsatisfied layer short-circuits the entire branch.
The logic is clear but debugging is hard because intermediate results are invisible. --debug prints the ConditionEvaluationReport split into Positive matches and Negative matches, with rejection reasons attached to each excluded class.
Custom Starter Module Structure
Official starters split into two modules: xxx-spring-boot-autoconfigure holds auto-configuration code and XxxProperties, while xxx-spring-boot-starter contains only dependencies (typically just a pom.xml). This separation lets consumers either pull the full starter (transitive dependencies included) or depend solely on the autoconfigure module to control implementations themselves. Swapping underlying implementations later only requires changing one dependency coordinate. The autoconfigure module includes spring-boot-configuration-processor as an optional dependency to generate META-INF/spring-configuration-metadata.json at compile time, enabling IDE completion for demo.alert.* in application.yml — missing metadata doesn't affect runtime, only design-time hints.
The auto-configuration class follows the standard pattern: @AutoConfiguration with conditional annotations, @EnableConfigurationProperties to bind the properties class, and @Bean methods annotated with @ConditionalOnMissingBean for overrideability. Each bean method provides a fallback; consumers replace implementations by declaring a same-type bean without touching starter code. Missing dependencies simply prevent bean creation, with errors pointing at injection sites rather than the starter.
Auto-Configuration Registration and Verification
Registration means creating
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importswith one fully-qualified class name per line. The path and filename must match exactly — a missing dot or extra space causes silent failure with no error logs. Verification requires checking the packaged JAR under BOOT-INF/classes/META-INF/spring/. During the 2.7-to-3.x transition, many tutorials still reference spring.factories; 3.x ignores it completely when the imports file exists. To confirm a starter is active, look for a log line like o.s.b.a.AlertAutoConfiguration: Matched. Absence doesn't prove inactivity because ConditionEvaluationReport is off by default; enable --debug or logging.level.org.springframework.boot.autoconfigure=DEBUG for the full report. With Actuator, /actuator/conditions exposes the same data for production troubleshooting.
Troubleshooting Auto-Configuration Failures
Ordered by likelihood: (1) Verify the imports file path and name — extremely common for first-time starter authors; the file must land under BOOT-INF/classes/META-INF/spring/ in the final JAR. (2) Check conditional annotations: missing @ConditionalOnClass dependencies, @ConditionalOnProperty switches off, or @ConditionalOnMissingBean blocked by a user bean —
--debug Negative matchesstates the exact cause. (3) Inspect exclusion configuration: spring.autoconfigure.exclude or @SpringBootApplication(exclude=...) may silently skip the class, often added by another team in a shared base package. Also, the condition report caches the last startup's results; adding dependencies or changing config mid-run requires a restart to refresh. (4) Detect class shadowing: a business project class with the same fully-qualified name as the auto-configuration class will replace it at registration time — logs show a positive match but the wrong class executes. Only printing and comparing fully-qualified names reveals the duplicate.
Building a starter isn't hard; the difficulty lies in its entirely implicit activation — any misstep in conditions, imports file, or ordering manifests as "nothing happens" with no exception. After implementation, test in a clean project with --debug and scan Positive matches; it's faster than iterating in a full business project.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
