Source code analysis of springboot — implementation principle of autoconfigure

Time:2021-12-9

Source code analysis is based on spring boot 2.1

This paper analyzes the implementation principle of springboot autoconfigure function by reading the source code.
(source code analysis articles are recommended to be read on the PC)

Use the @ enableautoconfiguration annotation in springboot to start the autoconfigure function

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    ...
}

What works here are actually @ import and autoconfigurationimportselector.
@The import annotation is very important. It is the basis of the autoconfiguration function in springboot.

The previous article on analyzing the springboot startup process said that when springboot starts, it will inject configurationclasspostprocessor, which is the class that handles @ import.

ConfigurationClassPostProcessor#postProcessBeanFactory -> ConfigurationClassPostProcessor#processConfigBeanDefinitions

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
    String[] candidateNames = registry.getBeanDefinitionNames();

    for (String beanName : candidateNames) {
        BeanDefinition beanDef = registry.getBeanDefinition(beanName);
        if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
                ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
            }
        }
        else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {    // #1
            configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
        }
    }
    if (configCandidates.isEmpty()) {
        return;
    }

    ...
    
    ConfigurationClassParser parser = new ConfigurationClassParser(
            this.metadataReaderFactory, this.problemReporter, this.environment,
            this.resourceLoader, this.componentScanBeanNameGenerator, registry);    //#2

    Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
    Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
    do {
        parser.parse(candidates);    //#3
        parser.validate();

        Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());    // #4
        configClasses.removeAll(alreadyParsed);

        if (this.reader == null) {
            this.reader = new ConfigurationClassBeanDefinitionReader(
                    registry, this.sourceExtractor, this.resourceLoader, this.environment,
                    this.importBeanNameGenerator, parser.getImportRegistry());
        }
        this.reader.loadBeanDefinitions(configClasses);    // #5
        alreadyParsed.addAll(configClasses);

        candidates.clear();
        if (registry.getBeanDefinitionCount() > candidateNames.length) {
            String[] newCandidateNames = registry.getBeanDefinitionNames();    // #6
            Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
            Set<String> alreadyParsedClasses = new HashSet<>();
            for (ConfigurationClass configurationClass : alreadyParsed) {
                alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
            }
            for (String candidateName : newCandidateNames) {
                if (!oldCandidateNames.contains(candidateName)) {
                    BeanDefinition bd = registry.getBeanDefinition(candidateName);    
                    if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                            !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                        candidates.add(new BeanDefinitionHolder(bd, candidateName));    // #7
                    }
                }
            }
            candidateNames = newCandidateNames;
        }
    }
    while (!candidates.isEmpty());    // #8

    ...
}

#1Check whether the bean is configurationclass. This is mainly to check the class annotation information (the class marked with @ configuration is classified as configurationclass in spring)
#2Building configurationclassparser
#3Resolve configurationclass
#4Get the results. Note that the configurationclassparser #getconfigurationclasses method gets the processing results of configurationclassparser
#5Get the class introduced by configurationclass, convert it into beandefinition, and register it in the spring context
Finally, the construction of bean is completed in the AbstractApplicationContext#refresh method, calling finishBeanFactoryInitialization to build a singleton bean with hot loading.
#6Get new beandefinition list
#7If the previous configurationclass introduces a new configurationclass, it is added to the pending collection
#8Loop processing until the set to be processed is empty

ConfigurationClassParser#parse -> ConfigurationClassParser#processConfigurationClass

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {    // #1
        return;
    }

    ...

    SourceClass sourceClass = asSourceClass(configClass);    // #2
    do {
        sourceClass = doProcessConfigurationClass(configClass, sourceClass);    // #3
    }
    while (sourceClass != null);    // #4

    this.configurationClasses.put(configClass, configClass);    // #5
}

#1Check whether the @ conditional annotation exists in the configurationclass. If so, take the condition judgment class in the annotation for judgment
#2Convert configurationclass to sourceclass
Sourceclass encapsulates the class metadata, which can be compatible with the class loaded by the JVM and the metadata read by the ASM, and obtain the annotation, method and other information in the metadata
#3The doprocessconfigurationclass method is very important. It handles @ component, @ propertysources, @ componentscans, @ import, @ importresource, methods with @ bean, interfaces and parent classes.
#4If the configurationclass has a parent class, doprocessconfigurationclass returns the parent class, where the parent class data is processed recursively
#5Add this configurationclass to configurationclasses so that the configurationclasspostprocessor#processconfigbeandefinitions method#5Step use
This method is the entry for processing configurationclass. If a new configurationclass is introduced into doprocessconfigurationclass, this method will also be called for processing.

ConfigurationClassParser#doProcessConfigurationClass

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
        throws IOException {
    if (configClass.getMetadata().isAnnotated(Component.class.getName())) {    
        processMemberClasses(configClass, sourceClass);    // #1
    }

    ...

    processImports(configClass, sourceClass, getImports(sourceClass), true);    // #2

    AnnotationAttributes importResource =
            AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);    // #3
    if (importResource != null) {
        String[] resources = importResource.getStringArray("locations");
        Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
        for (String resource : resources) {
            String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
            configClass.addImportedResource(resolvedResource, readerClass);
        }
    }

    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);    // #4
    for (MethodMetadata methodMetadata : beanMethods) {
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }

    processInterfaces(configClass, sourceClass);    // #5

    if (sourceClass.getMetadata().hasSuperClass()) {    // #6
        String superclass = sourceClass.getMetadata().getSuperClassName();
        if (superclass != null && !superclass.startsWith("java") &&
                !this.knownSuperclasses.containsKey(superclass)) {
            this.knownSuperclasses.put(superclass, configClass);
            return sourceClass.getSuperClass();
        }
    }

    return null;
}

Only the code related to the autoconfigure function of this method is concerned here
#1If the class has @ Component annotation, the internal class of the class will be queried. If the internal class is also configurationclass, the processconfigurationclass method will be called to process the internal class (note that the @ Component annotation is identified on the @ configuration annotation)
#2Process @ import annotation
#3Process @ importresource, add the imported resource information to the configurationclass #importedresources, configurationclasspostprocessor #processconfigbeandefinitions method#5Step will process
#4Select the method with @ bean annotation in class and add it to configurationclass #beanmethods
#5Select the method with @ bean annotation in the interface and add it to configurationclass#beanmethods as well
#6If there is a parent class, return the parent class to the configurationclassparser#processconfigurationclass and process the parent class recursively.

ConfigurationClassParser#processImports

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
        Collection<SourceClass> importCandidates, 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 (candidate.isAssignable(ImportSelector.class)) {    // #1
                    Class<?> candidateClass = candidate.loadClass();
                    ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
                    ParserStrategyUtils.invokeAwareMethods(
                            selector, this.environment, this.resourceLoader, this.registry);    
                    if (selector instanceof DeferredImportSelector) {    // #2
                        this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                    }
                    else {
                        String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());    // #3
                        Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);    // #4
                        processImports(configClass, currentSourceClass, importSourceClasses, false);    //#5
                    }
                }
                else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {    // #6
                    Class<?> candidateClass = candidate.loadClass();
                    ImportBeanDefinitionRegistrar registrar =
                            BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
                    ParserStrategyUtils.invokeAwareMethods(
                            registrar, this.environment, this.resourceLoader, this.registry);    
                    configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());    //#7
                }
                else {    
                    this.importStack.registerImport(
                            currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                    processConfigurationClass(candidate.asConfigClass(configClass));    //#8
                }
            }
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                    "Failed to process import candidates for configuration class [" +
                    configClass.getMetadata().getClassName() + "]", ex);
        }
        finally {
            this.importStack.pop();
        }
    }
}

#1@The class introduced by import is the importselector implementation class
#2Deferredimportselector interface needs delay processing and is added to deferredimportselectorhandler
The configurationclassparser #parse method finally calls the deferredimportselectorhandler #process method to handle the deferredimportselector interface
#3Call the importselector#selectimports method
#4Use importselector #selectimports to return the class name array and load the corresponding sourceclass
#5Continue processing these sourceclasses using the processimports method
#6@The class introduced by import is the importbeandefinitionregistrar implementation class
#7Add this class to configurationclass #importbeandefinitionregistrars, configurationclasspostprocessor #processconfigbeandefinitions method#5Step will process
#8@The classes introduced by import are other classes, which are forwarded to configurationclass and processed by processconfigurationclass method

There are three uses of @ import annotation. Import selector, importbeandefinitionregistrar or specific configurationclass are introduced.

@At the end of import, processconfigurationclass is required to process the imported configurationclass

Return to the configurationclasspostprocessor #processconfigbeandefinitions method#5Step,
ConfigurationClassBeanDefinitionReader#loadBeanDefinitions -> loadBeanDefinitionsForConfigurationClass

private void loadBeanDefinitionsForConfigurationClass(
        ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

    ...

    if (configClass.isImported()) {
        registerBeanDefinitionForImportedConfigurationClass(configClass);    // #1
    }
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
        loadBeanDefinitionsForBeanMethod(beanMethod);    // #2
    }

    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());    // #3
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());    // #4
}

#1Register the beandefinition of the configurationclass itself
#2Register the @ bean annotation to identify the bean introduced by the method
#3Read beandefinition from the resource introduced by @ importresource
#4Process importbeandefinitionregistrar introduced by @ import

@The autoconfigurationimportselector introduced by the enableautoconfiguration annotation implements the deferredimportselector interface
AutoConfigurationImportSelector#selectImports -> getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
        AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);    // #1
    configurations = removeDuplicates(configurations);    // #2
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);    
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);    // #3
    configurations = filter(configurations, autoConfigurationMetadata);    // #4
    fireAutoConfigurationImportEvents(configurations, exclusions);    // #5
    return new AutoConfigurationEntry(configurations, exclusions);
}

#1Get the configurationclass corresponding to @ enableautoconfiguration from the spring.factories file
#2Configurationclass de duplication
#3Exclude configurationclass for spring.autoconfigure.exclude configuration
#4Use the implementation classes (onbeancondition, onclasscondition, onwebapplicationcondition) of autoconfigurationimportfilter configured in spring.factories to filter some configurationclasses. Here, handle @ conditionalonbean, @ conditionalonclass, @ conditionalonmissingclass and other annotations.
Onclasscondition can judge whether a class exists or does not exist in the current Java environment. The springboot autoconfigure function can be realized. When we introduce a framework jar, we can automatically configure the framework to complete the configuration. It is through this condition that we judge the class implementation.

Let’s take an example, redisautoconfiguration

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
    ...
}

Redisautoconfiguration is a configurationclass that uses the @ bean identification method to introduce other beans.
(redisautoconfiguration has been configured as the configurationclass of @ enableautoconfiguration in the spring.factories file under the spring boot autoconfiguration jar)
@Conditionalonclass indicates that the redisautoconfiguration configuration takes effect only if the redisoperations class exists in the classpath
(after introducing the jar of spring data redis, redisautoconfiguration will take effect.)
@Lettueconnectionconfiguration.class introduced by import and jedisconnectionconfiguration.class are used to establish a connection with redis and generate redisconnectionfactory.
Similarly, lettucconnectionconfiguration takes effect after introducing lettuce related jars, and jedisconnectionconfiguration takes effect after introducing jedis related jars.

If you think this article is good, welcome to pay attention to my WeChat official account, your attention is my insisting power!
Source code analysis of springboot -- implementation principle of autoconfigure