05 Automatic Configuration How to Understand the Principles of Spring Boot Auto Configuration Correctly

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 its selectImports method.
  • If the class implements the DeferredImportSelector interface, the Spring container also instantiates the class and calls its selectImports method. The difference is that the selectImports method of a DeferredImportSelector instance is called later than that of an ImportSelector 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 its registerBeanDefinitions 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.