05 Automatic Configuration - How to Understand the Principles of Spring Boot Auto-configuration Correctly #
After the introduction of the previous lessons, I believe you have a comprehensive understanding of the configuration system in Spring Boot. The configuration system in Spring Boot is a powerful and complex system, and the most basic and core part of it is the auto-configuration mechanism. Today, we will discuss this topic in detail and see how Spring Boot achieves auto-configuration. Let’s start with the @SpringBootApplication annotation.
@SpringBootApplication Annotation #
The @SpringBootApplication annotation is located in the org.springframework.boot.autoconfigure package of the spring-boot-autoconfigure project and is defined as follows:
@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 {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
Compared to general annotations, the @SpringBootApplication annotation appears to be a bit complicated. We can use the exclude
and excludeName
attributes to configure the classes or class names that do not need to be auto-configured, and we can use the scanBasePackages
and scanBasePackageClasses
attributes to configure the package paths and class paths that need to be scanned.
Note that the @SpringBootApplication annotation is actually a composite annotation which consists of three annotations: @SpringBootConfiguration, @EnableAutoConfiguration, and @ComponentScan.
- @ComponentScan Annotation
The @ComponentScan annotation is not a new annotation introduced by Spring Boot, but it belongs to the content managed by the Spring container. The @ComponentScan annotation scans all the classes that need to be injected in the package where the classes marked with annotations like @Component are located, and loads the relevant bean definitions into the container in batch. Obviously, this functionality is also required in Spring Boot applications.
- @SpringBootConfiguration Annotation
The @SpringBootConfiguration annotation is relatively simple, in fact, it is an empty annotation that only uses the @Configuration annotation in Spring. The @Configuration annotation is more common and provides the implementation for JavaConfig configuration classes.
- @EnableAutoConfiguration Annotation
The @EnableAutoConfiguration annotation is the object we need to focus on, so we will discuss it in detail below. The definition of this annotation is shown in the following code:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
Here we pay attention to two new annotations, @AutoConfigurationPackage and @Import(AutoConfigurationImportSelector.class).
@AutoConfigurationPackage Annotation #
The @AutoConfigurationPackage annotation is defined as follows:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
In terms of naming, in this annotation, we perform automatic configuration on the classes in the package where this annotation is located, and in terms of implementation, we use the @Import annotation in Spring. When using Spring Boot, the @Import annotation is also a very common annotation that can be used to dynamically create beans. In order to facilitate the understanding of subsequent content, it is necessary to elaborate on the operating mechanism of the @Import annotation. The definition of this annotation is as follows:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
Class<?>[] value();
}
The @Import
annotation can be used to specify the classes that need to be imported. For example, in the @AutoConfigurationPackage
annotation, @Import(AutoConfigurationPackages.Registrar.class)
is used. Depending on the type of the imported class, the Spring container handles @Import
in the following four ways:
- If the class implements the
ImportSelector
interface, the Spring container instantiates the class and calls itsselectImports
method. - If the class implements the
DeferredImportSelector
interface, the Spring container also instantiates the class and calls itsselectImports
method. The difference is that theselectImports
method of aDeferredImportSelector
instance is called later than that of anImportSelector
instance. It is called only after all the relevant business in the@Configuration
annotation is handled. - If the class implements the
ImportBeanDefinitionRegistrar
interface, the Spring container instantiates the class and calls itsregisterBeanDefinitions
method. - If the class does not implement any of the three interfaces mentioned above, the Spring container directly instantiates the class.
With a basic understanding of the @Import
annotation, let’s take a look at the AutoConfigurationPackages.Registrar
class defined below:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
In this example, the Registrar
class implements the ImportBeanDefinitionRegistrar
interface and overrides the registerBeanDefinitions
method. In this method, it calls the register
method of the AutoConfigurationPackages
itself:
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
} else {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
The logic of this method is to first check if the bean has been registered. If it has, it retrieves the definition of the bean, gets the constructor arguments, and adds the parameter values. If it hasn’t been registered, it creates a new definition of the bean, sets the type of the bean to AutoConfigurationPackages
, and registers the bean.
AutoConfigurationImportSelector #
Now let’s take a look at the @Import(AutoConfigurationImportSelector.class)
part in the @EnableAutoConfiguration
annotation. First, we need to understand that the AutoConfigurationImportSelector
class implements the DeferredImportSelector
interface mentioned earlier, so the selectImports
method is executed:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// Get the configurations collection
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return StringUtils.toStringArray(configurations);
}
The core of this code is to get the configurations
collection through the getCandidateConfigurations
method and filter it. The getCandidateConfigurations
method is as follows:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
In this method, configurations
is obtained by loading the factory names from META-INF/spring.factories
. If no auto configuration classes are found, an exception is thrown.
}
Please note that this code is incomplete and cannot be executed. It seems to be an excerpt from a larger codebase.
return locator;
}
Here, two @ConditionalOn annotations are used, one is @ConditionalOnMissingBean and the other is @ConditionalOnProperty. For example, in the server-side code project spring-cloud-config-server of Spring Cloud Config, there is a ConfigServerAutoConfiguration auto-configuration class:
```java
@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)
@Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class,
ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {
}
Here, the @ConditionalOnBean annotation is used. In fact, Spring Boot provides a series of conditional annotations, including:
- @ConditionalOnProperty: Instantiate the bean only when the provided property is true
- @ConditionalOnBean: Instantiate the bean only when a certain object exists in the current context
- @ConditionalOnClass: Instantiate the bean only when a certain Class is present in the classpath
- @ConditionalOnExpression: Instantiate the bean only when the expression is true
- @ConditionalOnMissingBean: Instantiate the bean only when a certain object does not exist in the current context
- @ConditionalOnMissingClass: Instantiate the bean only when a certain Class is not present in the classpath
- @ConditionalOnNotWebApplication: Instantiate the bean only when it is not a web application
Of course, Spring Boot also provides some less commonly used @ConditionalOnXXX annotations, which are defined in the org.springframework.boot.autoconfigure.condition package.
Obviously, in the ConfigServicePropertySourceLocator class mentioned above, the instantiation only happens when the “spring.cloud.config.enabled” property is true (by default, true is assumed when the matchIfMissing configuration is not set) and the ConfigServicePropertySourceLocator does not exist in the classpath. Similarly, the ConfigServerAutoConfiguration is instantiated only when the ConfigServerConfiguration.Marker class exists in the classpath, which is a common technique for controlling auto-configuration.
Implementation Principle of @ConditionalOn Series Conditional Annotations #
There are many @ConditionalOn series conditional annotations, and it’s not necessary to explain all of them. In fact, these annotations have similar implementation principles. Understanding one of them can help understand the others. Here, we will choose the @ConditionalOnClass annotation to explain. The annotation is defined as follows:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}
It can be seen that the @ConditionalOnClass annotation itself has two attributes, one of type Class called value, and one of type String called name. Therefore, we can use either of the two methods to use this annotation. Additionally, the ConditionalOnClass annotation itself has a @Conditional(OnClassCondition.class) annotation. So, the judgment conditions of the ConditionalOnClass annotation are actually contained in the OnClassCondition class.
OnClassCondition is a subclass of SpringBootCondition, and SpringBootCondition implements the Condition interface. The Condition interface has only one matches method, as shown below:
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
The matches method in SpringBootCondition is implemented as follows:
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
// Omitted other methods
}
Here, the getClassOrMethodName method gets the name of the class or method annotated with @ConditionalOnClass. Then, the getMatchOutcome method is used to get the matching output. We can see that the getMatchOutcome method is actually an abstract method that needs to be implemented by each subclass of SpringBootCondition. In this case, the subclass would be the OnClassCondition class. When understanding the OnClassCondition class, it is important to know that in Spring Boot, the @ConditionalOnClass or @ConditionalOnMissingClass annotations correspond to the OnClassCondition class, so the getMatchOutcome in OnClassCondition handles both cases. Here, we have selected the code that handles the @ConditionalOnClass annotation, and the core logic is as follows:
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {
List<String> missing = getMatches(onClasses, MatchType.MISSING, classLoader);
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
.didNotFind("required class", "required classes")
.items(Style.QUOTE, missing));
}
matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
.found("required class", "required classes").items(Style.QUOTE, getMatches(onClasses, MatchType.PRESENT, classLoader));
}
Two methods are worth noting here, one is the getCandidates method, and the other is the getMatches method. First, the getCandidates method obtains the name and value attributes of ConditionalOnClass. Then, the getMatches method compares these attribute values and determines the classes specified by these attributes that do not exist in the class loader. If a class should exist in the class loader but in fact does not, then a matching failure ConditionOutcome is returned. On the other hand, if the class specified exists in the class loader, the matching information is recorded and a ConditionOutcome is returned.
From Source Code Analysis to Daily Development #
In today’s content, we have touched on the core topic of Spring Boot development, which is auto-configuration. Auto-configuration is a key element in understanding the construction and operation of Spring Boot applications. When trying to understand a tool or framework developed based on Spring Boot, today’s content can help you quickly delve into the implementation principles of that tool or framework. Also, in daily development, SPI mechanism and @ConditionalOn series conditional annotations can be directly applied to our own system design and development, providing highly extensible architecture implementation solutions.
Summary and Preview #
It can be said that auto-configuration is the most fundamental and core functionality of Spring Boot, and the @SpringBootApplication annotation is the entry point for Spring Boot applications. In this lesson, we started with the @SpringBootApplication annotation and analyzed the implementation process of auto-configuration in detail. It involves multiple knowledge points, including the SPI mechanism in JDK and the @ConditionalOn series conditional annotations. You need to analyze and grasp them.
Tomorrow, we will continue to learn about other features and techniques of Spring Boot, and explore more advanced and practical topics.