1. @SpringBootApplication

To talk about the auto-configuration of Spring Boot, it is important to start with the project’s starter class @SpringBootApplication, which is the starting point of the whole Spring Boot universe. The definition of this annotation is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@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 {

}

As can be seen, the @SpringBootApplication annotation combines the functionality of several common annotations, of which:

  • The first four are meta annotations, which we will not discuss here.
  • The fifth @SpringBootConfiguration is a configuration class support annotation, which we won’t discuss here either.
  • The sixth @EnableAutoConfiguration is an annotation to enable auto-configuration, which is what we’re going to talk about today.
  • The seventh @ComponentScan is a package scan annotation, and it’s the reason why Beans in a Spring Boot project are automatically scanned if they are in the right place.

There are only two annotations provided by Spring Boot, @SpringBootConfiguration and @EnableAutoConfiguration, the others have been around for years before Spring Boot came along.

2. @EnableAutoConfiguration

Next let’s see how @EnableAutoConfiguration implements auto-configuration.

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

}

There are 2 core annotations on this annotation:

  1. @AutoConfigurationPackage: This means that it automatically scans for various third-party annotations, which are not explained here.
  2. @Import is importing the AutoConfigurationImportSelector configuration class, which is used to load various auto-configuration classes.

3. AutoConfigurationImportSelector

The AutoConfigurationImportSelector class has a lot of methods, but the entry point is the process method, so we’ll start with the process method here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
    Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
            () -> String.format("Only %s implementations are supported, got %s",
                    AutoConfigurationImportSelector.class.getSimpleName(),
                    deferredImportSelector.getClass().getName()));
    AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
        .getAutoConfigurationEntry(annotationMetadata);
    this.autoConfigurationEntries.add(autoConfigurationEntry);
    for (String importClassName : autoConfigurationEntry.getConfigurations()) {
        this.entries.putIfAbsent(importClassName, annotationMetadata);
    }
}

As you can see from the class name, the objects related to the auto-configuration are loaded by AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(annotationMetadata); is loaded.

Of course the getAutoConfigurationEntry method here is actually the method provided by the current class, so let’s take a look at that method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

The naming of the methods in the source code here is well done, and you can basically see the name and know what it means.

3.1 isEnabled

The isEnabled method is called first to determine if the auto-configuration is enabled or not. This is mainly because even after we have introduced spring-boot-starter-xxx into the project, we can turn off all automation by configuring spring.boot.enableautoconfiguration=false in application.properties. configuration.

The relevant source code is as follows:

1
2
3
4
5
6
protected boolean isEnabled(AnnotationMetadata metadata) {
    if (getClass() == AutoConfigurationImportSelector.class) {
        return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
    }
    return true;
}

3.2 getCandidateConfigurations

Next, call the getCandidateConfigurations method to get all the candidate auto-configuration classes, which come from two main sources:

  1. all the auto-configuration classes defined in claspath\:META-INF/spring.factories, which is source one.
  2. the auto-configuration classes that come with Spring Boot. Spring Boot comes with its own auto-configuration classes located in spring-boot-autoconfigure-3.0.6.jar!\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports file.

The relevant source code is as follows:

1
2
3
4
5
6
7
8
9
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = new ArrayList<>(
            SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
    ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
    Assert.notEmpty(configurations,
            "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                    + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

The full path to the auto-configuration classes loaded here are stored in the configurations object, which has two places to get them from:

  1. call the SpringFactoriesLoader.loadFactoryNames method to get it, which is relatively simple and essentially loads the META-INF/spring.facts file, which defines the full paths to a large number of auto-configuration classes.
  2. call the ImportCandidates.load method to load, this is to load spring-boot-autoconfigure-3.0.6.jar!\META-INF\spring\org.springframework.boot.Autoconfigure.AutoConfiguration.imports file with the auto-configuration classes.

If no auto-configuration class is loaded in either of these places, then an exception is thrown.

3.3 removeDuplicates

The removeDuplicates method removes duplicate classes from a candidate auto-configuration class. The source code is as follows:

1
2
3
protected final <T> List<T> removeDuplicates(List<T> list) {
    return new ArrayList<>(new LinkedHashSet<>(list));
}

3.4 getExclusions

The getExclusions method indicates that all excluded auto-configuration classes need to be fetched, and that these excluded auto-configuration classes can be fetched from three places:

  1. the exclude attribute of the current annotation.
  2. the excludeName attribute of the current annotation.
  3. the spring.autoconfigure.exclude property in the application.properties configuration file.

Take a look at the source code:

1
2
3
4
5
6
7
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    Set<String> excluded = new LinkedHashSet<>();
    excluded.addAll(asList(attributes, "exclude"));
    excluded.addAll(asList(attributes, "excludeName"));
    excluded.addAll(getExcludeAutoConfigurationsProperty());
    return excluded;
}

3.5 checkExcludedClasses

This method checks for all excluded automation classes. Since the automation classes in Spring Boot are customizable and do not need to uniformly implement an interface or uniformly inherit from a class, the compilation will not check if you write an excluded class incorrectly, like the following:

1
2
3
4
5
6
@SpringBootApplication(exclude = HelloController.class)
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

Since HelloController is not an auto-configuration class, writing the project this way will throw an exception when it starts, as follows:

1
2
java.langIllegalStatexception: The following classes could not be excluded because they are not auto-configuration classes:
    - org.javaboy.HelloController

Where does this exception come from? It actually comes from the checkExcludedClasses method, which we look at:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
    List<String> invalidExcludes = new ArrayList<>(exclusions.size());
    for (String exclusion : exclusions) {
        if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
            invalidExcludes.add(exclusion);
        }
    }
    if (!invalidExcludes.isEmpty()) {
        handleInvalidExcludes(invalidExcludes);
    }
}
protected void handleInvalidExcludes(List<String> invalidExcludes) {
    StringBuilder message = new StringBuilder();
    for (String exclude : invalidExcludes) {
        message.append("\t- ").append(exclude).append(String.format("%n"));
    }
    throw new IllegalStateException(String.format(
            "The following classes could not be excluded because they are not auto-configuration classes:%n%s",
            message));
}

You can see that in the checkExcludedClasses method, all excluded auto-configuration classes that are located under the current class path but are not included in configurations are first found, and since the ones in configurations are all the auto-configuration classes, those that do not exist in configurations are faulty, and are not auto-configuration classes. These faulty classes are collected and stored in the invalidExcludes variable, and then subjected to additional processing.

The extra handling is to throw an exception in the handleInvalidExcludes method, which is where the previous exception came from.

3.6 removeAll

This method removes the excluded auto-configuration classes from configurations. configurations itself is a List set, and exclusions is a Set set, so it can be removed directly here.

3.7 filter

Now we have loaded all the auto-configuration classes, but not all of them will take effect, depending on whether your project uses specific dependencies.

For example, the auto-configuration loaded now includes RedisAutoConfiguration, which automatically configures Redis, but since my project does not use Redis, this auto-configuration class will not take effect. This process is done by getConfigurationClassFilter().filter(configurations);.

Let’s start with a bit of preparatory knowledge:

Since we have a particularly large number of auto-configuration classes in our project, each one will depend on other classes, and the auto-configuration class will only take effect when the other class exists, a bunch of dependencies on each other that exist in the spring-boot-autoconfigure-3.0.6.jar!/META-INF/spring- autoconfigure-metadata.properties file, and I’ll give you a random example of the configuration in that file:

org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration.ConditionalOnClass=org.springframework.amqp. rabbit.annotation.EnableRabbit means that for the RabbitAnnotationDrivenConfiguration class to take effect, one requirement is the presence of org.springframework.amqp.rabbit. annotation.EnableRabbit.

Let’s take a look at the annotations for the RabbitAnnotationDrivenConfiguration class:

1
2
3
4
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableRabbit.class)
class RabbitAnnotationDrivenConfiguration {
}

This class corresponds to the contents of the configuration file.

Once this preparatory knowledge is understood, the rest is easy to understand.

Let’s start with the getConfigurationClassFilter method, this is where you get all the filters, as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private ConfigurationClassFilter getConfigurationClassFilter() {
    if (this.configurationClassFilter == null) {
        List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
        for (AutoConfigurationImportFilter filter : filters) {
            invokeAwareMethods(filter);
        }
        this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
    }
    return this.configurationClassFilter;
}

As you can see, the filters obtained here are all of type AutoConfigurationImportFilter, and there are only three instances of this type of filter, as follows:

AutoConfigurationImportFilter

From the names of these three instances, you can basically see what each one does:

  • OnClassCondition: This is the condition for the conditional annotation @ConditionalOnClass, which is used to determine whether a class exists under the current classpath.
  • OnWebApplicationCondition: This is the condition for the conditional annotation ConditionalOnWebApplication, and is used to determine whether the current system environment is a Web environment.
  • OnBeanCondition: This is the condition for the conditional annotation @ConditionalOnBean, which determines whether a bean exists on the current system.

The three AutoConfigurationImportFilter filters obtained here are in fact the three above. The filter method is then executed as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
List<String> filter(List<String> configurations) {
    long startTime = System.nanoTime();
    String[] candidates = StringUtils.toStringArray(configurations);
    boolean skipped = false;
    for (AutoConfigurationImportFilter filter : this.filters) {
        boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
        for (int i = 0; i < match.length; i++) {
            if (!match[i]) {
                candidates[i] = null;
                skipped = true;
            }
        }
    }
    if (!skipped) {
        return configurations;
    }
    List<String> result = new ArrayList<>(candidates.length);
    for (String candidate : candidates) {
        if (candidate != null) {
            result.add(candidate);
        }
    }
    return result;
}

Here the three filters are iterated over and their respective match methods are called to match each of the 144 auto-configuration classes. If the conditions required by these automation classes are met, the match array will have a value of true at the index position, otherwise it will be false.

The match array is then iterated through, setting any automation classes that do not satisfy the condition to null, and finally removing these nulls.

This gives us the class we need to automate the configuration.

The last line, fireAutoConfigurationImportEvents, triggers the auto-configuration class import event.

Once these auto-configuration classes have been loaded, it is then up to the various conditional annotations to determine whether they take effect or not.

Reference: https://segmentfault.com/a/1190000043859515