Understanding Spring's Auto-Configuration Mechanism Through @Import Analysis
Use Case Demonstration
Example 1: @EnableXXX Annotation Usage
In a Spring MVC project, adding the @EnableWebMvc annotation to a configuration class triggers Spring to automatically register a series of Spring MVC components, including HandlerMapping, HandlerAdapter, ViewResolver, and more.
Example 2: Spring Boot Auto-Configuration
In a Spring Boot application, the @SpringBootApplication annotation decorates the main application class. When the spring-boot-starter-web dependency is included, Spring automatically registers the same set of Spring MVC components.
How does Spring implement this automatic registration capability? The answer lies in the @Import annotation, which can引入 a configuration class or a configuration class selector.
When using a standard @EnableXXX annotation, it actually uses @Import to import a pre-defined configuration class, which configures specific Beans to implement the corresponding functionality.
When using Spring Boot's auto-configuration feature, it actually uses @Import to import a configuration class selector. This selector reads all configuration classes from the configuration file, evaluates their conditions, and imports only those that satisfy the conditions—thus achieving automatic configuration of certain features.
Source Code Analysis
@EnableXXX Annotation Implementation Principle
Examining the @EnableWebMvc annotation reveals that it uses @Import to reference a DelegatingWebMvcConfiguration class, which is decorated with @Configuration:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}
The parent class WebMvcConfigurationSupport defines numerous methods decorated with @Bean, which represent the Spring MVC components that Spring will register:
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
@Bean
@SuppressWarnings("deprecation")
public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
// implementation omitted
}
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
// implementation omitted
}
@Bean
public ViewResolver mvcViewResolver(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
// implementation omitted
}
}
The article "3 Cases to Understand Spring @Component Scanning: From Regular Applications to Spring Boot" explains how Spring scans Beans from packages annotated with @Configuration. This is primarily implemented in the doProcessConfigurationClass() method of ConfigurationClassParser. The prcoessing of @Import annotated classes is also handled within this method:
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
// ... code omitted ...
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
// ... code omitted ...
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
if (methodMetadata.isAnnotated("kotlin.jvm.JvmStatic") && !methodMetadata.isStatic()) {
continue;
}
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
// ... code omitted ...
return null;
}
When processing classes imported via @Import that are annotated with @Configuration, they are treated as configuration classes. The method processConfigurationClass() is called recursively, which then enters doProcessConfigurationClass() to parse methods annotated with @Bean and add them to the configuration class:
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> filter, boolean checkForCircularImports) {
// ... code omitted ...
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
// ... error handling ...
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
// ... code omitted ...
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), filter);
}
} finally {
this.importStack.pop();
}
}
}
The actual parsing of Bean methods from configuration classes into Bean definitions is performed in ConfigurationClassPostProcessor's processConfigBeanDefinitions() method, which calls ConfigurationClassBeanDefinitionReader's loadBeanDefinitionsForBeanMethod():
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// ... code omitted ...
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = CollectionUtils.newHashSet(configCandidates.size());
do {
// ... code omitted ...
parser.parse(candidates);
parser.validate();
// ... code omitted ...
this.reader.loadBeanDefinitions(configClasses);
// ... code omitted ...
}
while (!candidates.isEmpty());
// ... code omitted ...
}
The loadBeanDefinitionsForBeanMethod() method in ConfigurationClassBeanDefinitionReader extracts initMethod and destroyMethod information from the @Bean annotation, then registers the Bean definition:
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
// ... code omitted ...
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
// ... code omitted ...
}
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();
// ... code omitted ...
ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName);
String initMethodName = bean.getString("initMethod");
if (StringUtils.hasText(initMethodName)) {
beanDef.setInitMethodName(initMethodName);
}
String destroyMethodName = bean.getString("destroyMethod");
beanDef.setDestroyMethodName(destroyMethodName);
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}
Spring Boot Auto-Configuration Principle
For a Spring Boot application, the @SpringBootApplication annotation is a composite annotation that includes @EnableAutoConfiguration, which is the key to auto-configuration:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
}
Similar to @EnableWebMvc, the @EnableAutoConfiguration annotation uses @Import to introduce a class called AutoConfigurationImportSelector. However, this class does not have a @Configuration annotation; instead, it implements the ImportSelector interface:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
public class AutoConfigurationImportSelector implements DeferredImportSelector,
BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
}
In the processImports() method of ConfigurationClassParser, there is a branch that checks whether the class imported via @Import implements the DeferredImportSelector interface. If it does, the DeferredImportSelectorHandler's handle() method is invoked:
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> filter, boolean checkForCircularImports) {
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (selector instanceof DeferredImportSelector deferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector);
}
}
} finally {
this.importStack.pop();
}
}
}
The handle() method of DeferredImportSelectorHandler simply adds the current class to its deferredImportSelectors property:
private class DeferredImportSelectorHandler {
@Nullable
private List<DeferredImportSelectorHolder> deferredImportSelectors = new ArrayList<>();
void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
this.deferredImportSelectors.add(holder);
}
}
}
Finally, in the parse() method of ConfigurationClassParser, its process() method is called. In the process() method of DeferredImportSelectorHandler, the processGroupImport() method of DeferredImportSelectorHolder is invoked:
public void parse(Set<BeanDefinitionHolder> configCandidates) {
// ... code omitted ...
this.deferredImportSelectorHandler.process();
}
void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
if (this.deferredImportSelectors == null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
handler.register(holder);
handler.processGroupImports();
}
else {
this.deferredImportSelectors.add(holder);
}
}
private class DeferredImportSelectorGroupingHandler {
@SuppressWarnings("NullAway")
void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
Predicate<String> filter = grouping.getCandidateFilter();
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass, filter),
Collections.singleton(asSourceClass(entry.getImportClassName(), filter)),
filter, false);
}
// ... code omitted ...
});
}
}
}
This then calls the process() method of AutoConfigurationGroup, which invokes AutoConfigurationImportSelector's getAutoConfigurationEntry() method—the class introduced via @EnableAutoConfiguration:
private static final class AutoConfigurationGroup
implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
// ... code omitted ...
AutoConfigurationEntry autoConfigurationEntry = autoConfigurationImportSelector
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
}
In AutoConfigurationImportSelector's getAutoConfigurationEntry() method, it calls ImportCandidates to load configuration classes from the default file org.springframework.boot.autoconfigure.AutoConfiguration.imports, then filters out configuration classes that don't meet the conditions. The filtering method can check whether certain classes exist in the CLASSPATH:
public class AutoConfigurationImportSelector {
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// ... code omitted ...
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation,
getBeanClassLoader());
return configurations;
}
}
public final class ImportCandidates implements Iterable<String> {
private static final String LOCATION = "META-INF/spring/%s.imports";
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format(LOCATION, annotation.getName());
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}
}
The org.springframework.boot.autoconfigure.AutoConfiguration.imports file contains entries like:
Taking WebMvcAutoConfiguration as an example, its filtering condition is that the classes Servlet, DispatcherServlet, and WebMvcConfigurer must exist in the CLASSPATH. If these classes exist, the WebMvcAutoConfiguration configuration class will be parsed, and its configured Beans will be registered:
@AutoConfiguration(after = {
DispatcherServletAutoConfiguration.class,
TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class
})
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
}