1. 程式人生 > >[Spring Boot] 4. Spring Boot實現自動配置的原理

[Spring Boot] 4. Spring Boot實現自動配置的原理

入口註解類@EnableAutoConfiguration

@SpringBootApplication註解中包含了自動配置的入口註解:

@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 { // ... }
@SuppressWarnings("deprecation")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public
@interface EnableAutoConfiguration { // ... }

這個註解的Javadoc內容還是不少,所有就不貼在文章裡面了,概括一下:

  1. 自動配置基於應用的類路徑以及你定義了什麼Beans
  2. 如果使用了@SpringBootApplication註解,那麼自動就啟用了自動配置
  3. 可以通過設定註解的excludeName屬性或者通過spring.autoconfigure.exclude配置項來指定不需要自動配置的專案
  4. 自動配置的發生時機在使用者定義的Beans被註冊之後
  5. 如果沒有和@SpringBootApplication一同使用,最好將@EnableAutoConfiguration註解放在root package的類上,這樣就能夠搜尋到所有子packages中的類了
  6. 自動配置類就是普通的Spring @Configuration類,通過SpringFactoriesLoader機制完成載入,實現上通常使用@Conditional(比如@ConditionalOnClass或者@ConditionalOnMissingBean)

@AutoConfigurationPackage

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

這個註解的職責就是引入了另外一個配置類:AutoConfigurationPackages.Registrar。

/**
 * ImportBeanDefinitionRegistrar用來從匯入的Config中儲存base package
 */
@Order(Ordered.HIGHEST_PRECEDENCE)
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        register(registry, new PackageImport(metadata).getPackageName());
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.<Object>singleton(new PackageImport(metadata));
    }

}

這個註解實現的功能已經比較底層了,除錯看看上面的register方法什麼會被呼叫:

呼叫引數中的packageNames陣列中僅包含一個值:com.example.demo,也就是專案的root package名。

從呼叫棧來看的話,呼叫register方法的時間在容器重新整理期間:

refresh -> invokeBeanFactoryPostProcessors -> invokeBeanDefinitionRegistryPostProcessors -> postProcessBeanDefinitionRegistry -> processConfigBeanDefinitions(開始處理配置Bean的定義) -> loadBeanDefinitions -> loadBeanDefinitionsForConfigurationClass(讀取配置Class中的Bean定義) -> loadBeanDefinitionsFromRegistrars(這裡開始準備進入上面的register方法) -> registerBeanDefinitions(即上述方法)

這個過程已經比較複雜了,目前暫且不深入研究了。它的功能簡單說就是將應用的root package給註冊到Spring容器中,供後續使用。

相比而言,下面要討論的幾個型別才是實現自動配置的關鍵。

@Import(EnableAutoConfigurationImportSelector.class)

@EnableAutoConfiguration註解的另外一個作用就是引入了EnableAutoConfigurationImportSelector:

它的類圖如下所示:

可以發現它除了實現幾個Aware類介面外,最關鍵的就是實現了DeferredImportSelector(繼承自ImportSelector)介面。

所以我們先來看看ImportSelector以及DeferredImportSelector介面的定義:

public interface ImportSelector {

    /**
     * 基於被引入的Configuration類的AnnotationMetadata資訊選擇並返回需要引入的類名列表
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

}

這個介面的Javadoc比較長,還是撿重點說明一下:

  1. 主要功能通過selectImports方法實現,用於篩選需要引入的類名
  2. 實現了ImportSelector的類也可以實現一系列Aware介面,這些Aware介面中的相應方法會在selectImports方法之前被呼叫(這一點通過上面的類圖也可以佐證,EnableAutoConfigurationImportSelector確實實現了四個Aware型別的介面)
  3. ImportSelector的實現和通常的@Import在處理方式上是一致的,然而還是可以在所有@Configuration類都被處理後再進行引入篩選(具體看下面即將介紹的DeferredImportSelector)
public interface DeferredImportSelector extends ImportSelector {

}

這個介面是一個標記介面,它本身沒有定義任何方法。那麼這個介面的含義是什麼呢:

  1. 它是ImportSelector介面的一個變體,在所有的@Configuration被處理之後才會執行。在需要篩選的引入型別具備@Conditional註解的時候非常有用
  2. 實現類同樣也可以實現Ordered介面,來定義多個DeferredImportSelector的優先級別(同樣地,EnableAutoConfigurationImportSelector也實現了Ordered介面)

明確了這兩個介面的意義,下面來看看是如何實現的:

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    try {
      // Step1: 得到註解資訊
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        // Step2: 得到註解中的所有屬性資訊
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        // Step3: 得到候選配置列表
        List<String> configurations = getCandidateConfigurations(annotationMetadata,
                attributes);
        // Step4: 去重
        configurations = removeDuplicates(configurations);
        // Step5: 排序
        configurations = sort(configurations, autoConfigurationMetadata);
        // Step6: 根據註解中的exclude資訊去除不需要的
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        // Step7: 派發事件
        fireAutoConfigurationImportEvents(configurations, exclusions);
        return configurations.toArray(new String[configurations.size()]);
    }
    catch (IOException ex) {
        throw new IllegalStateException(ex);
    }
}

很明顯,核心就在於上面的步驟3:

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

它將實現委託給了SpringFactoriesLoader的loadFactoryNames方法:

// 傳入的factoryClass:org.springframework.boot.autoconfigure.EnableAutoConfiguration
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    try {
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            String factoryClassNames = properties.getProperty(factoryClassName);
            result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
        }
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

// 相關常量
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

這段程式碼的意圖很明確,在第一篇文章討論Spring Boot啟動過程的時候就已經接觸到了。它會從類路徑中拿到所有名為META-INF/spring.factories的配置檔案,然後按照factoryClass的名稱取到對應的值。那麼我們就來找一個META-INF/spring.factories配置檔案看看。

META-INF/spring.factories

比如spring-boot-autoconfigure包:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
# 省略了很多

列舉了非常多的自動配置候選項,挑一個AOP相關的AopAutoConfiguration看看究竟:

// 如果設定了spring.aop.auto=false,那麼AOP不會被配置
// 需要檢測到@EnableAspectJAutoProxy註解存在才會生效
// 預設使用JdkDynamicAutoProxyConfiguration,如果設定了spring.aop.proxy-target-class=true,那麼使用CglibAutoProxyConfiguration
@Configuration
@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

    @Configuration
    @EnableAspectJAutoProxy(proxyTargetClass = false)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false", matchIfMissing = true)
    public static class JdkDynamicAutoProxyConfiguration {

    }

    @Configuration
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true", matchIfMissing = false)
    public static class CglibAutoProxyConfiguration {

    }

}

這個自動配置類的作用是判斷是否存在配置項:

spring.aop.proxy-target-class=true

如果存在並且值為true的話使用基於CGLIB位元組碼操作的動態代理方案,否則使用JDK自帶的動態代理機制。

在這個配置類中,使用到了兩個全新的註解:

  • @ConditionalOnClass
  • @ConditionalOnProperty

從這兩個註解的名稱,就大概能夠猜出它們的功能了:

@ConditionalOnClass

當類路徑上存在指定的類時,滿足條件。

@ConditionalOnProperty

當配置中存在指定的屬性時,滿足條件。

其實除了這兩個註解之外,還有幾個類似的,它們都在org.springframework.boot.autoconfigure.condition這個包下,在具體介紹實現之前,下面先來看看Spring Boot對於@Conditional的擴充套件。=

Spring Boot對於@Conditional的擴充套件

Spring Boot提供了一個實現了Condition介面的抽象類SpringBootCondition。

這個類的主要作用是列印一些用於診斷的日誌,告訴使用者哪些型別被自動配置了。

它實現Condition介面的方法:

@Override
public final boolean matches(ConditionContext context,
        AnnotatedTypeMetadata metadata) {
    String classOrMethodName = getClassOrMethodName(metadata);
    try {
        ConditionOutcome outcome = getMatchOutcome(context, metadata);
        logOutcome(classOrMethodName, outcome);
        recordEvaluation(context, classOrMethodName, outcome);
        return outcome.isMatch();
    }
    catch (NoClassDefFoundError ex) {
        throw new IllegalStateException(
                "Could not evaluate condition on " + classOrMethodName + " due to "
                        + ex.getMessage() + " not "
                        + "found. Make sure your own configuration does not rely on "
                        + "that class. This can also happen if you are "
                        + "@ComponentScanning a springframework package (e.g. if you "
                        + "put a @ComponentScan in the default package by mistake)",
                ex);
    }
    catch (RuntimeException ex) {
        throw new IllegalStateException(
                "Error processing condition on " + getName(metadata), ex);
    }
}

/**
 * Determine the outcome of the match along with suitable log output.
 * @param context the condition context
 * @param metadata the annotation metadata
 * @return the condition outcome
 */
public abstract ConditionOutcome getMatchOutcome(ConditionContext context,
        AnnotatedTypeMetadata metadata);

SpringBootCondition已經提供了基本的實現,將內部的匹配細節定義成抽象方法getMatchOutcome,交給其子類去完成。

另外,還提供了兩個可能會被子類使用到的方法:

/**
 * 如果指定的conditions中有任意一個匹配,那麼就返回true
 * @param context the context
 * @param metadata the annotation meta-data
 * @param conditions conditions to test
 * @return {@code true} if any condition matches.
 */
protected final boolean anyMatches(ConditionContext context,
        AnnotatedTypeMetadata metadata, Condition... conditions) {
    for (Condition condition : conditions) {
        if (matches(context, metadata, condition)) {
            return true;
        }
    }
    return false;
}

/**
 * 檢查指定的condition是否匹配
 * @param context the context
 * @param metadata the annotation meta-data
 * @param condition condition to test
 * @return {@code true} if the condition matches.
 */
protected final boolean matches(ConditionContext context,
        AnnotatedTypeMetadata metadata, Condition condition) {
    if (condition instanceof SpringBootCondition) {
        return ((SpringBootCondition) condition).getMatchOutcome(context, metadata)
                .isMatch();
    }
    return condition.matches(context, metadata);
}

org.springframework.boot.autoconfigure.condition包

除了上面已經遇到的@ConditionalOnClass和@ConditionalOnProperty,這個包中還定義了很多條件實現類,下面簡單列舉幾個:

@ConditionalOnExpression - 基於SpEL的條件判斷

/**
 * Configuration annotation for a conditional element that depends on the value of a SpEL
 * expression.
 *
 * @author Dave Syer
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnExpressionCondition.class)
public @interface ConditionalOnExpression {

    /**
     * The SpEL expression to evaluate. Expression should return {@code true} if the
     * condition passes or {@code false} if it fails.
     * @return the SpEL expression
     */
    String value() default "true";

然後相應的實現類是OnExpressionCondition,它繼承自SpringBootCondition。

@ConditionalOnMissingClass - 基於類不存在與classpath的條件判斷

這一個條件實現正好和@ConditionalOnClass條件相反。

下面列舉所有由Spring Boot提供的條件註解:

  • @ConditionalOnBean
  • @ConditionalOnClass
  • @ConditionalOnCloudPlatform
  • @ConditionalOnExpression
  • @ConditionalOnJava
  • @ConditionalOnJndi
  • @ConditionalOnMissingBean
  • @ConditionalOnMissingClass
  • @ConditionalOnNotWebApplication
  • @ConditionalOnProperty
  • @ConditionalOnResource
  • @ConditionalOnSingleCandidate
  • @ConditionalOnWebApplication

一般的模式,就是一個條件註解對應一個繼承自SpringBootCondition的具體實現類。