精盡Spring Boot原始碼分析 - 剖析 @SpringBootApplication 註解

月圓吖發表於2021-07-05

該系列文章是筆者在學習 Spring Boot 過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 Spring Boot 原始碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 原始碼有一定的瞭解,可以先檢視我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點選一下“推薦”,也可以關注博主,感激不盡~

該系列其他文章請檢視:《精盡 Spring Boot 原始碼分析 - 文章導讀》

概述

現如今,Spring Boot 在許多中大型企業中被普及,想必大家對於 @SpringBootApplication 並不陌生,這個註解通常標註在我們應用的啟動類上面,標記是一個 Spring Boot 應用,同時開啟自動配置的功能,那麼你是否有深入瞭解過該註解呢?沒有的話,或許這篇文章可以讓你對它有一個新的認識。

提示:@EnableAutoConfiguration 是開啟自動配置功能的模組驅動註解,是 Spring Boot 的核心註解

整篇文章主要是對這個註解,也就是 Spring Boot 的自動配置功能進行展述

@SpringBootApplication

org.springframework.boot.autoconfigure.SpringBootApplication 註解在 Spring Boot 的 spring-boot-autoconfigre 子模組下,當我們引入 spring-boot-starter 模組後會自動引入該子模組

該註解是一個組合註解,如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited // 表明該註解定義在某個類上時,其子類會繼承該註解
@SpringBootConfiguration // 繼承 `@Configuration` 註解
@EnableAutoConfiguration // 開啟自動配置功能
// 掃描指定路徑下的 Bean
@ComponentScan( excludeFilters = {
    			// 預設沒有 TypeExcludeFilter
				@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    			// 排除掉自動配置類
				@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
	/**
	 * 需要自動配置的 Class 類
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	/**
	 * 需要自動配置的類名稱
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	/**
	 * 需要掃描的路徑
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	/**
	 * 需要掃描的 Class 類
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

	/**
	 * 被標記的 Bean 是否進行 CGLIB 提升
	 */
	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;
}

@SpringBootApplication 註解就是一個組合註解,裡面的每個配置都是元註解中對應的屬性,上面已做描述

該註解上面的 @Inherited 元註解是 Java 提供的,標註後表示當前註解定義在某個類上時,其子類會繼承該註解,我們一起來看看其他三個註解

@SpringBootConfiguration

org.springframework.boot.SpringBootConfiguration 註解,Spring Boot 自定義註解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

    /**
	 * 被標記的 Bean 是否進行 CGLIB 提升
	 */
	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;
}

該註解很簡單,上面標註了 @Configuration 元註解,所以作用相同,同樣是將一個類標註為配置類,能夠作為一個 Bean 被 Spring IoC 容器管理

至於為什麼不直接使用 @Configuration 註解呢,我想這應該是 領域驅動設計 中的一種思想,可以使得 Spring Boot 更加靈活,總有它的用武之地

領域驅動設計:Domain-Driven Design,簡稱 DDD。過去系統分析和系統設計都是分離的,這樣割裂的結果導致需求分析的結果無法直接進行設計程式設計,而能夠進行程式設計執行的程式碼卻扭曲需求,導致客戶執行軟體後才發現很多功能不是自己想要的,而且軟體不能快速跟隨需求變化。DDD 則打破了這種隔閡,提出了領域模型概念,統一了分析和設計程式設計,使得軟體能夠更靈活快速跟隨需求變化。

@ComponentScan

org.springframework.context.annotation.ComponentScan 註解,Spring 註解,掃描指定路徑下的標有 @Component 註解的類,解析成 Bean 被 Spring IoC 容器管理

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

    /**
     * 指定的掃描的路徑
     */
	@AliasFor("basePackages")
	String[] value() default {};

    /**
     * 指定的掃描的路徑
     */
	@AliasFor("value")
	String[] basePackages() default {};

    /**
     * 指定的掃描的 Class 物件
     */
	Class<?>[] basePackageClasses() default {};

	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

	ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

	String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

	boolean useDefaultFilters() default true;

    /**
     * 掃描時的包含過濾器
     */
	Filter[] includeFilters() default {};
	/**
     * 掃描時的排除過濾器
     */
	Filter[] excludeFilters() default {};

	boolean lazyInit() default false;
}

想深入瞭解該註解的小夥伴可以檢視我前面對 Spring IoC 進行原始碼分析的文章:

該註解通常需要和 @Configuration 註解一起使用,因為需要先被當做一個配置類,然後解析到上面有 @ComponentScan 註解後則處理該註解,通過 ClassPathBeanDefinitionScanner 掃描器去掃描指定路徑下標註了 @Component 註解的類,將他們解析成 BeanDefinition(Bean 的前身),後續則會生成對應的 Bean 被 Spring IoC 容器管理

當然,如果該註解沒有通過 basePackages 指定路徑,Spring 會選在以該註解標註的類所在的包作為基礎路徑,然後掃描包下面的這些類

@EnableAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration 註解,Spring Boot 自定義註解,用於驅動 Spring Boot 自動配置模組

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage // 註冊一個 Bean 儲存當前註解標註的類所在包路徑
@Import(AutoConfigurationImportSelector.class) // Spring Boot 自動配置的實現
public @interface EnableAutoConfiguration {
	/**
	 * 可通過這個配置關閉 Spring Boot 的自動配置功能
	 */
	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * 指定需要排除的自動配置類的 Class 物件
	 */
	Class<?>[] exclude() default {};

	/**
	 * 指定需要排除的自動配置類的名稱
	 */
	String[] excludeName() default {};
}

對於 Spring 中的模組驅動註解的實現都是通過 @Import 註解來實現的

模組驅動註解通常需要結合 @Configuration 註解一起使用,因為需要先被當做一個配置類,然後解析到上面有 @Import 註解後則進行處理,對於 @Import 註解的值有三種情況:

  1. 該 Class 物件實現了 ImportSelector 介面,呼叫它的 selectImports(..) 方法獲取需要被處理的 Class 物件的名稱,也就是可以將它們作為一個 Bean 被 Spring IoC 管理

    • 該 Class 物件實現了 DeferredImportSelector 介面,和上者的執行時機不同,在所有配置類處理完後再執行,且支援 @Order 排序
  2. 該 Class 物件實現了 ImportBeanDefinitionRegistrar 介面,會呼叫它的 registerBeanDefinitions(..) 方法,自定義地往 BeanDefinitionRegistry 註冊中心註冊 BeanDefinition(Bean 的前身)

  3. 該 Class 物件是一個 @Configuration 配置類,會將這個類作為一個 Bean 被 Spring IoC 管理

對於 @Import 註解不熟悉的小夥伴可檢視我前面的 《死磕Spring之IoC篇 - @Bean 等註解的實現原理》 這篇文章

這裡的 @EnableAutoConfiguration 自動配置模組驅動註解,通過 @Import 匯入 AutoConfigurationImportSelector 這個類(實現了 DeferredImportSelector 介面)來驅動 Spring Boot 的自動配置模組,下面會進行分析

@AutoConfigurationPackage

我們注意到 @EnableAutoConfiguration 註解上面還有一個 @AutoConfigurationPackage 元註解,它的作用就是註冊一個 Bean,儲存了當前註解標註的類所在包路徑

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
/**
 * 將當前註解所標註的類所在包名封裝成一個 {@link org.springframework.boot.autoconfigure.AutoConfigurationPackages.BasePackages} 進行註冊
 * 例如 JPA 模組的會使用到這個物件(JPA entity scanner)
 */
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage { }

同樣這裡使用了 @Import 註解來實現的,對應的是一個 AutoConfigurationPackages.Registrar 內部類,如下:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 註冊一個 BasePackages 類的 BeanDefinition,角色為內部角色,名稱為 `org.springframework.boot.autoconfigure.AutoConfigurationPackages`
        register(registry, new PackageImport(metadata).getPackageName());
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        // 將註解元資訊封裝成 PackageImport 物件,對註解所在的包名進行封裝
        return Collections.singleton(new PackageImport(metadata));
    }
}

比較簡單,這裡直接跳過了

自動配置

在開始之前,我們先來了解一下 Spring Boot 的自動配置,就是通過引入某個功能的相關 jar 包依賴後,Spring Boot 能夠自動配置應用程式,讓我們很方便的使用該功能

  • 例如當你引入 spring-boot-starter-aop 後,會自動引入 AOP 相關的 jar 包依賴,那麼在 spring-boot-autoconfigure 中有一個 AopAutoConfiguration 自動配置類會自動驅動整個 AOP 模組

  • 例如當你引入 spring-boot-starter-web 後,會自動引入 Spring MVC、Tomcat 相關的 jar 包依賴,那麼在 spring-boot-autoconfigure 中會有相應的自動配置類會自動配置 Spring MVC

當然,還有許多自動配置類,結合這 Spring Boot 的 Starter 模組,讓許多功能或者第三方 jar 包能夠很簡便的和 Spring Boot 整合在一起使用

現在很多開源框架都提供了對應的 Spring Boot Starter 模組,能夠更好的整合 Spring Boot,當你熟悉自動配置功能後,你也可以很輕鬆的寫一個 Starter 包供他人使用???

這裡先提前劇透一下,自動配置類為什麼在你引入相關 jar 包後會自動配置對應的模組呢?

主要就是擴充了 Spring 的 Condition,例如 @ConditionalOnClass 註解,當存在指定的 Class 物件時才注入某個 Bean

同時也可以再結合 @EnableXxx 模組註解,通過 @Import 註解驅動某個模組

具體細節,請繼續往下看

AutoConfigurationImportSelector

org.springframework.boot.autoconfigure.AutoConfigurationImportSelector,實現了 DeferredImportSelector 介面,是 @EnableAutoConfiguration 註解驅動自動配置模組的核心類

直接看到實現的 ImportSelector 介面的方法

1. selectImports 方法

selectImports(AnnotationMetadata) 方法,返回需要注入的 Bean 的類名稱

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // <1> 如果通過 `spring.boot.enableautoconfiguration` 配置關閉了自動配置功能
    if (!isEnabled(annotationMetadata)) {
        // 返回一個空陣列
        return NO_IMPORTS;
    }
    /**
     * <2> 解析 `META-INF/spring-autoconfigure-metadata.properties` 檔案,生成一個 AutoConfigurationMetadata 自動配置類後設資料物件
     *
     * 說明:引入 `spring-boot-autoconfigure-processor` 工具模組依賴後,其中會通過 Java SPI 機制引入 {@link AutoConfigureAnnotationProcessor} 註解處理器在編譯階段進行相關處理
     * 其中 `spring-boot-autoconfigure` 模組會引入該工具模組(不具有傳遞性),那麼 Spring Boot 在編譯 `spring-boot-autoconfigure` 這個 `jar` 包的時候,
     * 在編譯階段會掃描到帶有 `@ConditionalOnClass` 等註解的 `.class` 檔案,也就是自動配置類,將自動配置類的資訊儲存至 `META-INF/spring-autoconfigure-metadata.properties` 檔案中
     * 例如儲存類 `自動配置類類名.註解簡稱` => `註解中的值(逗號分隔)` 和 `自動配置類類名` => `空字串`
     *
     * 當然,你自己寫的 Spring Boot Starter 中的自動配置模組也可以引入這個 Spring Boot 提供的外掛
     */
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
    // <3> 從所有的 `META-INF/spring.factories` 檔案中找到 `@EnableAutoConfiguration` 註解對應的類(需要自動配置的類)
    // 會進行過濾處理,然後封裝在一個物件中
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
    // <4> 返回所有需要自動配置的類
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

過程如下:

  1. 如果通過 spring.boot.enableautoconfiguration 配置關閉了自動配置功能,那麼直接返回一個空陣列
  2. 解析 META-INF/spring-autoconfigure-metadata.properties 檔案,生成一個 AutoConfigurationMetadata 自動配置類後設資料物件
  3. 呼叫 getAutoConfigurationEntry(..) 方法, 從所有的 META-INF/spring.factories 檔案中找到 @EnableAutoConfiguration 註解對應的類(需要自動配置的類),會進行過濾處理,然後封裝在一個 AutoConfigurationEntry 物件中
  4. 返回所有需要自動配置的類

上面第 2 步呼叫的方法:

final class AutoConfigurationMetadataLoader {

	protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";

	private AutoConfigurationMetadataLoader() {
	}

	static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
		return loadMetadata(classLoader, PATH);
	}

	static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
		try {
			// <1> 獲取所有 `META-INF/spring-autoconfigure-metadata.properties` 檔案 URL
			Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
					: ClassLoader.getSystemResources(path);
			Properties properties = new Properties();
			// <2> 載入這些檔案並將他們的屬性新增到 Properties 中
			while (urls.hasMoreElements()) {
				properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
			}
			// <3> 將這個 Properties 封裝到 PropertiesAutoConfigurationMetadata 物件中並返回
			return loadMetadata(properties);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
		}
	}

	static AutoConfigurationMetadata loadMetadata(Properties properties) {
		return new PropertiesAutoConfigurationMetadata(properties);
	}
}

這一步困惑了我很久,因為在 Spring Boot 工程中根本找不到 META-INF/spring-autoconfigure-metadata.properties 檔案,而我們自己也沒有配置過,但是在我們自己的 Spring Boot 應用依賴的 spring-boot-autoconfigure.jar 包裡面又存在這個檔案,如下:

精盡Spring Boot原始碼分析 - 剖析 @SpringBootApplication 註解

那麼這是為什麼呢?經過我長時間的 Google,找到了答案

  1. 在引入 spring-boot-autoconfigure-processor 工具模組依賴後,其中會通過 Java SPI 機制引入 AutoConfigureAnnotationProcessor 註解處理器在編譯階段進行相關處理

  2. 其中 spring-boot-autoconfigure 模組會引入該工具模組(不具有傳遞性),那麼 Spring Boot 在編譯 spring-boot-autoconfigure 這個 jar 包的時候,在編譯階段會掃描到帶有 @ConditionalOnClass 等註解的 .class 檔案,也就是自動配置類,然後將自動配置類的一些資訊儲存至 META-INF/spring-autoconfigure-metadata.properties 檔案中

  3. 檔案中儲存了 自動配置類類名.註解簡稱 --> 註解中的值(逗號分隔)自動配置類類名 --> 空字串

  4. 當然,你自己寫的 Spring Boot Starter 中的自動配置模組也可以引入這個 Spring Boot 提供的外掛

得到的結論:

至於為什麼這麼做,是因為 Spring Boot 提供的自動配置類比較多,而我們不可能使用到很多自動配置功能,大部分都沒必要,如果每次你啟動應用的過程中,都需要一個一個去解析他們上面的 Conditional 註解,那麼肯定會有不少的效能損耗

這裡,Spring Boot 做了一個優化,通過自己提供的工具,在編譯階段將自動配置類的一些註解資訊儲存在一個 properties 檔案中,這樣一來,在你啟動應用的過程中,就可以直接讀取該檔案中的資訊,提前過濾掉一些自動配置類,相比於每次都去解析它們所有的註解,效能提升不少

2. getAutoConfigurationEntry 方法

getAutoConfigurationEntry(AutoConfigurationMetadata, AnnotationMetadata) 方法,返回符合條件的自動配置類,如下:

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
        AnnotationMetadata annotationMetadata) {
    // <1> 如果通過 `spring.boot.enableautoconfiguration` 配置關閉了自動配置功能
    if (!isEnabled(annotationMetadata)) {
        // 則返回一個“空”的物件
        return EMPTY_ENTRY;
    }
    // <2> 獲取 `@EnableAutoConfiguration` 註解的配置資訊
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // <3> 從所有的 `META-INF/spring.factories` 檔案中找到 `@EnableAutoConfiguration` 註解對應的類(需要自動配置的類)
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    // <4> 對所有的自動配置類進行去重
    configurations = removeDuplicates(configurations);
    // <5> 獲取需要排除的自動配置類
    // 可通過 `@EnableAutoConfiguration` 註解的 `exclude` 和 `excludeName` 配置
    // 也可以通過 `spring.autoconfigure.exclude` 配置
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    // <6> 處理 `exclusions` 中特殊的類名稱,保證能夠排除它
    checkExcludedClasses(configurations, exclusions);
    // <7> 從 `configurations` 中將 `exclusions` 需要排除的自動配置類移除
    configurations.removeAll(exclusions);
    /**
     * <8> 從 `META-INF/spring.factories` 找到所有的 {@link AutoConfigurationImportFilter} 對 `configurations` 進行過濾處理
     * 例如 Spring Boot 中配置了 {@link org.springframework.boot.autoconfigure.condition.OnClassCondition}
     * 在這裡提前過濾掉一些不滿足條件的自動配置類,在 Spring 注入 Bean 的時候也會判斷哦~
     */
    configurations = filter(configurations, autoConfigurationMetadata);
    /**
     * <9> 從 `META-INF/spring.factories` 找到所有的 {@link AutoConfigurationImportListener} 事件監聽器
     * 觸發每個監聽器去處理 {@link AutoConfigurationImportEvent} 事件,該事件中包含了 `configurations` 和 `exclusions`
     * Spring Boot 中配置了一個 {@link org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener}
     * 目的就是將 `configurations` 和 `exclusions` 儲存至 {@link AutoConfigurationImportEvent} 物件中,並註冊到 IoC 容器中,名稱為 `autoConfigurationReport`
     * 這樣一來我們可以注入這個 Bean 獲取到自動配置類資訊
     */
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // <10> 將所有的自動配置類封裝成一個 AutoConfigurationEntry 物件,並返回
    return new AutoConfigurationEntry(configurations, exclusions);
}

過程如下:

  1. 如果通過 spring.boot.enableautoconfiguration 配置關閉了自動配置功能,則返回一個“空”的物件

  2. 獲取 @EnableAutoConfiguration 註解的配置資訊

  3. 從所有的 META-INF/spring.factories 檔案中找到 @EnableAutoConfiguration 註解對應的類(需要自動配置的類),儲存在 configurations 集合中

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        // 從所有的 `META-INF/spring.factories` 檔案中找到 `@EnableAutoConfiguration` 註解對應的類(需要自動配置的類)
        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;
    }
    

    這個 SpringFactoriesLoader 是由 Spring 提供的一個類

  4. 對所有的自動配置類進行去重

    protected final <T> List<T> removeDuplicates(List<T> list) {
        return new ArrayList<>(new LinkedHashSet<>(list));
    }
    
  5. 獲取需要排除的自動配置類,可通過 @EnableAutoConfiguration 註解的 excludeexcludeName 配置,也可以通過 spring.autoconfigure.exclude 配置

  6. 處理 exclusions 中特殊的類名稱,保證能夠排除它

  7. configurations 中將 exclusions 需要排除的自動配置類移除

  8. 呼叫 filter(..) 方法, 目的就是過濾掉一些不符合 Condition 條件的自動配置類,和在 1. selectImports 方法 小節中講到的效能優化有關哦

  9. META-INF/spring.factories 找到所有的 AutoConfigurationImportListener 事件監聽器,觸發每個監聽器去處理 AutoConfigurationImportEvent 事件,該事件中包含了 configurationsexclusions

    Spring Boot 中配置了一個監聽器,目的就是將 configurationsexclusions 儲存至 AutoConfigurationImportEvent 物件中,並註冊到 IoC 容器中,名稱為 autoConfigurationReport,這樣一來我們可以注入這個 Bean 獲取到自動配置類資訊

  10. 將所有的自動配置類封裝成一個 AutoConfigurationEntry 物件,並返回

整個過程不復雜,關鍵在於上面的第 3 步和第 8 步,先從所有的 META-INF/spring.factories 檔案中找到 @EnableAutoConfiguration 註解對應的類(需要自動配置的類),然後進行過濾

3. filter 方法

filter(List<String>, AutoConfigurationMetadata) 方法,過濾一些自動配置類

我們得先知道這兩個入參:

  1. 所有的自動配置類名稱
  2. META-INF/spring-autoconfigure-metadata.properties 檔案儲存的 Spring Boot 的自動配置類的註解元資訊(Sprng Boot 編譯時生成的)

這裡的目的就是根據 2 裡面的註解元資訊,先過濾掉一些自動配置類

private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
    long startTime = System.nanoTime();
    // <1> 將自動配置類儲存至 `candidates` 陣列中
    String[] candidates = StringUtils.toStringArray(configurations);
    boolean[] skip = new boolean[candidates.length];
    boolean skipped = false;
    /*
     * <2> 從 `META-INF/spring.factories` 找到所有的 AutoConfigurationImportFilter 對 `candidates` 進行過濾處理
     * 有 OnClassCondition、OnBeanCondition、OnWebApplicationCondition
     */
    for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
        // <2.1> Aware 回撥
        invokeAwareMethods(filter);
        // <2.2> 對 `candidates` 進行匹配處理,獲取所有的匹配結果
        boolean[] match = filter.match(candidates, autoConfigurationMetadata);
        // <2.3> 遍歷匹配結果,將不匹配的自動配置類至空
        for (int i = 0; i < match.length; i++) {
            if (!match[i]) {
                skip[i] = true;
                candidates[i] = null;
                skipped = true;
            }
        }
    }
    // <3> 如果沒有不匹配的結果則全部返回
    if (!skipped) {
        return configurations;
    }
    // <4> 獲取到所有匹配的自動配置類,並返回
    List<String> result = new ArrayList<>(candidates.length);
    for (int i = 0; i < candidates.length; i++) {
        if (!skip[i]) {
            result.add(candidates[i]);
        }
    }
    return new ArrayList<>(result);
}

過程如下:

  1. 將自動配置類儲存至 candidates 陣列中

  2. META-INF/spring.factories 找到所有的 AutoConfigurationImportFiltercandidates 進行過濾處理

    # Auto Configuration Import Filters
    org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
    org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
    org.springframework.boot.autoconfigure.condition.OnClassCondition,\
    org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
    
    1. Aware 回撥
    2. candidates 進行匹配處理,獲取所有的匹配結果,注意,這裡傳入了 AutoConfigurationMetadata 物件
    3. 遍歷匹配結果,將不匹配的自動配置類至空
  3. 如果沒有不匹配的結果則全部返回

  4. 獲取到所有匹配的自動配置類,並返回

關鍵在於上面的第 2 步,通過 Spring Boot 自己擴充套件的幾個自動配置類過濾器進行過濾,由於這部分內容和 Spring Boot 擴充 Condition 相關,放入下篇文章進行分析

下面我們一起來看看上面 1. selectImports 方法 小節中講到的效能優化,META-INF/spring-autoconfigure-metadata.properties 檔案是如何生成的,檔案的內容又是什麼

AutoConfigureAnnotationProcessor

org.springframework.boot.autoconfigureprocessor.AutoConfigureAnnotationProcessor,Spring Boot 的 spring-boot-autoconfigure-processor 工具模組中的自動配置類的註解處理器,在編譯階段掃描自動配置類的註解元資訊,並將他們儲存至一個 properties 檔案中

@SupportedAnnotationTypes({ "org.springframework.boot.autoconfigure.condition.ConditionalOnClass",
		"org.springframework.boot.autoconfigure.condition.ConditionalOnBean",
		"org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate",
		"org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication",
		"org.springframework.boot.autoconfigure.AutoConfigureBefore",
		"org.springframework.boot.autoconfigure.AutoConfigureAfter",
		"org.springframework.boot.autoconfigure.AutoConfigureOrder" })
public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
	/**
	 * 生成的檔案
	 */
	protected static final String PROPERTIES_PATH = "META-INF/spring-autoconfigure-metadata.properties";
	/**
	 * 儲存指定註解的簡稱和註解全稱之間的對應關係(不可修改)
	 */
	private final Map<String, String> annotations;

	private final Map<String, ValueExtractor> valueExtractors;

	private final Properties properties = new Properties();

	public AutoConfigureAnnotationProcessor() {
		// <1> 建立一個 Map 集合
		Map<String, String> annotations = new LinkedHashMap<>();
		// <1.1> 將指定註解的簡稱和全稱之間的對應關係儲存至第 `1` 步建立的 Map 中
		addAnnotations(annotations);
		// <1.2> 將 `1.1` 的 Map 轉換成不可修改的 UnmodifiableMap 集合,賦值給 `annotations`
		this.annotations = Collections.unmodifiableMap(annotations);
		// <2> 建立一個 Map 集合
		Map<String, ValueExtractor> valueExtractors = new LinkedHashMap<>();
		// <2.1> 將指定註解的簡稱和對應的 ValueExtractor 物件儲存至第 `2` 步建立的 Map 中
		addValueExtractors(valueExtractors);
		// <2.2> 將 `2.1` 的 Map 轉換成不可修改的 UnmodifiableMap 集合,賦值給 `valueExtractors`
		this.valueExtractors = Collections.unmodifiableMap(valueExtractors);
	}
}

AbstractProcessor 是 JDK 1.6 引入的一個抽象類,支援在編譯階段進行處理,在構造器中做了以下事情:

  1. 建立一個 Map 集合

    1. 將指定註解的簡稱和全稱之間的對應關係儲存至第 1 步建立的 Map 中

      protected void addAnnotations(Map<String, String> annotations) {
          annotations.put("ConditionalOnClass", "org.springframework.boot.autoconfigure.condition.ConditionalOnClass");
          annotations.put("ConditionalOnBean", "org.springframework.boot.autoconfigure.condition.ConditionalOnBean");
          annotations.put("ConditionalOnSingleCandidate", "org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate");
          annotations.put("ConditionalOnWebApplication", "org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication");
          annotations.put("AutoConfigureBefore", "org.springframework.boot.autoconfigure.AutoConfigureBefore");
          annotations.put("AutoConfigureAfter", "org.springframework.boot.autoconfigure.AutoConfigureAfter");
          annotations.put("AutoConfigureOrder", "org.springframework.boot.autoconfigure.AutoConfigureOrder");
      }
      
    2. 1.1 的 Map 轉換成不可修改的 UnmodifiableMap 集合,賦值給 annotations

  2. 建立一個 Map 集合

    1. 將指定註解的簡稱和對應的 ValueExtractor 物件儲存至第 2 步建立的 Map 中

      private void addValueExtractors(Map<String, ValueExtractor> attributes) {
          attributes.put("ConditionalOnClass", new OnClassConditionValueExtractor());
          attributes.put("ConditionalOnBean", new OnBeanConditionValueExtractor());
          attributes.put("ConditionalOnSingleCandidate", new OnBeanConditionValueExtractor());
          attributes.put("ConditionalOnWebApplication", ValueExtractor.allFrom("type"));
          attributes.put("AutoConfigureBefore", ValueExtractor.allFrom("value", "name"));
          attributes.put("AutoConfigureAfter", ValueExtractor.allFrom("value", "name"));
          attributes.put("AutoConfigureOrder", ValueExtractor.allFrom("value"));
      }
      
    2. 2.1 的 Map 轉換成不可修改的 UnmodifiableMap 集合,賦值給 valueExtractors

getSupportedSourceVersion 方法

返回支援的 Java 版本

@Override
public SourceVersion getSupportedSourceVersion() {
    // 返回 Java 版本,預設 1.5
    return SourceVersion.latestSupported();
}

process 方法

處理過程

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    // <1> 遍歷上面的幾個 `@Conditional` 註解和幾個定義自動配置類順序的註解,依次進行處理
    for (Map.Entry<String, String> entry : this.annotations.entrySet()) {
        // <1.1> 對支援的註解進行處理,也就是找到所有標註了該註解的類,然後解析出該註解的值,儲存至 Properties
        // 例如 `類名.註解簡稱` => `註解中的值(逗號分隔)` 和 `類名` => `空字串`,將自動配置類的資訊已經對應註解的資訊都儲存起來
        // 避免你每次啟動 Spring Boot 應用都要去解析自動配置類上面的註解,是引入 `spring-boot-autoconfigure` 後可以從 `META-INF/spring-autoconfigure-metadata.properties` 檔案中直接獲取
        // 這麼一想,Spring Boot 設計的太棒了,所以你自己寫的 Spring Boot Starter 中的自動配置模組也可以引入這個 Spring Boot 提供的外掛
        process(roundEnv, entry.getKey(), entry.getValue());
    }
    // <2> 如果處理完成
    if (roundEnv.processingOver()) {
        try {
            // <2.1> 將 Properties 寫入 `META-INF/spring-autoconfigure-metadata.properties` 檔案
            writeProperties();
        }
        catch (Exception ex) {
            throw new IllegalStateException("Failed to write metadata", ex);
        }
    }
    // <3> 返回 `false`
    return false;
}

過程如下:

  1. 遍歷上面的幾個 @Conditional 註解和幾個定義自動配置類順序的註解,依次進行處理

    1. 呼叫 process(..) 過載方法,對支援的註解進行處理,也就是找到所有標註了該註解的類,然後解析出該註解的值,儲存至 Properties
  2. 如果處理完成

    1. 呼叫 writeProperties() 方法,將 Properties 寫入 META-INF/spring-autoconfigure-metadata.properties 檔案

      private void writeProperties() throws IOException {
          if (!this.properties.isEmpty()) {
              FileObject file = this.processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", PROPERTIES_PATH);
              try (OutputStream outputStream = file.openOutputStream()) {
                  this.properties.store(outputStream, null);
              }
          }
      }
      

上面的第 1.1 步處理後的 Properties 包含以下內容:

  • 自動配置類的類名.註解簡稱 --> 註解中的值(逗號分隔)
  • 自動配置類的類名 --> 空字串

通過後續寫入的檔案,避免你每次啟動 Spring Boot 應用都要去解析自動配置類上面的註解,從而提高應用啟動時的效率

這麼一想,Spring Boot 設計的太棒了,所以你自己寫的 Spring Boot Starter 中的自動配置模組也可以引入這個 Spring Boot 提供的外掛

process 過載方法

private void process(RoundEnvironment roundEnv, String propertyKey, String annotationName) {
    // <1> 獲取到這個註解名稱對應的 Java 型別
    TypeElement annotationType = this.processingEnv.getElementUtils().getTypeElement(annotationName);
    if (annotationType != null) {
        // <2> 如果存在該註解,則從 RoundEnvironment 中獲取標註了該註解的所有 Element 元素,進行遍歷
        for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
            // <2.1> 獲取這個 Element 元素 innermost 最深處的 Element
            Element enclosingElement = element.getEnclosingElement();
            // <2.2> 如果最深處的 Element 的型別是 PACKAGE 包,那麼表示這個元素是一個類,則進行處理
            if (enclosingElement != null && enclosingElement.getKind() == ElementKind.PACKAGE) {
                // <2.2.1> 解析這個類上面 `annotationName` 註解的資訊,並儲存至 `properties` 中
                processElement(element, propertyKey, annotationName);
            }
        }
    }
}

過程如下:

  1. 獲取到這個註解名稱對應的 Java 型別
  2. 如果存在該註解,則從 RoundEnvironment 中獲取標註了該註解的所有 Element 元素,進行遍歷
    1. 獲取這個 Element 元素 innermost 最深處的 Element
    2. 如果最深處的 Element 的型別是 PACKAGE 包,那麼表示這個元素是一個類,則進行處理
      1. 呼叫 processElement(..) 方法,解析這個類上面 annotationName 註解的資訊,並儲存至 properties

processElement 方法

private void processElement(Element element, String propertyKey, String annotationName) {
    try {
        // <1> 獲取這個類的名稱
        String qualifiedName = Elements.getQualifiedName(element);
        // <2> 獲取這個類上面的 `annotationName` 型別的註解資訊
        AnnotationMirror annotation = getAnnotation(element, annotationName);
        if (qualifiedName != null && annotation != null) {
            // <3> 獲取這個註解中的值
            List<Object> values = getValues(propertyKey, annotation);
            // <4> 往 `properties` 中新增 `類名.註解簡稱` => `註解中的值(逗號分隔)`
            this.properties.put(qualifiedName + "." + propertyKey, toCommaDelimitedString(values));
            // <5> 往 `properties` 中新增 `類名` => `空字串`
            this.properties.put(qualifiedName, "");
        }
    }
    catch (Exception ex) {
        throw new IllegalStateException("Error processing configuration meta-data on " + element, ex);
    }
}

過程如下:

  1. 獲取這個類的名稱

  2. 獲取這個類上面的 annotationName 型別的註解資訊

    private AnnotationMirror getAnnotation(Element element, String type) {
        if (element != null) {
            for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
                if (type.equals(annotation.getAnnotationType().toString())) {
                    return annotation;
                }
            }
        }
        return null;
    }
    
  3. 獲取這個註解中的值

    private List<Object> getValues(String propertyKey, AnnotationMirror annotation) {
        // 獲取該註解對應的 value 抽取器
        ValueExtractor extractor = this.valueExtractors.get(propertyKey);
        if (extractor == null) {
            return Collections.emptyList();
        }
        // 獲取這個註解中的值,並返回
        return extractor.getValues(annotation);
    }
    
  4. properties 中新增 類名.註解簡稱 --> 註解中的值(逗號分隔)

  5. properties 中新增 類名 --> 空字串

總結

本文分析了 @SpringBootApplication 組合註解,它是 @SpringBootConfiguration@ComponentScan@EnableAutoConfiguration 幾個註解的組合註解,對於前兩個註解我想你並不陌生,分析 Spring 原始碼的時候差不多已經講過,最後一個註解則是 Spring Boot 自動配置功能的驅動註解,也是本文講述的一個重點。

@EnableAutoConfiguration 註解的實現原理並不複雜,藉助於 @Import 註解,從所有 META-INF/spring.factories 檔案中找到 org.springframework.boot.autoconfigure.EnableAutoConfiguration 對應的值,例如:

精盡Spring Boot原始碼分析 - 剖析 @SpringBootApplication 註解

會將這些自動配置類作為一個 Bean 嘗試注入到 Spring IoC 容器中,注入的時候 Spring 會通過 @Conditional 註解判斷是否符合條件,因為並不是所有的自動配置類都滿足條件。當然,Spring Boot 對 @Conditional 註解進行了擴充套件,例如 @ConditionalOnClass 可以指定必須存在哪些 Class 物件才注入這個 Bean。

Spring Boot 會藉助 spring-boot-autoconfigure-processor 工具模組在編譯階段將自己的自動配置類的註解元資訊儲存至一個 properties 檔案中,避免每次啟動應用都要去解析這麼多自動配置類上面的註解。同時會通過幾個過濾器根據這個 properties 檔案過濾掉一些自動配置類,具體怎麼過濾的會在下面文章講到。

好了,總結下來就是 Spring Boot 會從 META-INF/spring.factories 檔案中找到配置的自動配置類,然後根據 Condition 條件進行注入,如果注入的話則可以通過 @EnableXxx 模組驅動註解驅動某個模組,例如 Spring AOP 模組。那麼關於 Spring Boot 對 Spring Condition 的擴充套件在下篇文章進行分析。

相關文章