SpringBoot是如何实现自动配置的?--SpringBoot源码(四)

简介: SpringBoot是如何实现自动配置的?--SpringBoot源码(四)

注:该源码分析对应SpringBoot版本为2.1.0.RELEASE

1 前言

本篇接
助力SpringBoot自动配置的条件注解ConditionalOnXXX分析--SpringBoot源码(三)

温故而知新,我们来简单回顾一下上篇的内容,上一篇我们分析了SpringBoot的条件注解@ConditionalOnXxx的相关源码,现挑重点总结如下:

  1. SpringBoot的所有@ConditionalOnXxx的条件类OnXxxCondition都是继承于SpringBootCondition基类,而SpringBootCondition又实现了Condition接口。
  2. SpringBootCondition基类主要用来打印一些条件注解评估报告的日志,这些条件评估信息全部来源于其子类注解条件类OnXxxCondition,因此其也抽象了一个模板方法getMatchOutcome留给子类去实现来评估其条件注解是否符合条件。
  3. 前一篇我们也还有一个重要的知识点还没分析,那就是跟过滤自动配置类逻辑有关的AutoConfigurationImportFilter接口,这篇文章我们来填一下这个坑。

前面我们分析了跟SpringBoot的自动配置息息相关内置条件注解@ConditionalOnXxx后,现在我们就开始来撸SpringBoot自动配置的相关源码了。

2 @SpringBootApplication注解

在开始前,我们先想一下,SpringBoot为何一个标注有@SpringBootApplication注解的启动类通过执行一个简单的run方法就能实现SpringBoot大量Starter的自动配置呢?
其实SpringBoot的自动配置就跟@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 {
// ...省略非关键代码
}

@SpringBootApplication标注了很多注解,我们可以看到其中跟SpringBoot自动配置有关的注解就有一个即@EnableAutoConfiguration,因此,可以肯定的是SpringBoot的自动配置肯定跟@EnableAutoConfiguration息息相关(其中@ComponentScan注解的excludeFilters属性也有一个类AutoConfigurationExcludeFilter,这个类跟自动配置也有点关系,但不是我们关注的重点)。
现在我们来打开@EnableAutoConfiguration注解的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

看到@EnableAutoConfiguration注解又标有@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)两个注解,顾名思义,@AutoConfigurationPackage注解肯定跟自动配置的包有关,而AutoConfigurationImportSelector则是跟SpringBoot的自动配置选择导入有关(Spring中的ImportSelector是用来导入配置类的,通常是基于某些条件注解@ConditionalOnXxxx来决定是否导入某个配置类)。

因此,可以看出AutoConfigurationImportSelector类是我们本篇的重点,因为SpringBoot的自动配置肯定有一个配置类,而这个配置类的导入则需要靠AutoConfigurationImportSelector这个哥们来实现。

接下来我们重点来看AutoConfigurationImportSelector这个类,完了我们再简单分析下@AutoConfigurationPackage这个注解的逻辑。

3 如何去找SpringBoot自动配置实现逻辑的入口方法?

可以肯定的是SpringBoot的自动配置的逻辑肯定与AutoConfigurationImportSelector这个类有关,那么我们该如何去找到SpringBoot自动配置实现逻辑的入口方法呢?

在找SpringBoot自动配置实现逻辑的入口方法前,我们先来看下AutoConfigurationImportSelector的相关类图,好有个整体的理解。看下图:


图1

可以看到AutoConfigurationImportSelector重点是实现了DeferredImportSelector接口和各种Aware接口,然后DeferredImportSelector接口又继承了ImportSelector接口。

自然而然的,我们会去关注AutoConfigurationImportSelector复写DeferredImportSelector接口的实现方法selectImports方法,因为selectImports方法跟导入自动配置类有关,而这个方法往往是程序执行的入口方法。经过调试发现selectImports方法很具有迷惑性,selectImports方法跟自动配置相关的逻辑有点关系,但实质关系不大。

此时剧情的发展好像不太符合常理,此时我们又该如何来找到自动配置逻辑有关的入口方法呢?

最简单的方法就是在AutoConfigurationImportSelector类的每个方法都打上断点,然后调试看先执行到哪个方法。但是我们可以不这么做,我们回想下,自定义一个Starter的时候我们是不是要在spring.factories配置文件中配置

EnableAutoConfiguration=XxxAutoConfiguration

因此可以推断,SpringBoot的自动配置原理肯定跟从spring.factories配置文件中加载自动配置类有关,于是结合AutoConfigurationImportSelector的方法注释,我们找到了getAutoConfigurationEntry方法。于是我们在这个方法里面打上一个断点,此时通过调用栈帧来看下更上层的入口方法在哪里,然后我们再从跟自动配置相关的更上层的入口方法开始分析。


图2

通过图1我们可以看到,跟自动配置逻辑相关的入口方法在DeferredImportSelectorGrouping类的getImports方法处,因此我们就从DeferredImportSelectorGrouping类的getImports方法来开始分析SpringBoot的自动配置源码好了。

4 分析SpringBoot自动配置原理

既然找到ConfigurationClassParser.getImports()方法是自动配置相关的入口方法,那么下面我们就来真正分析SpringBoot自动配置的源码了。

先看一下getImports方法代码:

// ConfigurationClassParser.java

public Iterable<Group.Entry> getImports() {
    // 遍历DeferredImportSelectorHolder对象集合deferredImports,deferredImports集合装了各种ImportSelector,当然这里装的是AutoConfigurationImportSelector
    for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
        // 【1】,利用AutoConfigurationGroup的process方法来处理自动配置的相关逻辑,决定导入哪些配置类(这个是我们分析的重点,自动配置的逻辑全在这了)
        this.group.process(deferredImport.getConfigurationClass().getMetadata(),
                deferredImport.getImportSelector());
    }
    // 【2】,经过上面的处理后,然后再进行选择导入哪些配置类
    return this.group.selectImports();
}

【1】处的的代码是我们分析的重中之重,自动配置的相关的绝大部分逻辑全在这里了,将在4.1 分析自动配置的主要逻辑深入分析。那么`this.group.process(deferredImport.getConfigurationClass().getMetadata(),

        deferredImport.getImportSelector())`;主要做的事情就是在`this.group`即`AutoConfigurationGroup`对象的`process`方法中,传入的`AutoConfigurationImportSelector`对象来选择一些符合条件的自动配置类,过滤掉一些不符合条件的自动配置类,就是这么个事情,无他。

      

注:

  1. AutoConfigurationGroup:是AutoConfigurationImportSelector的内部类,主要用来处理自动配置相关的逻辑,拥有processselectImports方法,然后拥有entriesautoConfigurationEntries集合属性,这两个集合分别存储被处理后的符合条件的自动配置类,我们知道这些就足够了;
  2. AutoConfigurationImportSelector:承担自动配置的绝大部分逻辑,负责选择一些符合条件的自动配置类;
  3. metadata:标注在SpringBoot启动类上的@SpringBootApplication注解元数据

    【2】this.group.selectImports的方法主要是针对前面的process方法处理后的自动配置类再进一步有选择的选择导入,将在4.2 有选择的导入自动配置类这小节深入分析。

4.1 分析自动配置的主要逻辑

这里继续深究前面 4 分析SpringBoot自动配置原理这节标【1】处的
this.group.process方法是如何处理自动配置相关逻辑的。

// AutoConfigurationImportSelector$AutoConfigurationGroup.java

// 这里用来处理自动配置类,比如过滤掉不符合匹配条件的自动配置类
public void process(AnnotationMetadata annotationMetadata,
        DeferredImportSelector deferredImportSelector) {
    Assert.state(
            deferredImportSelector instanceof AutoConfigurationImportSelector,
            () -> String.format("Only %s implementations are supported, got %s",
                    AutoConfigurationImportSelector.class.getSimpleName(),
                    deferredImportSelector.getClass().getName()));
    // 【1】,调用getAutoConfigurationEntry方法得到自动配置类放入autoConfigurationEntry对象中
    AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
            .getAutoConfigurationEntry(getAutoConfigurationMetadata(),
                    annotationMetadata);
    // 【2】,又将封装了自动配置类的autoConfigurationEntry对象装进autoConfigurationEntries集合
    this.autoConfigurationEntries.add(autoConfigurationEntry); 
    // 【3】,遍历刚获取的自动配置类
    for (String importClassName : autoConfigurationEntry.getConfigurations()) {
        // 这里符合条件的自动配置类作为key,annotationMetadata作为值放进entries集合
        this.entries.putIfAbsent(importClassName, annotationMetadata); 
    }
}

上面代码中我们再来看标【1】的方法getAutoConfigurationEntry,这个方法主要是用来获取自动配置类有关,承担了自动配置的主要逻辑。直接上代码:

// AutoConfigurationImportSelector.java

// 获取符合条件的自动配置类,避免加载不必要的自动配置类从而造成内存浪费
protected AutoConfigurationEntry getAutoConfigurationEntry(
        AutoConfigurationMetadata autoConfigurationMetadata,
        AnnotationMetadata annotationMetadata) {
    // 获取是否有配置spring.boot.enableautoconfiguration属性,默认返回true
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    // 获得@Congiguration标注的Configuration类即被审视introspectedClass的注解数据,
    // 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)
    // 将会获取到exclude = FreeMarkerAutoConfiguration.class和excludeName=""的注解数据
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 【1】得到spring.factories文件配置的所有自动配置类
    List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);
    // 利用LinkedHashSet移除重复的配置类
    configurations = removeDuplicates(configurations);
    // 得到要排除的自动配置类,比如注解属性exclude的配置类
    // 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)
    // 将会获取到exclude = FreeMarkerAutoConfiguration.class的注解数据
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    // 检查要被排除的配置类,因为有些不是自动配置类,故要抛出异常
    checkExcludedClasses(configurations, exclusions);
    // 【2】将要排除的配置类移除
    configurations.removeAll(exclusions);
    // 【3】因为从spring.factories文件获取的自动配置类太多,如果有些不必要的自动配置类都加载进内存,会造成内存浪费,因此这里需要进行过滤
    // 注意这里会调用AutoConfigurationImportFilter的match方法来判断是否符合@ConditionalOnBean,@ConditionalOnClass或@ConditionalOnWebApplication,后面会重点分析一下
    configurations = filter(configurations, autoConfigurationMetadata);
    // 【4】获取了符合条件的自动配置类后,此时触发AutoConfigurationImportEvent事件,
    // 目的是告诉ConditionEvaluationReport条件评估报告器对象来记录符合条件的自动配置类
    // 该事件什么时候会被触发?--> 在刷新容器时调用invokeBeanFactoryPostProcessors后置处理器时触发
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // 【5】将符合条件和要排除的自动配置类封装进AutoConfigurationEntry对象,并返回
    return new AutoConfigurationEntry(configurations, exclusions); 
}

AutoConfigurationEntry方法主要做的事情就是获取符合条件的自动配置类,避免加载不必要的自动配置类从而造成内存浪费。我们下面总结下AutoConfigurationEntry方法主要做的事情:

【1】从spring.factories配置文件中加载EnableAutoConfiguration自动配置类,获取的自动配置类如图3所示。这里我们知道该方法做了什么事情就行了,后面还会有一篇文章详述spring.factories的原理;

【2】若@EnableAutoConfiguration等注解标有要exclude的自动配置类,那么再将这个自动配置类排除掉;

【3】排除掉要exclude的自动配置类后,然后再调用filter方法进行进一步的过滤,再次排除一些不符合条件的自动配置类;这个在稍后会详细分析。

【4】经过重重过滤后,此时再触发AutoConfigurationImportEvent事件,告诉ConditionEvaluationReport条件评估报告器对象来记录符合条件的自动配置类;(这个在6 AutoConfigurationImportListener这小节详细分析。)

【5】 最后再将符合条件的自动配置类返回。


图3

总结了AutoConfigurationEntry方法主要的逻辑后,我们再来细看一下AutoConfigurationImportSelectorfilter方法:

// AutoConfigurationImportSelector.java

private List<String> filter(List<String> configurations,
            AutoConfigurationMetadata autoConfigurationMetadata) {
    long startTime = System.nanoTime();
    // 将从spring.factories中获取的自动配置类转出字符串数组
    String[] candidates = StringUtils.toStringArray(configurations);
    // 定义skip数组,是否需要跳过。注意skip数组与candidates数组顺序一一对应
    boolean[] skip = new boolean[candidates.length];
    boolean skipped = false;
    // getAutoConfigurationImportFilters方法:拿到OnBeanCondition,OnClassCondition和OnWebApplicationCondition
    // 然后遍历这三个条件类去过滤从spring.factories加载的大量配置类
    for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
        // 调用各种aware方法,将beanClassLoader,beanFactory等注入到filter对象中,
        // 这里的filter对象即OnBeanCondition,OnClassCondition或OnWebApplicationCondition
        invokeAwareMethods(filter);
        // 判断各种filter来判断每个candidate(这里实质要通过candidate(自动配置类)拿到其标注的
        // @ConditionalOnClass,@ConditionalOnBean和@ConditionalOnWebApplication里面的注解值)是否匹配,
        // 注意candidates数组与match数组一一对应
        /**********************【主线,重点关注】********************************/
        boolean[] match = filter.match(candidates, autoConfigurationMetadata);
        // 遍历match数组,注意match顺序跟candidates的自动配置类一一对应
        for (int i = 0; i < match.length; i++) {
            // 若有不匹配的话
            if (!match[i]) {
                // 不匹配的将记录在skip数组,标志skip[i]为true,也与candidates数组一一对应
                skip[i] = true;
                // 因为不匹配,将相应的自动配置类置空
                candidates[i] = null;
                // 标注skipped为true
                skipped = true; 
            }
        }
    } 
    // 这里表示若所有自动配置类经过OnBeanCondition,OnClassCondition和OnWebApplicationCondition过滤后,全部都匹配的话,则全部原样返回
    if (!skipped) {
        return configurations;
    }
    // 建立result集合来装匹配的自动配置类
    List<String> result = new ArrayList<>(candidates.length); 
    for (int i = 0; i < candidates.length; i++) {
        // 若skip[i]为false,则说明是符合条件的自动配置类,此时添加到result集合中
        if (!skip[i]) { 
            result.add(candidates[i]);
        }
    }
    // 打印日志
    if (logger.isTraceEnabled()) {
        int numberFiltered = configurations.size() - result.size();
        logger.trace("Filtered " + numberFiltered + " auto configuration class in "
                + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
                + " ms");
    }
    // 最后返回符合条件的自动配置类
    return new ArrayList<>(result);
}

AutoConfigurationImportSelectorfilter方法主要做的事情就是调用AutoConfigurationImportFilter接口的match方法来判断每一个自动配置类上的条件注解(若有的话)@ConditionalOnClass,@ConditionalOnBean@ConditionalOnWebApplication是否满足条件,若满足,则返回true,说明匹配,若不满足,则返回false说明不匹配。

我们现在知道AutoConfigurationImportSelectorfilter方法主要做了什么事情就行了,现在先不用研究的过深,至于AutoConfigurationImportFilter接口的match方法将在5 AutoConfigurationImportFilter这小节再详细分析,填补一下我们前一篇条件注解源码分析中留下的坑。

注意:我们坚持主线优先的原则,其他枝节代码这里不深究,以免丢了主线哈。

4.2 有选择的导入自动配置类

这里继续深究前面 4 分析SpringBoot自动配置原理这节标【2】处的
this.group.selectImports方法是如何进一步有选择的导入自动配置类的。直接看代码:

// AutoConfigurationImportSelector$AutoConfigurationGroup.java

public Iterable<Entry> selectImports() {
    if (this.autoConfigurationEntries.isEmpty()) {
        return Collections.emptyList();
    } 
    // 这里得到所有要排除的自动配置类的set集合
    Set<String> allExclusions = this.autoConfigurationEntries.stream()
            .map(AutoConfigurationEntry::getExclusions)
            .flatMap(Collection::stream).collect(Collectors.toSet());
    // 这里得到经过滤后所有符合条件的自动配置类的set集合
    Set<String> processedConfigurations = this.autoConfigurationEntries.stream() 
            .map(AutoConfigurationEntry::getConfigurations)
            .flatMap(Collection::stream)
            .collect(Collectors.toCollection(LinkedHashSet::new));
    // 移除掉要排除的自动配置类
    processedConfigurations.removeAll(allExclusions); 
    // 对标注有@Order注解的自动配置类进行排序,
    return sortAutoConfigurations(processedConfigurations,
            getAutoConfigurationMetadata())
                    .stream()
                    .map((importClassName) -> new Entry(
                            this.entries.get(importClassName), importClassName))
                    .collect(Collectors.toList());
}

可以看到,selectImports方法主要是针对经过排除掉exclude的和被AutoConfigurationImportFilter接口过滤后的满足条件的自动配置类再进一步排除exclude的自动配置类,然后再排序。逻辑很简单,不再详述。

不过有个疑问,前面已经exclude过一次了,为何这里还要再exclude一次?

5 AutoConfigurationImportFilter

这里继续深究前面 4.1节
AutoConfigurationImportSelector.filter方法的过滤自动配置类的boolean[] match = filter.match(candidates, autoConfigurationMetadata);这句代码。

因此我们继续分析AutoConfigurationImportFilter接口,分析其match方法,同时也是对前一篇@ConditionalOnXxx的源码分析文章中留下的坑进行填补。

AutoConfigurationImportFilter接口只有一个match方法用来过滤不符合条件的自动配置类。

@FunctionalInterface
public interface AutoConfigurationImportFilter {
    boolean[] match(String[] autoConfigurationClasses,
            AutoConfigurationMetadata autoConfigurationMetadata);
}

同样,在分析AutoConfigurationImportFilter接口的match方法前,我们先来看下其类关系图:


图4

可以看到,AutoConfigurationImportFilter接口有一个具体的实现类FilteringSpringBootConditionFilteringSpringBootCondition又有三个具体的子类:OnClassCondition,OnBeanCondtitionOnWebApplicationCondition

那么这几个类之间的关系是怎样的呢?

FilteringSpringBootCondition实现了AutoConfigurationImportFilter接口的match方法,然后在FilteringSpringBootConditionmatch方法调用getOutcomes这个抽象模板方法返回自动配置类的匹配与否的信息。同时,最重要的是FilteringSpringBootCondition的三个子类OnClassCondition,OnBeanCondtitionOnWebApplicationCondition将会复写这个模板方法实现自己的匹配判断逻辑。

好了,AutoConfigurationImportFilter接口的整体关系已经清楚了,现在我们再进入其具体实现类FilteringSpringBootConditionmatch方法看看是其如何根据条件过滤自动配置类的。

// FilteringSpringBootCondition.java

@Override
public boolean[] match(String[] autoConfigurationClasses,
        AutoConfigurationMetadata autoConfigurationMetadata) {
    // 创建评估报告
    ConditionEvaluationReport report = ConditionEvaluationReport
            .find(this.beanFactory);
    // 注意getOutcomes是模板方法,将spring.factories文件种加载的所有自动配置类传入
    // 子类(这里指的是OnClassCondition,OnBeanCondition和OnWebApplicationCondition类)去过滤
    // 注意outcomes数组存储的是不匹配的结果,跟autoConfigurationClasses数组一一对应
    /*****************************【主线,重点关注】*********************************************/
    ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses,
            autoConfigurationMetadata);
    boolean[] match = new boolean[outcomes.length];
    // 遍历outcomes,这里outcomes为null则表示匹配,不为null则表示不匹配
    for (int i = 0; i < outcomes.length; i++) {
        ConditionOutcome outcome = outcomes[i];
        match[i] = (outcome == null || outcome.isMatch());
        if (!match[i] && outcomes[i] != null) {
            // 这里若有某个类不匹配的话,此时调用父类SpringBootCondition的logOutcome方法打印日志
            logOutcome(autoConfigurationClasses[i], outcomes[i]);
            // 并将不匹配情况记录到report
            if (report != null) {
                report.recordConditionEvaluation(autoConfigurationClasses[i], this,
                        outcomes[i]);
            }
        }
    }
    return match;
}

FilteringSpringBootConditionmatch方法主要做的事情还是调用抽象模板方法getOutcomes来根据条件来过滤自动配置类,而复写getOutcomes模板方法的有三个子类,这里不再一一分析,只挑选OnClassCondition复写的getOutcomes方法进行分析。

5.1 OnClassCondition

先直接上OnClassCondition复写的getOutcomes方法的代码:

// OnClassCondition.java

protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
        AutoConfigurationMetadata autoConfigurationMetadata) {
    // Split the work and perform half in a background thread. Using a single
    // additional thread seems to offer the best performance. More threads make
    // things worse
    // 这里经过测试用两个线程去跑的话性能是最好的,大于两个线程性能反而变差
    int split = autoConfigurationClasses.length / 2;
    // 【1】开启一个新线程去扫描判断已经加载的一半自动配置类
    OutcomesResolver firstHalfResolver = createOutcomesResolver(
            autoConfigurationClasses, 0, split, autoConfigurationMetadata);
    // 【2】这里用主线程去扫描判断已经加载的一半自动配置类
    OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(
            autoConfigurationClasses, split, autoConfigurationClasses.length,
            autoConfigurationMetadata, getBeanClassLoader());
    // 【3】先让主线程去执行解析一半自动配置类是否匹配条件
    ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();
    // 【4】这里用新开启的线程取解析另一半自动配置类是否匹配
    // 注意为了防止主线程执行过快结束,resolveOutcomes方法里面调用了thread.join()来
    // 让主线程等待新线程执行结束,因为后面要合并两个线程的解析结果
    ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes();
    // 新建一个ConditionOutcome数组来存储自动配置类的筛选结果
    ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
    // 将前面两个线程的筛选结果分别拷贝进outcomes数组
    System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length);
    System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length);
    // 返回自动配置类的筛选结果
    return outcomes;
}

可以看到,OnClassConditiongetOutcomes方法主要解析自动配置类是否符合匹配条件,当然这个匹配条件指自动配置类上的注解@ConditionalOnClass指定的类存不存在于classpath中,存在则返回匹配,不存在则返回不匹配。

由于解析自动配置类是否匹配比较耗时,因此从上面代码中我们可以看到分别创建了firstHalfResolversecondHalfResolver两个解析对象,这两个解析对象个分别对应一个线程去解析加载的自动配置类是否符合条件,最终将两个线程的解析自动配置类的匹配结果合并后返回。

那么自动配置类是否符合条件的解析判断过程又是怎样的呢?现在我们分别来看一下上面代码注释标注的【1】【2】【3】【4】处。

5.1.1 createOutcomesResolver

这里对应前面5.1节的代码注释标注【1】处的OutcomesResolver firstHalfResolver = createOutcomesResolver(...);的方法:

// OnClassCondition.java

private OutcomesResolver createOutcomesResolver(String[] autoConfigurationClasses,
        int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) {
    // 新建一个StandardOutcomesResolver对象
    OutcomesResolver outcomesResolver = new StandardOutcomesResolver(
            autoConfigurationClasses, start, end, autoConfigurationMetadata,
            getBeanClassLoader());
    try {
        // new一个ThreadedOutcomesResolver对象,并将StandardOutcomesResolver类型的outcomesResolver对象作为构造器参数传入
        return new ThreadedOutcomesResolver(outcomesResolver);
    }
    // 若上面开启的线程抛出AccessControlException异常,则返回StandardOutcomesResolver对象
    catch (AccessControlException ex) {
        return outcomesResolver;
    }
}

可以看到createOutcomesResolver方法创建了一个封装了StandardOutcomesResolver类的ThreadedOutcomesResolver解析对象。
我们再来看下ThreadedOutcomesResolver这个线程解析类封装StandardOutcomesResolver这个对象的目的是什么?我们继续跟进代码:

// OnClassCondtion.java

private ThreadedOutcomesResolver(OutcomesResolver outcomesResolver) {
    // 这里开启一个新的线程,这个线程其实还是利用StandardOutcomesResolver的resolveOutcomes方法
    // 对自动配置类进行解析判断是否匹配
    this.thread = new Thread(
            () -> this.outcomes = outcomesResolver.resolveOutcomes());
    // 开启线程
    this.thread.start();
}

可以看到在构造ThreadedOutcomesResolver对象时候,原来是开启了一个线程,然后这个线程其实还是调用了刚传进来的StandardOutcomesResolver对象的resolveOutcomes方法去解析自动配置类。具体如何解析呢?稍后我们在分析【3】处代码secondHalfResolver.resolveOutcomes();的时候再深究。

5.1.2 new StandardOutcomesResolver

这里对应前面5.1节【2】处的代码OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(...);,逻辑很简单,就是创建了一个StandardOutcomesResolver对象,用于后面解析自动配置类是否匹配,同时,新建的一个线程也是利用它来完成自动配置类的解析的。

5.1.3 StandardOutcomesResolver.resolveOutcomes方法

这里对应前面5.1节标注的【3】的代码ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();

这里StandardOutcomesResolver.resolveOutcomes方法承担了解析自动配置类匹配与否的全部逻辑,是我们要重点分析的方法,resolveOutcomes方法最终把解析的自动配置类的结果赋给secondHalf数组。那么它是如何解析自动配置类是否匹配条件的呢?

// OnClassCondition$StandardOutcomesResolver.java

public ConditionOutcome[] resolveOutcomes() {
    // 再调用getOutcomes方法来解析
    return getOutcomes(this.autoConfigurationClasses, this.start, this.end,
            this.autoConfigurationMetadata);
}

private ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
        int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) { // 只要autoConfigurationMetadata没有存储相关自动配置类,那么outcome默认为null,则说明匹配
    ConditionOutcome[] outcomes = new ConditionOutcome[end - start];
    // 遍历每一个自动配置类
    for (int i = start; i < end; i++) {
        String autoConfigurationClass = autoConfigurationClasses[i];
        // TODO 对于autoConfigurationMetadata有个疑问:为何有些自动配置类的条件注解能被加载到autoConfigurationMetadata,而有些又不能,比如自己定义的一个自动配置类HelloWorldEnableAutoConfiguration就没有被存到autoConfigurationMetadata中
        if (autoConfigurationClass != null) {
            // 这里取出注解在AutoConfiguration自动配置类类的@ConditionalOnClass注解的指定类的全限定名,
            // 举个栗子,看下面的KafkaStreamsAnnotationDrivenConfiguration这个自动配置类
            /**
             * @ConditionalOnClass(StreamsBuilder.class)
             * class KafkaStreamsAnnotationDrivenConfiguration {
             * // 省略无关代码
             * }
             */
            // 那么取出的就是StreamsBuilder类的全限定名即candidates = org.apache.kafka.streams.StreamsBuilder
            String candidates = autoConfigurationMetadata
                    .get(autoConfigurationClass, "ConditionalOnClass"); // 因为这里是处理某个类是否存在于classpath中,所以传入的key是ConditionalOnClass
            // 若自动配置类标有ConditionalOnClass注解且有值,此时调用getOutcome判断是否存在于类路径中
            if (candidates != null) {
                // 拿到自动配置类注解@ConditionalOnClass的值后,再调用getOutcome方法去判断匹配结果,若该类存在于类路径,则getOutcome返回null,否则非null
                /*******************【主线,重点关注】******************/
                outcomes[i - start] = getOutcome(candidates);
            }
        }
    }
    return outcomes;
}

可以看到StandardOutcomesResolver.resolveOutcomes的方法中再次调用getOutcomes方法,主要是从autoConfigurationMetadata对象中获取到自动配置类上的注解@ConditionalOnClass指定的类的全限定名,然后作为参数传入getOutcome方法用于去类路径加载该类,若能加载到则说明注解@ConditionalOnClass满足条件,此时说明自动配置类匹配成功。

但是别忘了,这里只是过了@ConditionalOnClass注解这一关,若自动配置类还有其他注解比如@ConditionalOnBean,若该@ConditionalOnBean注解不满足条件的话,同样最终结果是不匹配的。这里扯的有点远,我们回到OnClassCondtion的判断逻辑,继续进入getOutcome方法看它是如何去判断@ConditionalOnClass注解满不满足条件的。

// OnClassCondition$StandardOutcomesResolver.java

// 返回的outcome记录的是不匹配的情况,不为null,则说明不匹配;为null,则说明匹配
private ConditionOutcome getOutcome(String candidates) {
    // candidates的形式为“org.springframework.boot.autoconfigure.aop.AopAutoConfiguration.ConditionalOnClass=org.aspectj.lang.annotation.Aspect,org.aspectj.lang.reflect.Advice,org.aspectj.weaver.AnnotatedElement”
    try {// 自动配置类上@ConditionalOnClass的值只有一个的话,直接调用getOutcome方法判断是否匹配
        if (!candidates.contains(",")) {
            // 看到因为传入的参数是 ClassNameFilter.MISSING,因此可以猜测这里应该是得到不匹配的结果
            /******************【主线,重点关注】********************/
            return getOutcome(candidates, ClassNameFilter.MISSING, 
                    this.beanClassLoader);
        }
        // 自动配置类上@ConditionalOnClass的值有多个的话,则遍历每个值(其值以逗号,分隔)
        for (String candidate : StringUtils
                .commaDelimitedListToStringArray(candidates)) {
            ConditionOutcome outcome = getOutcome(candidate,
                    ClassNameFilter.MISSING, this.beanClassLoader);
            // 可以看到,这里只要有一个不匹配的话,则返回不匹配结果
            if (outcome != null) { 
                return outcome;
            }
        }
    }
    catch (Exception ex) {
        // We'll get another chance later
    }
    return null;
}

可以看到,getOutcome方法再次调用重载方法getOutcome进一步去判断注解@ConditionalOnClass指定的类存不存在类路径中,跟着主线继续跟进去:

// OnClassCondition$StandardOutcomesResolver.java

private ConditionOutcome getOutcome(String className,
        ClassNameFilter classNameFilter, ClassLoader classLoader) {
    // 调用classNameFilter的matches方法来判断`@ConditionalOnClass`指定的类存不存在类路径中
    /******************【主线,重点关注】********************/
    if (classNameFilter.matches(className, classLoader)) { // 这里调用classNameFilter去判断className是否存在于类路径中,其中ClassNameFilter又分为PRESENT和MISSING两种;目前只看到ClassNameFilter为MISSING的调用情况,所以默认为true的话记录不匹配信息;若传入ClassNameFilter为PRESENT的话,估计还要再写一个else分支
        return ConditionOutcome.noMatch(ConditionMessage
                .forCondition(ConditionalOnClass.class)
                .didNotFind("required class").items(Style.QUOTE, className));
    }
    return null;
}

我们一层一层的剥,最终剥到了最底层了,这个真的需要足够耐心,没办法,源码只能一点一点的啃,嘿嘿。可以看到最终是调用ClassNameFiltermatches方法来判断@ConditionalOnClass指定的类存不存在类路径中,若不存在的话,则返回不匹配。

我们继续跟进ClassNameFilter的源码:

// FilteringSpringBootCondition.java

protected enum ClassNameFilter {
    // 这里表示指定的类存在于类路径中,则返回true
    PRESENT {

        @Override
        public boolean matches(String className, ClassLoader classLoader) {
            return isPresent(className, classLoader);
        }

    },
    // 这里表示指定的类不存在于类路径中,则返回true
    MISSING {

        @Override
        public boolean matches(String className, ClassLoader classLoader) {
            return !isPresent(className, classLoader); // 若classpath不存在className这个类,则返回true
        }

    };
    // 这又是一个抽象方法,分别被PRESENT和MISSING枚举类实现
    public abstract boolean matches(String className, ClassLoader classLoader);
    // 检查指定的类是否存在于类路径中    
    public static boolean isPresent(String className, ClassLoader classLoader) {
        if (classLoader == null) {
            classLoader = ClassUtils.getDefaultClassLoader();
        }
        // 利用类加载器去加载相应类,若没有抛出异常则说明类路径中存在该类,此时返回true
        try {
            forName(className, classLoader); 
            return true;
        }// 若不存在于类路径中,此时抛出的异常将catch住,返回false。
        catch (Throwable ex) {
            return false;
        }
    }
    // 利用类加载器去加载指定的类
    private static Class<?> forName(String className, ClassLoader classLoader)
            throws ClassNotFoundException {
        if (classLoader != null) {
            return classLoader.loadClass(className);
        }
        return Class.forName(className);
    }

}

可以看到ClassNameFilter原来是FilteringSpringBootCondition的一个内部枚举类,其实现了判断指定类是否存在于classpath中的逻辑,这个类很简单,这里不再详述。

5.1.4 ThreadedOutcomesResolver.resolveOutcomes方法

这里对应前面5.1节的标注的【4】的代码ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes()

前面分析5.1.3 StandardOutcomesResolver.resolveOutcomes方法已经刨根追底,陷入细节比较深,现在我们需要跳出来继续看前面标注的【4】的代码ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes()的方法哈。

这里是用新开启的线程去调用StandardOutcomesResolver.resolveOutcomes方法解析另一半自动配置类是否匹配,因为是新线程,这里很可能会出现这么一种情况:主线程解析完属于自己解析的一半自动配置类后,那么久继续往下跑了,此时不会等待新开启的子线程的。

因此,为了让主线程解析完后,我们需要让主线程继续等待正在解析的子线程,直到子线程结束。那么我们继续跟进代码区看下ThreadedOutcomesResolver.resolveOutcomes方法是怎样实现让主线程等待子线程的:

// OnClassCondition$ThreadedOutcomesResolver.java

public ConditionOutcome[] resolveOutcomes() {
    try {
        // 调用子线程的Join方法,让主线程等待
        this.thread.join();
    }
    catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
    // 若子线程结束后,此时返回子线程的解析结果
    return this.outcomes;
}

可以看到用了Thread.join()方法来让主线程等待正在解析自动配置类的子线程,这里应该也可以用CountDownLatch来让主线程等待子线程结束。最终将子线程解析后的结果赋给firstHalf数组。

5.2 OnBeanCondition和OnWebApplicationCondition

前面5.1 OnClassCondition节深入分析了OnClassCondition是如何过滤自动配置类的,那么自动配置类除了要经过OnClassCondition的过滤,还要经过OnBeanConditionOnWebApplicationCondition这两个条件类的过滤,这里不再详述,有兴趣的小伙伴可自行分析。

6 AutoConfigurationImportListener

这里继续深究前面 4.1节
AutoConfigurationImportSelector.getAutoConfigurationEntry方法的触发自动配置类过滤完毕的事件fireAutoConfigurationImportEvents(configurations, exclusions);这句代码。

我们直接点进fireAutoConfigurationImportEvents方法看看其是如何触发事件的:

// AutoConfigurationImportSelector.java

private void fireAutoConfigurationImportEvents(List<String> configurations,
        Set<String> exclusions) {
    // 从spring.factories总获取到AutoConfigurationImportListener即ConditionEvaluationReportAutoConfigurationImportListener
    List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners(); 
    if (!listeners.isEmpty()) {
        // 新建一个AutoConfigurationImportEvent事件
        AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
                configurations, exclusions);
        // 遍历刚获取到的AutoConfigurationImportListener
        for (AutoConfigurationImportListener listener : listeners) {
            // 这里调用各种Aware方法用于触发事件前赋值,比如设置factory,environment等
            invokeAwareMethods(listener);
            // 真正触发AutoConfigurationImportEvent事件即回调listener的onXXXEveent方法。这里用于记录自动配置类的评估信息
            listener.onAutoConfigurationImportEvent(event); 
        }
    }
}

如上,fireAutoConfigurationImportEvents方法做了以下两件事情:

  1. 调用getAutoConfigurationImportListeners方法从spring.factoris配置文件获取实现AutoConfigurationImportListener接口的事件监听器;如下图,可以看到获取的是ConditionEvaluationReportAutoConfigurationImportListener

  1. 遍历获取的各个事件监听器,然后调用监听器各种Aware方法给监听器赋值,最后再依次回调事件监听器的onAutoConfigurationImportEvent方法,执行监听事件的逻辑。

此时我们再来看下ConditionEvaluationReportAutoConfigurationImportListener监听器监听到事件后,它的onAutoConfigurationImportEvent方法究竟做了哪些事情:

// ConditionEvaluationReportAutoConfigurationImportListener.java

public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) {
    if (this.beanFactory != null) {
        // 获取到条件评估报告器对象
        ConditionEvaluationReport report = ConditionEvaluationReport
                .get(this.beanFactory);
        // 将符合条件的自动配置类记录到unconditionalClasses集合中
        report.recordEvaluationCandidates(event.getCandidateConfigurations());
        // 将要exclude的自动配置类记录到exclusions集合中
        report.recordExclusions(event.getExclusions()); 
    }
}

可以看到,ConditionEvaluationReportAutoConfigurationImportListener监听器监听到事件后,做的事情很简单,只是分别记录下符合条件和被exclude的自动配置类。

7 AutoConfigurationPackages

前面已经详述了SpringBoot的自动配置原理了,最后的最后,跟SpringBoot自动配置有关的注解@AutoConfigurationPackage还没分析,我们来看下这个注解的源码:

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

可以看到@AutoConfigurationPackage注解是跟SpringBoot自动配置所在的包相关的,即将 添加该注解的类所在的package 作为 自动配置package 进行管理。

接下来我们再看看AutoConfigurationPackages.Registrar类是干嘛的,直接看源码:

//AutoConfigurationPackages.Registrar.java

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.singleton(new PackageImport(metadata));
    }
}

可以看到Registrar类是AutoConfigurationPackages的静态内部类,实现了ImportBeanDefinitionRegistrarDeterminableImports两个接口。现在我们主要来关注下Registrar实现的registerBeanDefinitions方法,顾名思义,这个方法是注册bean定义的方法。看到它又调用了AutoConfigurationPackagesregister方法,继续跟进源码:

// AutoConfigurationPackages.java

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
    if (registry.containsBeanDefinition(BEAN)) {
        BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
        ConstructorArgumentValues constructorArguments = beanDefinition
                .getConstructorArgumentValues();
        constructorArguments.addIndexedArgumentValue(0,
                addBasePackages(constructorArguments, packageNames));
    }
    else {
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(BasePackages.class);
        beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,
                packageNames);
        beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registry.registerBeanDefinition(BEAN, beanDefinition);
    }
}

如上,可以看到register方法注册了一个packageNames即自动配置类注解@EnableAutoConfiguration所在的所在的包名相关的bean。那么注册这个bean的目的是为了什么呢?
结合官网注释知道,注册这个自动配置包名相关的bean是为了被其他地方引用,比如JPA entity scanner,具体拿来干什么久不知道了,这里不再深究了。

8 小结

好了,SpringBoot的自动配置的源码分析就到这里了,比较长,有些地方也很深入细节,读完需要一定的耐心。

最后,我们再总结下SpringBoot自动配置的原理,主要做了以下事情:

  1. 从spring.factories配置文件中加载自动配置类;
  2. 加载的自动配置类中排除掉@EnableAutoConfiguration注解的exclude属性指定的自动配置类;
  3. 然后再用AutoConfigurationImportFilter接口去过滤自动配置类是否符合其标注注解(若有标注的话)@ConditionalOnClass,@ConditionalOnBean@ConditionalOnWebApplication的条件,若都符合的话则返回匹配结果;
  4. 然后触发AutoConfigurationImportEvent事件,告诉ConditionEvaluationReport条件评估报告器对象来分别记录符合条件和exclude的自动配置类。
  5. 最后spring再将最后筛选后的自动配置类导入IOC容器中

最后留个自己的疑问,还望知道答案的大佬解答,这里表示感谢

为了避免加载不必要的自动配置类造成内存浪费,FilteringSpringBootCondition用于过滤spring.factories文件的自动配置类,而FilteringSpringBootCondition为啥只有OnOnBeanCondition,OnClassConditiononWebApplicationCondition这三个条件类用于过滤,为啥没有onPropertyCondtiononResourceCondition等条件类来过滤自动配置类呢?

下节预告:
SpringBoot的启动流程是怎样的?--SpringBoot源码(五)

原创不易,帮忙点个赞呗!

由于笔者水平有限,若文中有错误还请指出,谢谢。

参考:

1,@AutoConfigurationPackage注解


欢迎关注【源码笔记】公众号,一起学习交流。

相关文章
|
11天前
|
Web App开发 编解码 Java
B/S基层卫生健康云HIS医院管理系统源码 SaaS模式 、Springboot框架
基层卫生健康云HIS系统采用云端SaaS服务的方式提供,使用用户通过浏览器即能访问,无需关注系统的部署、维护、升级等问题,系统充分考虑了模板化、配置化、智能化、扩展化等设计方法,覆盖了基层医疗机构的主要工作流程,能够与监管系统有序对接,并能满足未来系统扩展的需要。
42 4
|
9天前
|
运维 监控 安全
云HIS医疗管理系统源码——技术栈【SpringBoot+Angular+MySQL+MyBatis】
云HIS系统采用主流成熟技术,软件结构简洁、代码规范易阅读,SaaS应用,全浏览器访问前后端分离,多服务协同,服务可拆分,功能易扩展;支持多样化灵活配置,提取大量公共参数,无需修改代码即可满足不同客户需求;服务组织合理,功能高内聚,服务间通信简练。
26 4
|
2天前
|
缓存 Java 开发者
10个点介绍SpringBoot3工作流程与核心组件源码解析
Spring Boot 是Java开发中100%会使用到的框架,开发者不仅要熟练使用,对其中的核心源码也要了解,正所谓知其然知其所以然,V 哥建议小伙伴们在学习的过程中,一定要去研读一下源码,这有助于你在开发中游刃有余。欢迎一起交流学习心得,一起成长。
|
5天前
|
JavaScript Java 大数据
springboot高精度UWB定位系统源码
UWB (ULTRA WIDE BAND,) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。UWB定位系统依托在移动通信,雷达,微波电路,云计算与大数据处理等专业领域的多年积累,自主研发,开发并产业化的一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的优点。
10 0
|
13天前
|
人工智能 移动开发 前端开发
Springboot医院智慧导诊系统源码:精准推荐科室
医院智慧导诊系统是在医疗中使用的引导患者自助就诊挂号,在就诊的过程中有许多患者不知道需要挂什么号,要看什么病,通过智慧导诊系统,可输入自身疾病的症状表现,或选择身体部位,在经由智慧导诊系统多维度计算,精准推荐科室,引导患者挂号就诊,实现科学就诊,不用担心挂错号。
26 2
|
13天前
|
存储 消息中间件 Java
基于Springboot框架+云计算的区域HIS云平台源码
基于云计算技术的B/S架构的HIS系统,为基层医院机构提供标准化的、信息化的、可共享的医疗信息管理系统,实现医患事务管理和临床诊疗管理等标准管理信息系统的功能。系统利用健康云计算平台的技术优势,建立统一的健康档案存储平台,有效实现医院数据共享与交换,解决数据重复采集及信息孤岛等问题,为实现区域卫生信息化平台奠定了基础。
40 8
|
14天前
|
人工智能 前端开发 Java
Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
智慧导诊解决盲目就诊问题,减轻分诊工作压力。降低挂错号比例,优化就诊流程,有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位,了解对应病症信息和推荐就医科室。
161 10
|
14天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
15天前
|
存储 数据可视化 安全
Java全套智慧校园系统源码springboot+elmentui +Quartz可视化校园管理平台系统源码 建设智慧校园的5大关键技术
智慧校园指的是以物联网为基础的智慧化的校园工作、学习和生活一体化环境,这个一体化环境以各种应用服务系统为载体,将教学、科研、管理和校园生活进行充分融合。无处不在的网络学习、融合创新的网络科研、透明高效的校务治理、丰富多彩的校园文化、方便周到的校园生活。简而言之,“要做一个安全、稳定、环保、节能的校园。
39 6
|
16天前
|
数据采集 机器学习/深度学习 自然语言处理
springboot药物不良反应智能监测系统源码
报告详情包含患者基本信息、监测结果、指标数据、医嘱数据,同时展示命中指标趋势图,以及医嘱使用周期,辅助药师做准确判断。
20 4