精盡Spring Boot原始碼分析 - Condition 介面的擴充套件

月圓吖發表於2021-07-07

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

Spring Boot 版本:2.2.x

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

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

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

概述

在上一篇《剖析 @SpringBootApplication 註解》文章分析了 Spring Boot 的自動配置功能,在通過 @EnableAutoConfiguration 註解驅動整個自動配置模組的過程中,並不是所有的自動配置類都需要被注入,不同的自動配置類需要滿足一定條件後,才應該進行自動配置

那麼 Spring Boot 怎麼知道滿足一定條件呢?Spring Boot 對 Spring 的 Condition 介面進行了擴充套件,然後結合自定義的註解,則可以判斷自動配置類是否符合條件。

例如 @ConditionalOnClass 可以指定必須存在哪些 Class 物件才注入這個 Bean。

那麼接下來,我們一起來看看 Spring 的 Condition 介面以及 Spring Boot 對其的擴充套件

Condition 演進史

Profile 的出場

在 Spring 3.1 的版本,為了滿足不同環境註冊不同的 Bean ,引入了 @Profile 註解。例如:

@Configuration
public class DataSourceConfiguration {

    @Bean
    @Profile("DEV")
    public DataSource devDataSource() {
        // ... 單機 MySQL
    }

    @Bean
    @Profile("PROD")
    public DataSource prodDataSource() {
        // ... 叢集 MySQL
    }
}
  • 在測試環境下,我們註冊單機 MySQL 的 DataSource Bean
  • 在生產環境下,我們註冊叢集 MySQL 的 DataSource Bean

Spring 3.1.x 的 @Profile 註解如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Profile {

	/**
	 * The set of profiles for which this component should be registered.
	 */
	String[] value();
}

可以看到,最開始 @Profile 註解並沒有結合 @Conditional 註解一起使用,而是在後續版本才引入的

Condition 的出現

在 Spring 4.0 的版本,出現了 Condition 功能,體現在 org.springframework.context.annotation.Condition 介面,如下:

@FunctionalInterface
public interface Condition {

	/**
	 * Determine if the condition matches.
	 * @param context the condition context
	 * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
	 * or {@link org.springframework.core.type.MethodMetadata method} being checked
	 * @return {@code true} if the condition matches and the component can be registered,
	 * or {@code false} to veto the annotated component's registration
	 */
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

函式式介面,只有一個 matches(..) 方法,判斷是否匹配,從入參中可以知道,它是和註解配合一起實現 Condition 功能的,也就是 @Conditional 註解,如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

	/**
	 * All {@link Condition} classes that must {@linkplain Condition#matches match}
	 * in order for the component to be registered.
	 */
	Class<? extends Condition>[] value();
}

隨之 @Profile 註解也進行了修改,和 @Conditional 註解配合使用

Spring 5.1.x 的 @Profile 註解如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

	/**
	 * The set of profiles for which the annotated component should be registered.
	 */
	String[] value();
}

這裡指定的的 Condition 實現類是 ProfileCondition,如下:

class ProfileCondition implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
			for (Object value : attrs.get("value")) {
				if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
					return true;
				}
			}
			return false;
		}
		return true;
	}
}

邏輯很簡答,從當前 Spring 應用上下文的 Environment 中判斷 @Profile 指定的環境是否被啟用,被啟用了表示匹配成功,則注入對應的 Bean,否則,不進行操作

但是 Spring 本身提供的 Condition 實現類不多,只有一個 ProfileCondition 物件

SpringBootCondition 的進擊

Spring Boot 為了滿足更加豐富的 Condition 場景,對 Spring 的 Condition 介面進行了擴充套件,提供更多的實現類,如下:

精盡Spring Boot原始碼分析 - Condition 介面的擴充套件

上面僅列出了部分 SpringBootCondition 的子類,同時這些子類與對應的註解配置一起使用

  • @ConditionalOnClass:必須都存在指定的 Class 物件們
  • @ConditionalOnMissingClass:指定的 Class 物件們必須都不存在
  • @ConditionalOnBean:必須都存在指定的 Bean 們
  • @ConditionalOnMissingBean:指定的 Bean 們必須都不存在
  • @ConditionalOnSingleCandidate:必須存在指定的 Bean
  • @ConditionalOnProperty:指定的屬性是否有指定的值
  • @ConditionalOnWebApplication:當前的 WEB 應用型別是否在指定的範圍內(ANY、SERVLET、REACTIVE)
  • @ConditionalOnNotWebApplication:不是 WEB 應用型別

上面列出了 Spring Boot 中常見的幾種 @ConditionXxx 註解,他們都配合 @Conditional 註解與對應的 Condition 實現類一起使用,提供了非常豐富的 Condition 場景

Condition 在哪生效?

Spring 提供了 Condition 介面以及 @Conditional 註解,那麼在 Spring 中哪裡體現,或者說是哪裡進行判斷的呢?

其實我在 《死磕Spring之IoC篇 - @Bean 等註解的實現原理》 這篇文章中有提到過,我們稍微回顧一下,有兩種情況:

  • 通過 @Component 註解(及派生註解)標註的 Bean
  • @Configuration 標註的配置類中的 @Bean 標註的方法 Bean

普通 Bean

第一種情況是在 Spring 掃描指定路徑下的 .class 檔案解析成對應的 BeanDefinition(Bean 的前身)時,會根據 @Conditional 註解判斷是否符合條件,如下:

// ClassPathBeanDefinitionScanner.java
public int scan(String... basePackages) {
    // <1> 獲取掃描前的 BeanDefinition 數量
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

    // <2> 進行掃描,將過濾出來的所有的 .class 檔案生成對應的 BeanDefinition 並註冊
    doScan(basePackages);

    // Register annotation config processors, if necessary.
    // <3> 如果 `includeAnnotationConfig` 為 `true`(預設),則註冊幾個關於註解的 PostProcessor 處理器(關鍵)
    // 在其他地方也會註冊,內部會進行判斷,已註冊的處理器不會再註冊
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }

    // <4> 返回本次掃描註冊的 BeanDefinition 數量
    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
// ClassPathScanningCandidateComponentProvider.java
private boolean isConditionMatch(MetadataReader metadataReader) {
    if (this.conditionEvaluator == null) {
        this.conditionEvaluator =
                new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver);
    }
    return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata());
}

上面只是簡單的提一下,可以看到會通過 ConditionEvaluator 計算器進行計算,判斷是否滿足條件

配置類

第二種情況是 Spring 會對 配置類進行處理,掃描到帶有 @Bean 註解的方法,嘗試解析成 BeanDefinition(Bean 的前身)時,會根據 @Conditional 註解判斷是否符合條件,如下:

// ConfigurationClassBeanDefinitionReader.java
private void loadBeanDefinitionsForConfigurationClass(
        ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

    // <1> 如果不符合 @Conditional 註解的條件,則跳過
    if (trackedConditionEvaluator.shouldSkip(configClass)) {
        String beanName = configClass.getBeanName();
        if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
            this.registry.removeBeanDefinition(beanName);
        }
        this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
        return;
    }

    // <2> 如果當前 ConfigurationClass 是通過 @Import 註解被匯入的
    if (configClass.isImported()) {
        // <2.1> 根據該 ConfigurationClass 物件生成一個 BeanDefinition 並註冊
        registerBeanDefinitionForImportedConfigurationClass(configClass);
    }
    // <3> 遍歷當前 ConfigurationClass 中所有的 @Bean 註解標註的方法
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
        // <3.1> 根據該 BeanMethod 物件生成一個 BeanDefinition 並註冊(注意這裡有無 static 修飾會有不同的配置)
        loadBeanDefinitionsForBeanMethod(beanMethod);
    }

    // <4> 對 @ImportResource 註解配置的資源進行處理,對裡面的配置進行解析並註冊 BeanDefinition
    loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
    // <5> 通過 @Import 註解匯入的 ImportBeanDefinitionRegistrar 實現類往 BeanDefinitionRegistry 註冊 BeanDefinition
    // Mybatis 整合 Spring 就是基於這個實現的,可檢視 Mybatis-Spring 專案中的 MapperScannerRegistrar 這個類
    // https://github.com/liu844869663/mybatis-spring/blob/master/src/main/java/org/mybatis/spring/annotation/MapperScannerRegistrar.java
    loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}

上面只是簡單的提一下,可以看到會通過 TrackedConditionEvaluator 計算器進行計算,判斷是否滿足條件

這裡提一下,對於 @Bean 標註的方法,會得到 CGLIB 的提升,也就是返回的是一個代理物件,設定一個攔截器專門對 @Bean 方法進行攔截處理,通過依賴查詢的方式從 IoC 容器中獲取 Bean 物件,如果是單例 Bean,那麼每次都是返回同一個物件,所以當主動呼叫這個方法時獲取到的都是同一個物件。

SpringBootCondition

org.springframework.boot.autoconfigure.condition.SpringBootCondition 抽象類,實現了 Condition 介面,Spring Boot 擴充套件 Condition 的抽象基類,主要用於列印相應的日誌,並記錄每次的匹配結果,如下:

/**
 * Base of all {@link Condition} implementations used with Spring Boot. Provides sensible
 * logging to help the user diagnose what classes are loaded.
 *
 * @author Phillip Webb
 * @author Greg Turnquist
 * @since 1.0.0
 */
public abstract class SpringBootCondition implements Condition {

	private final Log logger = LogFactory.getLog(getClass());

	@Override
	public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		// <1> 從註解元資訊中獲取所標註的`類名`(或者`類名#方法名`)
		String classOrMethodName = getClassOrMethodName(metadata);
		try {
			// <2> 獲取匹配結果(包含匹配訊息),抽象方法,交由子類實現
			ConditionOutcome outcome = getMatchOutcome(context, metadata);
			// <3> 列印匹配日誌
			logOutcome(classOrMethodName, outcome);
			// <4> 向 ConditionEvaluationReport 中記錄本次的匹配結果
			recordEvaluation(context, classOrMethodName, outcome);
			// <5> 返回匹配結果
			return outcome.isMatch();
		}
		catch (NoClassDefFoundError ex) {
			// 丟擲異常
		} catch (RuntimeException ex) {
			// 丟擲異常
		}
	}
}

實現的 Condition 介面方法處理過程如下:

  1. 從註解元資訊中獲取所標註的類名(或者類名#方法名

    private static String getClassOrMethodName(AnnotatedTypeMetadata metadata) {
        if (metadata instanceof ClassMetadata) {
            ClassMetadata classMetadata = (ClassMetadata) metadata;
            return classMetadata.getClassName();
        }
        MethodMetadata methodMetadata = (MethodMetadata) metadata;
        return methodMetadata.getDeclaringClassName() + "#" + methodMetadata.getMethodName();
    }
    
  2. 呼叫 getMatchOutcome(..) 方法,獲取匹配結果(包含匹配訊息),抽象方法,交由子類實現

  3. 列印匹配日誌

    protected final void logOutcome(String classOrMethodName, ConditionOutcome outcome) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(getLogMessage(classOrMethodName, outcome));
        }
    }
    
  4. 向 ConditionEvaluationReport 中記錄本次的匹配結果

    private void recordEvaluation(ConditionContext context, String classOrMethodName, ConditionOutcome outcome) {
        if (context.getBeanFactory() != null) {
            ConditionEvaluationReport.get(context.getBeanFactory()).recordConditionEvaluation(classOrMethodName, this, outcome);
        }
    }
    
  5. 返回匹配結果

SpringBootCondition 的實現類

OnClassCondition

org.springframework.boot.autoconfigure.condition.OnClassCondition,繼承 SpringBootCondition 抽象類,如下:

@Order(Ordered.HIGHEST_PRECEDENCE)
class OnClassCondition extends FilteringSpringBootCondition {
    /**
	 * 該方法來自 {@link SpringBootCondition} 判斷某個 Bean 是否符合注入條件(`@ConditionalOnClass` 和 `ConditionalOnMissingClass`)
	 */
	@Override
	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
		ClassLoader classLoader = context.getClassLoader();
		ConditionMessage matchMessage = ConditionMessage.empty();
		// <1> 獲取這個類上面的 `@ConditionalOnClass` 註解的值
		// 也就是哪些 Class 物件必須存在
		List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
		if (onClasses != null) {
			// <1.1> 找到這些 Class 物件中哪些是不存在的
			List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
			// <1.2> 如果存在不存在的,那麼不符合條件,返回不匹配
			if (!missing.isEmpty()) {
				return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
						.didNotFind("required class", "required classes").items(Style.QUOTE, missing));
			}
			// <1.3> 新增 `@ConditionalOnClass` 滿足條件的匹配資訊
			matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
					.found("required class", "required classes")
					.items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
		}
		// <2> 獲取這個類上面的 `@ConditionalOnMissingClass` 註解的值
		// 也就是這些 Class 物件必須都不存在
		List<String> onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class);
		if (onMissingClasses != null) {
			// <2.1> 找到這些 Class 物件中哪些是存在的
			List<String> present = filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);
			// <2.2> 如果有一個存在,那麼不符合條件,返回不匹配
			if (!present.isEmpty()) {
				return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class)
						.found("unwanted class", "unwanted classes").items(Style.QUOTE, present));
			}
			// <2.3> 新增 `@ConditionalOnMissingClass` 滿足條件的匹配資訊
			matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
					.didNotFind("unwanted class", "unwanted classes")
					.items(Style.QUOTE, filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));
		}
		// <3> 返回符合條件的結果
		return ConditionOutcome.match(matchMessage);
	}
}

判斷是否匹配的過程如下:

  1. 獲取這個類上面的 @ConditionalOnClass 註解的值,也就是哪些 Class 物件必須存在

    1. 找到這些 Class 物件中哪些是不存在的

      protected final List<String> filter(Collection<String> classNames, ClassNameFilter classNameFilter,
              ClassLoader classLoader) {
          // 如果為空,則返回空結果
          if (CollectionUtils.isEmpty(classNames)) {
              return Collections.emptyList();
          }
          List<String> matches = new ArrayList<>(classNames.size());
          // 使用 `classNameFilter` 對 `classNames` 進行過濾
          for (String candidate : classNames) {
              if (classNameFilter.matches(candidate, classLoader)) {
                  matches.add(candidate);
              }
          }
          // 返回匹配成功的 `className` 們
          return matches;
      }
      
    2. 如果存在不存在的,那麼不符合條件,返回不匹配

    3. 新增 @ConditionalOnClass 滿足條件的匹配資訊

  2. 獲取這個類上面的 @ConditionalOnMissingClass 註解的值,也就是這些 Class 物件必須都不存在

    1. 找到這些 Class 物件中哪些是存在的,和上面的 1.1 差不多,只不過這裡傳的是 ClassNameFilter.PRESENT 過濾器
    2. 如果有一個存在,那麼不符合條件,返回不匹配
    3. 新增 @ConditionalOnMissingClass 滿足條件的匹配資訊
  3. 返回符合條件的結果

上面使用到的 ClassNameFilter 如下:

protected enum ClassNameFilter {

    /** 指定類存在 */
    PRESENT {
        @Override
        public boolean matches(String className, ClassLoader classLoader) {
            return isPresent(className, classLoader);
        }
    },

    /** 指定類不存在 */
    MISSING {
        @Override
        public boolean matches(String className, ClassLoader classLoader) {
            return !isPresent(className, classLoader);
        }
    };

    abstract boolean matches(String className, ClassLoader classLoader);

    static boolean isPresent(String className, ClassLoader classLoader) {
        if (classLoader == null) {
            classLoader = ClassUtils.getDefaultClassLoader();
        }
        try {
            // 載入指定類,載入成功表示存在這個類
            resolve(className, classLoader);
            return true;
        }
        catch (Throwable ex) {
            // 載入失敗表示不存在這個類
            return false;
        }
    }
}

邏輯很簡單,就是判斷 Class 物件是否存在或者不存在

其它實現類

關於 SpringBootCondition 其他的實現類邏輯都差不多,感興趣的可以去看看

回顧自動配置

在上一篇《剖析 @SpringBootApplication 註解》 文章分析通過 @EnableAutoConfiguration 註解驅動整個自動配置模組的過程中,會通過指定的 AutoConfigurationImportFilter 對所有的自動配置類進行過濾,滿足條件才進行自動配置

可以回顧一下上一篇文章的 2. getAutoConfigurationEntry 方法 小節和 3. filter 方法 小節

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);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // <10> 將所有的自動配置類封裝成一個 AutoConfigurationEntry 物件,並返回
    return new AutoConfigurationEntry(configurations, exclusions);
}

我們看到第 8 步,呼叫 filter(..) 方法, 目的就是過濾掉一些不符合 Condition 條件的自動配置類

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);
}

可以看到第 2 步,會從 META-INF/spring.factories 中找到對應的 AutoConfigurationImportFilter 實現類對所有的自動配置類進行過濾

# 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

這裡,我們一定要注意到入參中的 AutoConfigurationMetadata 物件,它裡面儲存了 META-INF/spring-autoconfigure-metadata.properties 檔案中 Spring Boot 的自動配置類的註解元資訊(Sprng Boot 編譯時生成的),如何來的請回顧上一篇文章

AutoConfigurationImportFilter

org.springframework.boot.autoconfigure.AutoConfigurationImportFilter 介面,用於過濾掉無需自動引入的自動配置類

/**
 * Filter that can be registered in {@code spring.factories} to limit the
 * auto-configuration classes considered. This interface is designed to allow fast removal
 * of auto-configuration classes before their bytecode is even read.
 *
 * @author Phillip Webb
 * @since 1.5.0
 */
@FunctionalInterface
public interface AutoConfigurationImportFilter {

	boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata);
}

可以看到它的註釋,因為自動配置類會很多,如果無需使用,而建立對應的 Bean(位元組碼)到 JVM 記憶體中,將是一種浪費

精盡Spring Boot原始碼分析 - Condition 介面的擴充套件

可以看到它的最終實現類,都是構建在 SpringBootCondition 之上。? 不過這也很正常,因為 Condition 本身提供的一個功能,就是作為配置類(Configuration)是否能夠符合條件被引入。

FilteringSpringBootCondition

org.springframework.boot.autoconfigure.condition.FilteringSpringBootCondition,繼承 SpringBootCondition 抽象類,實現 AutoConfigurationImportFilter 介面,作為具有 AutoConfigurationImportFilter 功能的 SpringBootCondition 的抽象基類。

abstract class FilteringSpringBootCondition extends SpringBootCondition
		implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {
	/**
	 * 底層 IoC 容器
	 */
	private BeanFactory beanFactory;
	/**
	 * 類載入器
	 */
	private ClassLoader beanClassLoader;

	@Override
	public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
		// <1> 從 Spring 應用上下文中獲取 ConditionEvaluationReport 物件
		ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory);
		// <2> 獲取所有自動配置類的匹配結果,空方法,交由子類實現
		ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
		// <3> 將自動配置類的匹配結果儲存至一個 `boolean[]` 陣列中,並將匹配結果一一儲存至 ConditionEvaluationReport 中
		boolean[] match = new boolean[outcomes.length];
		for (int i = 0; i < outcomes.length; i++) {
			// 注意這裡匹配結果為空也表示匹配成功
			match[i] = (outcomes[i] == null || outcomes[i].isMatch());
			if (!match[i] && outcomes[i] != null) {
				logOutcome(autoConfigurationClasses[i], outcomes[i]);
				if (report != null) {
					report.recordConditionEvaluation(autoConfigurationClasses[i], this, outcomes[i]);
				}
			}
		}
		// <4> 返回所有自動配置類是否滿足條件的結果陣列
		return match;
	}
}

可以看到,這實現方法和 SpringBootCondition 的 match(..) 方法很想,他們的入參不同,不要搞混了,這裡的處理過程如下:

  1. 從 Spring 應用上下文中獲取 ConditionEvaluationReport 物件
  2. 呼叫 getOutcomes(..) 抽象方法,獲取所有自動配置類的匹配結果,空方法,交由子類實現
  3. 將自動配置類的匹配結果儲存至一個 boolean[] 陣列中,並將匹配結果一一儲存至 ConditionEvaluationReport 中
    • 注意這裡匹配結果為空也表示匹配成功
  4. 返回所有自動配置類是否滿足條件的結果陣列

FilteringSpringBootCondition 的實現類

OnClassCondition

org.springframework.boot.autoconfigure.condition.OnClassCondition,繼承 FilteringSpringBootCondition 抽象類,如下:

@Order(Ordered.HIGHEST_PRECEDENCE)
class OnClassCondition extends FilteringSpringBootCondition {

	/**
	 * 該方法來自 {@link AutoConfigurationImportFilter} 判斷這些自動配置類是否符合條件(`@ConditionalOnClass`)
	 */
	@Override
	protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
			AutoConfigurationMetadata autoConfigurationMetadata) {
		// Split the work and perform half in a background thread if more than one
		// processor is available. Using a single additional thread seems to offer the
		// best performance. More threads make things worse.
		// 考慮到自動配置類上面的 `@Conditional` 相關注解比較多,所以採用多執行緒以提升效率。經過測試使用,使用兩個執行緒的效率是最高的,
		// 所以會將 `autoConfigurationClasses` 一分為二進行處理

		// <1> 如果 JVM 可用的處理器不止一個,那麼這裡用兩個執行緒去處理
		if (Runtime.getRuntime().availableProcessors() > 1) {
			// <1.1> 對 `autoConfigurationClasses` 所有的自動配置類進行處理
			// 這裡是對 `@ConditionalOnClass` 註解進行處理,必須存在指定 Class 類物件
			return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
		}
		// <2> 否則,就是單核處理,當前執行緒去處理
		else {
			// <2.1> 建立一個匹配處理器 `outcomesResolver`
			OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0,
					autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());
			// <2.2> 返回 `outcomesResolver` 的執行結果
			// 這裡是對 `@ConditionalOnClass` 註解進行處理,必須存在指定 Class 類物件
			return outcomesResolver.resolveOutcomes();
		}
	}
}

過濾所有自動配置類的的過程如下:

考慮到自動配置類上面的 @Conditional 相關注解比較多,所以採用多執行緒以提升效率。經過測試使用,使用兩個執行緒的效率是最高的,所以會嘗試將 autoConfigurationClasses 一分為二進行處理

  1. 如果 JVM 可用的處理器不止一個,那麼這裡用兩個執行緒去處理
    1. autoConfigurationClasses 所有的自動配置類進行處理,這裡是對 @ConditionalOnClass 註解進行處理,必須存在指定 Class 類物件
  2. 否則,就是單核處理,當前執行緒去處理
    1. 建立一個匹配處理器 outcomesResolver
    2. 返回 outcomesResolver 的執行結果,這裡是對 @ConditionalOnClass 註解進行處理,必須存在指定 Class 類物件
resolveOutcomesThreaded 方法
private ConditionOutcome[] resolveOutcomesThreaded(String[] autoConfigurationClasses,
        AutoConfigurationMetadata autoConfigurationMetadata) {
    // <1> 將自動配置類的個數一分為二
    int split = autoConfigurationClasses.length / 2;
    // <2> 建立一個 StandardOutcomesResolver 匹配處理器,另起一個執行緒去處理前一半的自動配置類
    OutcomesResolver firstHalfResolver = createOutcomesResolver(autoConfigurationClasses, 0, split,
            autoConfigurationMetadata);
    // <3> 建立一個 StandardOutcomesResolver 匹配處理器,當前執行緒去處理後一半的自動配置類
    OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(autoConfigurationClasses, split,
            autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());
    // <4> 獲取兩個匹配器處理器的處理結果,將他們合併,然後返回
    ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();
    ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes();
    ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
    System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length);
    System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length);
    return outcomes;
}

邏輯很簡單,就是同時兩個執行緒分半處理,還是通過 StandardOutcomesResolver 匹配處理器來處理

StandardOutcomesResolver 處理器

private final class StandardOutcomesResolver implements OutcomesResolver {
    /**
     * 需要處理的自動配置類
     */
    private final String[] autoConfigurationClasses;
    /**
     * 區間開始位置
     */
    private final int start;
    /**
     * 區間結束位置
     */
    private final int end;
    /**
     * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration} 註解的元資訊
     */
    private final AutoConfigurationMetadata autoConfigurationMetadata;
    /**
     * 類載入器
     */
    private final ClassLoader beanClassLoader;

    @Override
    public ConditionOutcome[] resolveOutcomes() {
        // 獲取自動配置類的匹配結果
        return getOutcomes(this.autoConfigurationClasses, this.start, this.end, this.autoConfigurationMetadata);
    }
}
getOutcomes 方法
private ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, int start, int end,
        AutoConfigurationMetadata autoConfigurationMetadata) {
    ConditionOutcome[] outcomes = new ConditionOutcome[end - start];
    /**
     * 遍歷執行區間內的自動配置類
     */
    for (int i = start; i < end; i++) {
        String autoConfigurationClass = autoConfigurationClasses[i];
        if (autoConfigurationClass != null) {
            /**
             * 獲取這個自動配置類的 `@ConditionalOnClass` 註解的值
             * 這裡不需要解析,而是從一個 `Properties` 中直接獲取
             * 參考 {@link AutoConfigurationImportSelector} 中我的註釋
             * 參考 {@link AutoConfigureAnnotationProcessor}
             */
            String candidates = autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnClass");
            // 如果值不為空,那麼這裡先進行匹配處理,判斷這個自動配置類是否需要注入,也就是是否存在 指定的 Class 物件(`candidates`)
            // 否則,不進行任何處理,也就是不過濾掉
            if (candidates != null) {
                // 判斷指定的 Class 類物件是否都存在,都存在返回 `null`,有一個不存在返回不匹配
                outcomes[i - start] = getOutcome(candidates);
            }
        }
    }
    return outcomes;
}

private ConditionOutcome getOutcome(String candidates) {
    try {

        // 這個配置類的 `@ConditionalOnClass` 註解只指定了一個,則直接處理
        if (!candidates.contains(",")) {
            // 判斷這個 Class 類物件是否存在,存在返回 `null`,不存在返回不匹配
            return getOutcome(candidates, this.beanClassLoader);
        }
        // 這個配置類的 `@ConditionalOnClass` 註解指定了多個,則遍歷處理,必須都存在
        for (String candidate : StringUtils.commaDelimitedListToStringArray(candidates)) {
            // 判斷這個 Class 類物件是否存在,存在返回 `null`,不存在返回不匹配
            ConditionOutcome outcome = getOutcome(candidate, this.beanClassLoader);
            // 如果不為空,表示不匹配,直接返回
            if (outcome != null) {
                return outcome;
            }
        }
    }
    catch (Exception ex) { }
    return null;
}

private ConditionOutcome getOutcome(String className, ClassLoader classLoader) {
    // 如果這個 Class 物件不存在,則返回不匹配
    if (ClassNameFilter.MISSING.matches(className, classLoader)) {
        return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
                .didNotFind("required class").items(Style.QUOTE, className));
    }
    return null;
}

邏輯比較簡單,先從 AutoConfigurationMetadata 這個物件中找到這個自動配置類 @ConditionalOnClass 註解的值,如下:

@Override
public String get(String className, String key) {
    return get(className, key, null);
}

@Override
public String get(String className, String key, String defaultValue) {
    // 獲取 `類名.註解簡稱` 對應的值,也就是這個類上面該註解的值
    String value = this.properties.getProperty(className + "." + key);
    // 如果存在該註解的值,則返回,沒有的話返回指定的預設值
    return (value != null) ? value : defaultValue;
}

如果沒有該註解,匹配結果為空,也表示匹配成功,存在該註解,則對指定的 Class 物件進行判斷,如果有一個 Class 物件不存在,匹配結果則是不匹配,所以這個 AutoConfigurationMetadata 對於過濾自動配置類很關鍵

其它實現類

另外兩個 OnBeanCondition 和 OnWebApplicationCondition 實現類的原理差不多,感興趣的可以去看看

AutoConfigurationMetadata

org.springframework.boot.autoconfigure.AutoConfigurationMetadata 介面,僅有一個 PropertiesAutoConfigurationMetadata 實現類

這個物件很關鍵,在文中提到不少次數,是解析 META-INF/spring-autoconfigure-metadata.properties 檔案生成的物件,裡面儲存了 Spring Boot 中自動配置類的註解後設資料

至於如何解析這個檔案,如何生成該物件,這裡不在講述,可以回顧上一篇《剖析 @SpringBootApplication 註解》 文章的 AutoConfigureAnnotationProcessor 小節

下面列舉檔案中的部分內容

org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration=
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.ConditionalOnClass=org.springframework.web.servlet.DispatcherServlet
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.AutoConfigureOrder=-2147483648
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration

org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration=
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.ConditionalOnClass=org.springframework.data.redis.core.RedisOperations

看到這個檔案是不是明白了,判斷是否要引入 DispatcherServletAutoConfiguration 這個自動配置類,從這個 properties 檔案中找到它的 @ConditionalOnClass 註解的值,然後判斷指定的 Class 物件們是否都存在,不存在則表示不需要引入這個自動配置類

當然,還有 @OnBeanCondition@OnWebApplicationCondition 兩個註解的判斷

總結

本文分析了 Spring 的 Condition 介面,配合 @Conditional 註解的使用,可以判斷某個 Class 物件是否有必要生成一個 Bean 被 Spring IoC 容器管理。

由於在 Spring 中 Condition 介面的實現類就一個,Spring Boot 則對 Condition 介面進行擴充套件,豐富了更多的使用場景,其內部主要用於自動配置類。同時,在 Spring Boot 中提供了很多 Condition 相關的註解,配合 @Conditional 註解一起使用,就能將滿足條件的自動配置類引入進來。例如 @OnClassCondition 註解標註的配置類必須存在所有的指定的 Class 物件們,才將其生成一個 Bean 被 Spring IoC 容器管理。

@EnableAutoConfiguration 註解驅動自動配置模組的過程中,會通過 AutoConfigurationImportFilter 過濾掉一些不滿足條件的自動配置類,原理和 Condition 差不多,主要通過上面這個 AutoConfigurationMetadata 類,再結合不同的 AutoConfigurationImportFilter 實現類實現的。

相關文章