The Import Candidates Mechanism and Auto-Configuration Discovery
From spring.factories to AutoConfiguration.imports
Spring Boot 2.x registered auto-configuration classes in META-INF/spring.factories. That file used a key-value format shared across multiple Spring extension points:
# spring.factories (Spring Boot 2.x)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
...
The problem was cost. SpringFactoriesLoader loaded every entry in every spring.factories file at startup, regardless of the key. Auto-configuration entries were interleaved with ApplicationListener, EnvironmentPostProcessor, and other extension points. Parsing the file meant reading all entries, even those irrelevant to auto-configuration.
Spring Boot 3.0 introduced a dedicated file:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
One class name per line. No key-value syntax. No shared file:
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
Lines starting with # are comments. Blank lines are ignored. The file is read by ImportCandidates.load(), which uses ClassLoader.getResources() to find every instance of this file across all JARs. Each JAR contributes its own candidates.
The migration was not just cosmetic. By separating auto-configuration discovery from the general-purpose spring.factories mechanism, Spring Boot reduced the amount of data parsed at startup and eliminated interference between unrelated extension points. The old spring.factories key for EnableAutoConfiguration is no longer read in Spring Boot 3.x. If you have a library that still uses it, those auto-configuration classes will not be discovered.
AutoConfigurationImportSelector Processing Flow
AutoConfigurationImportSelector is the class that drives the entire discovery pipeline. It implements DeferredImportSelector, which means it runs after all regular @Configuration classes and @Import annotations have been processed. Here is the flow through its internals:
Step 1: Candidate Loading
// AutoConfigurationImportSelector.java
protected List<String> getCandidateConfigurations(
AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = ImportCandidates.load(
AutoConfiguration.class, getBeanClassLoader()
).getCandidates();
// configurations now contains every class name from every imports file
return configurations;
}
ImportCandidates.load() is the entry point. It constructs the resource path from the annotation class name (org.springframework.boot.autoconfigure.AutoConfiguration) and appends it to META-INF/spring/. Then it reads every matching resource from the classloader:
// ImportCandidates.java
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
String location = String.format("META-INF/spring/%s.imports", annotation.getName());
Enumeration<URL> urls = classLoader.getResources(location);
List<String> candidates = new ArrayList<>();
while (urls.hasMoreElements()) {
// Read each file, parse line by line, skip comments and blanks
candidates.addAll(readCandidateConfigurations(urls.nextElement()));
}
return new ImportCandidates(candidates);
}
A typical Spring Boot web application with JPA, security, and actuator pulls in 150+ candidate class names at this stage.
Step 2: Deduplication
Multiple JARs might declare the same auto-configuration class. AutoConfigurationImportSelector removes duplicates using a LinkedHashSet, which preserves insertion order while eliminating repeats.
Step 3: Exclusion Processing
Exclusions are applied next. The selector collects exclusions from three sources:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })@SpringBootApplication(excludeName = "com.example.SomeAutoConfiguration")- The property
spring.autoconfigure.exclude
// AutoConfigurationImportSelector.java
protected Set<String> getExclusions(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude")); // Class references
excluded.addAll(asList(attributes, "excludeName")); // String class names
excluded.addAll(getExcludeAutoConfigurationsProperty()); // Property value
return excluded;
}
Excluded classes are removed from the candidate list before any condition evaluation occurs. This is a hard removal. No @Conditional annotations on the excluded class are checked. No bean definitions from the excluded class are registered. No side effects.
Step 4: Filtering
The remaining candidates pass through AutoConfigurationImportFilter implementations. The three built-in filters perform lightweight checks without loading the configuration classes:
OnBeanConditionFilterchecks@ConditionalOnBean/@ConditionalOnMissingBeanmetadataOnClassConditionFilterchecks@ConditionalOnClass/@ConditionalOnMissingClassusing ASMOnWebApplicationConditionFilterchecks the application type
These filters read annotation metadata from the class bytecode using ASM. They never trigger class loading. If a candidate has @ConditionalOnClass(MongoClient.class) and MongoClient is not on the classpath, the candidate is discarded here, before Spring attempts to load or parse the configuration class.
Step 5: Sorting
The surviving candidates are sorted by AutoConfigurationSorter. It reads @AutoConfigureBefore, @AutoConfigureAfter, and @AutoConfigureOrder annotations and produces a topological sort. The sorted list becomes the final import order.
AutoConfigurationPackages: How Spring Knows Your Base Package
When @SpringBootApplication is on com.example.saas.SaasApplication, Spring Boot needs to know that com.example.saas is the base package. This information is used by JPA entity scanning, MyBatis mapper scanning, and other features that need to know “where the user’s code lives.”
@AutoConfigurationPackage (which is on @EnableAutoConfiguration, which is on @SpringBootApplication) registers a special bean via AutoConfigurationPackages.Registrar:
// AutoConfigurationPackages.java
static class Registrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// Extracts the package name from the annotated class
String[] packageNames = new PackageImports(metadata).getPackageNames()
.toArray(new String[0]);
// Registers a bean definition holding these package names
register(registry, packageNames);
}
}
The registered bean holds the package names as a List<String>. Other auto-configuration classes retrieve it:
// Used by JPA auto-configuration to scan for @Entity classes
List<String> packages = AutoConfigurationPackages.get(beanFactory);
// packages = ["com.example.saas"]
This is why placing your main application class in a root package is important. If SaasApplication is in com.example.saas, the base package is com.example.saas, and entity scanning covers com.example.saas.tenant, com.example.saas.billing, and so on. If you put it in com.example.saas.config, entities in com.example.saas.tenant are outside the scanned tree.
The Failure Mode: Silent Exclusion Failures
In a multi-tenant SaaS backend, you decide to exclude the default security auto-configuration because you have custom tenant-aware security:
// BROKEN: Typo in excludeName, fails silently
@SpringBootApplication(
excludeName = "org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration"
)
public class SaasApplication { }
The correct class name is org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration. The package includes .servlet. With the wrong name, AutoConfigurationImportSelector does not find a matching candidate to exclude. It does not throw an exception. The exclusion is silently ignored. The default security auto-configuration activates, and your multi-tenant application gets the default security filter chain instead of your custom one.
You notice in production when all endpoints return 401 because the default SecurityFilterChain requires authentication, and your custom TenantSecurityConfig is fighting the auto-configured one for precedence.
Starting with Spring Boot 2.x, there is a safety check: handleInvalidExcludes() validates that every exclusion class name actually exists as a candidate. But this check requires the class to be loadable. If the class name is misspelled in a way that does not match any candidate and does not match any loadable class, the behavior depends on the Spring Boot version. Some versions log a warning. Others silently skip the invalid exclusion.
The Correct Pattern
Use the class reference form, not the string form. The compiler validates the reference:
// CORRECT: Class reference catches typos at compile time
@SpringBootApplication(
exclude = SecurityAutoConfiguration.class
)
public class SaasApplication { }
If SecurityAutoConfiguration does not exist or is not importable, the code does not compile. No silent failure possible.
For classes that are conditionally on the classpath (you want to exclude them when present but the import might not resolve), use excludeName with verification:
// CORRECT: String exclusion with runtime verification
@SpringBootApplication(
excludeName = "org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration"
)
public class SaasApplication { }
Then verify with the condition evaluation report:
java -jar app.jar --debug 2>&1 | grep -A 2 "Exclusions"
The output should list your excluded class:
Exclusions:
-----------
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
If the class appears under “Negative matches” instead of “Exclusions,” your exclusion is not working. Negative matches mean the class was evaluated and its conditions failed. Exclusions mean the class was removed before evaluation. Both prevent the class from activating, but only exclusions guarantee that no processing of the class occurred.
For multi-tenant backends with complex exclusion requirements, create a test that validates exclusions are effective:
@SpringBootTest
class AutoConfigurationExclusionTest {
@Autowired
private ApplicationContext context;
@Test
void securityAutoConfigurationIsExcluded() {
ConditionEvaluationReport report = ConditionEvaluationReport.get(
(ConfigurableListableBeanFactory) context.getAutowireCapableBeanFactory()
);
// Verify it is in exclusions, not in unconditional classes
assertThat(report.getExclusions())
.contains("org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration");
}
}
This test fails if someone removes the exclusion or misspells it. It runs in CI and catches the problem before it reaches production.