Spring Boot Starter自动配置的加载原理

7,268 阅读5分钟

Spring Boot Starter的自动配置(Auto-configuration)是个很强大的东东,也是重要且巧妙的设计。不过不把它的原理搞清楚,在判断哪些自动配置会被启用时,会摸不着头脑。下面就从源码的角度来一探究竟。

入门:自动配置的概念

Auto-configuration和Spring的Java Configuration不是一回事哦。具体可看Spring Boot文档中的定义。

Part 0:Spring Boot应用的启动过程

参考文章

Part 1:引入spring.factories

从@SpringBootApplication开始

@SpringBootApplication注解的定义如下:

// 前面略
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

首先看@ComponentScan部分,使用了一个AutoConfigurationExcludeFilter作为exclude filter,从字面上讲,就是包扫描排除掉自动配置的包。So,如果Spring判断某个类是自动配置类,则不会对其进行包扫描(以创建声明的Bean)。具体逻辑不再深究。

这里重点要讲的是@EnableAutoConfiguration这个注解:

// 省略不重要的内容
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

关注@Import(AutoConfigurationImportSelector.class)这句话。@Import我们都很熟悉了,它是XML配置中的<import>的替代。然而AutoConfigurationImportSelector这个类并没有被@Configuration修饰,不是个Java配置类。所以就很奇怪了。

看源码应该优先看注释。

我们来看看@Import的注释透露了什么:

Allows for importing @Configuration classes, ImportSelector and ImportBeanDefinitionRegistrar implementations, as well as regular component classes

嘿!原来它不仅能导入Java配置类,还能处理ImportSelectorImportBeanDefinitionRegistrar

在讲本节的重点ImportSelector之前,还需要提一下@AutoConfigurationPackage

/**
 * Indicates that the package containing the annotated class should be registered with
 * {@link AutoConfigurationPackages}.
 */
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

AutoConfigurationPackages.Registrar正好是个ImportBeanDefinitionRegistrar,它有个registerBeanDefinitions的接口方法,可以直接注册Bean。后面(扩展阅读)可以看到,ImportBeanDefinitionRegistrar是多个滴重要,它是各种"@EnableXX"注解的幕后英雄。

好了,再看看ImportSelector接口:

// 省略不重要注释
/**
 * Interface that determine which @{@link Configuration} class(es) should be imported 
 * based on a given selection criteria, usually one or more annotation attributes.
 */
public interface ImportSelector {
    /**
     * Select and return the names of which class(es) should be imported based on
     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);
}

所以,它是用来决定一个被@Configuration修饰了的Java配置类,应该被导入哪些配置的。依据是这个Java配置类的所有注解信息(保存在AnnotationMetadata里面)。

好,下一步,我们就来看看AutoConfigurationImportSelector如何实现。

AutoConfigurationImportSelector 类

此类对ImportSelector接口的实现如下:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        	return NO_IMPORTS;
    }
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
    	.loadMetadata(this.beanClassLoader);
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
    		annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

其中的getAutoConfigurationEntry方法又调用了getCandidateConfigurations方法,该方法如下:

/**
 * Return the auto-configuration class names that should be considered. By default
 * this method will load candidates using {@link SpringFactoriesLoader}
 */
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;
}

所以,到这里,会从包的META-INF/spring.factories文件中读取需要导入的自动配置类。更细节的代码不再往下贴了,具体可见SpringFactoriesLoader这个类。

接下来的问题是,selectImports是被谁调用的呢?说实话,调用的链路太深了,很容易被绕晕。下面简单地描述一下主要流程。

Part 2:Spring的Java配置类加载过程

从ConfigurationClassPostProcessor说起

作为一个Spring的PostProcessor,这个类的功能是处理有 @Configuration 修饰的Java配置类。关注其核心方法:

// 省略非关键代码
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    ConfigurationClassParser parser = new ConfigurationClassParser(
    	this.metadataReaderFactory, this.problemReporter, this.environment,
    	this.resourceLoader, this.componentScanBeanNameGenerator, registry);
    parser.parse(candidates);
    parser.validate();
    
    Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
    // Read the model and create bean definitions based on its content
    if (this.reader == null) {
    	this.reader = new ConfigurationClassBeanDefinitionReader(
    		registry, this.sourceExtractor, this.resourceLoader, this.environment,
    		this.importBeanNameGenerator, parser.getImportRegistry());
    }
    this.reader.loadBeanDefinitions(configClasses);
}

关键逻辑就是调用ConfigurationClassParser类的parse方法,然后对解析到的所有ConfigurationClass,解析并加载内部定义的Bean。

ConfigurationClassParser#parse()

parse()方法主要是调用processConfigurationClass方法。

// 省略非核心代码
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
    	return;
    }
    
    // Recursively process the configuration class and its superclass hierarchy.
    SourceClass sourceClass = asSourceClass(configClass);
    do {
    	sourceClass = doProcessConfigurationClass(configClass, sourceClass);
    }
    while (sourceClass != null);
    
    this.configurationClasses.put(configClass, configClass);
}

此方法做了三件事:

  1. 判断在PARSE_CONFIGURATION阶段此Java Config类是否应该跳过(根据Conditional判断)
  2. 调用doProcessConfigurationClass()做具体解析
  3. 将方法参数的配置类放入configurationClasses

而第二步的doProcessConfigurationClass()具体解析啥呢?其实就是递归地遍历当前Java 配置类的各种@Import、内部类、声明的@ComponentScan等等,当发现其它Java配置类时,再次调用parse()方法(或者processConfigurationClass()方法),解析那个类。最终的结果是把所有能找到的Java配置类都加入了configurationClasses容器中。

Part 3:再看@SpringBootApplication的加载流程

由于@SpringBootApplication其实也就是一个带有@Configuration的Java配置类,由Part II可知,它也会被按相同的方式解析。

而@SpringBootApplication所包含的注解最终又导入了AutoConfigurationImportSelector类,因此这个类将会被调用到。下面是调用到这个类的getCandidateConfigurations方法(Part I提到过)时的结果截图:

spring.factories结果

到此,所有的Spring Boot Starter中定义的自动配置类都会被扫描到,并将被解析。

补充:@Conditional的实现

在Part 2 中,已经提到processConfigurationClass这个方法的第一行就是在判断Conditioanal条件是否满足。当需要debug某个自动配置为何生效/不生效时,可以重点关注这里。

总结:

@Import是用来导入配置类的,而导入方式主要分为以下三种类型。

  1. 直接导入配置类,被@Configuration修饰的类。
  2. ImportSelector接口的实现类,返回一个配置类名称的数组,然后再导入这些配置类。
  3. ImportBeanDefinitionRegistar接口的实现类,直接在接口方法中注册Bean。

ImportSelector接口的一个实现类AutoConfigurationImportSelector则承包了从ClassPath下各个starter中的META-INF/spring.factories文件中读取需要导入的自动配置类的工作。

@SpringBootApplication注解则间接继承了AutoConfigurationImportSelector的功能。

在扩展阅读中,你将能看到各种@Enable*注解的工作原理。

参考资料