当前位置: 首页 > news >正文

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的相关类图,好有个整体的理解。看下图:

b1f939db01ab4d8873c2a34bbfb15dbf.png

图1

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

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

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

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

EnableAutoConfiguration=XxxAutoConfiguration

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

5493143cc8f71b3c14c866690cf22e71.png

图2

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

4 分析SpringBoot自动配置原理

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

先看一下getImports方法代码:

// ConfigurationClassParser.javapublic Iterable 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的内部类,主要用来处理自动配置相关的逻辑,拥有process和selectImports方法,然后拥有entries和autoConfigurationEntries集合属性,这两个集合分别存储被处理后的符合条件的自动配置类,我们知道这些就足够了;
  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属性,默认返回trueif (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}// 获得@Congiguration标注的Configuration类即被审视introspectedClass的注解数据,// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)// 将会获取到exclude = FreeMarkerAutoConfiguration.class和excludeName=""的注解数据AnnotationAttributes attributes = getAttributes(annotationMetadata);// 【1】得到spring.factories文件配置的所有自动配置类List configurations = getCandidateConfigurations(annotationMetadata,attributes);// 利用LinkedHashSet移除重复的配置类configurations = removeDuplicates(configurations);// 得到要排除的自动配置类,比如注解属性exclude的配置类// 比如:@SpringBootApplication(exclude = FreeMarkerAutoConfiguration.class)// 将会获取到exclude = FreeMarkerAutoConfiguration.class的注解数据Set 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】 最后再将符合条件的自动配置类返回。

3d0c907898496e98ad316911c4493d34.png

图3

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

// AutoConfigurationImportSelector.javaprivate List filter(List 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或OnWebApplicationConditioninvokeAwareMethods(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为trueskipped = true; }}} // 这里表示若所有自动配置类经过OnBeanCondition,OnClassCondition和OnWebApplicationCondition过滤后,全部都匹配的话,则全部原样返回if (!skipped) {return configurations;}// 建立result集合来装匹配的自动配置类List 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);}

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

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

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

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

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

// AutoConfigurationImportSelector$AutoConfigurationGroup.javapublic Iterable selectImports() {if (this.autoConfigurationEntries.isEmpty()) {return Collections.emptyList();} // 这里得到所有要排除的自动配置类的set集合Set allExclusions = this.autoConfigurationEntries.stream().map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());// 这里得到经过滤后所有符合条件的自动配置类的set集合Set 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方法用来过滤不符合条件的自动配置类。

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

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

046355ff8915c7ef4f60d208dad8ab23.png

图4

可以看到,AutoConfigurationImportFilter接口有一个具体的实现类FilteringSpringBootCondition,FilteringSpringBootCondition又有三个具体的子类:OnClassCondition,OnBeanCondtition和OnWebApplicationCondition。

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

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

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

// FilteringSpringBootCondition.java@Overridepublic 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]);// 并将不匹配情况记录到reportif (report != null) {report.recordConditionEvaluation(autoConfigurationClasses[i], this,outcomes[i]);}}}return match;}

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

5.1 OnClassCondition

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

// OnClassCondition.javaprotected 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;}

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

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

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

5.1.1 createOutcomesResolver

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

// OnClassCondition.javaprivate 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.javaprivate 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.javapublic 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.StreamsBuilderString 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.javaprivate 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;}

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

我们继续跟进ClassNameFilter的源码:

// FilteringSpringBootCondition.javaprotected enum ClassNameFilter {// 这里表示指定的类存在于类路径中,则返回truePRESENT {@Overridepublic boolean matches(String className, ClassLoader classLoader) {return isPresent(className, classLoader);}},// 这里表示指定的类不存在于类路径中,则返回trueMISSING {@Overridepublic 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();}// 利用类加载器去加载相应类,若没有抛出异常则说明类路径中存在该类,此时返回truetry {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.javapublic 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的过滤,还要经过OnBeanCondition和OnWebApplicationCondition这两个条件类的过滤,这里不再详述,有兴趣的小伙伴可自行分析。

6 AutoConfigurationImportListener

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

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

// AutoConfigurationImportSelector.javaprivate void fireAutoConfigurationImportEvents(List configurations,Set exclusions) {// 从spring.factories总获取到AutoConfigurationImportListener即ConditionEvaluationReportAutoConfigurationImportListenerList listeners = getAutoConfigurationImportListeners(); if (!listeners.isEmpty()) {// 新建一个AutoConfigurationImportEvent事件AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,configurations, exclusions);// 遍历刚获取到的AutoConfigurationImportListenerfor (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.javapublic 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.javastatic class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {    @Override    public void registerBeanDefinitions(AnnotationMetadata metadata,    BeanDefinitionRegistry registry) {    register(registry, new PackageImport(metadata).getPackageName());    }        @Override    public Set determineImports(AnnotationMetadata metadata) {    return Collections.singleton(new PackageImport(metadata));    }}

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

// AutoConfigurationPackages.javapublic 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,OnClassCondition和onWebApplicationCondition这三个条件类用于过滤,为啥没有onPropertyCondtion,onResourceCondition等条件类来过滤自动配置类呢?

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

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

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

参考:

1,@AutoConfigurationPackage注解


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

相关文章:

  • etw系统provider事件较多_使用Spring Gateway和KeyCloak构建一个OIDC认证系统
  • @data注解_一枚 架构师 也不会用的Lombok注解,相见恨晚
  • java中int和integer的区别_Java中关于强、软、弱、虚引用的区别
  • android 投屏_[Android] 虫洞手机投屏电脑(支持键盘映射和传声音)
  • 云丁智能锁说明书_真硬核!行业爆发前夜,这把锁登上航母
  • python调用node_node-python:在nodejs中调用python代码
  • python多线程读取文件内容_python多线程读取logcat内容,导致其他线程阻塞
  • python闭包满足的三个条件_Python中的闭包
  • javascript等待异步线程完成_程序员修神之路--问世间异步为何物?
  • python取随机数画图_python3测试工具开发快速入门教程1turtle绘图-4选择与随机数...
  • python中如何输入多行字符_python中怎么输入多行字符串
  • python饼图显示百分比_解决echarts饼图显示百分比,和显示内容字体及大小
  • java 二维数组定义长方体_47.二维数组的定义
  • zap 自定义日志格式_Go 每日一库之 zap
  • python接管已经打开ie浏览器_Python Webdriver 重新使用已经打开的浏览器实例
  • ----------
  • __proto__ 和 prototype的关系
  • 【347天】每日项目总结系列085(2018.01.18)
  • 〔开发系列〕一次关于小程序开发的深度总结
  • Java 网络编程(2):UDP 的使用
  • Laravel Mix运行时关于es2015报错解决方案
  • PHP变量
  • Redis在Web项目中的应用与实践
  • 基于Volley网络库实现加载多种网络图片(包括GIF动态图片、圆形图片、普通图片)...
  • 软件开发学习的5大技巧,你知道吗?
  • 适配mpvue平台的的微信小程序日历组件mpvue-calendar
  • ​Kaggle X光肺炎检测比赛第二名方案解析 | CVPR 2020 Workshop
  • # Apache SeaTunnel 究竟是什么?
  • # Panda3d 碰撞检测系统介绍
  • #WEB前端(HTML属性)
  • (11)MATLAB PCA+SVM 人脸识别
  • (11)工业界推荐系统-小红书推荐场景及内部实践【粗排三塔模型】
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (二)springcloud实战之config配置中心
  • (附源码)计算机毕业设计SSM教师教学质量评价系统
  • (免费领源码)python#django#mysql公交线路查询系统85021- 计算机毕业设计项目选题推荐
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • (转)用.Net的File控件上传文件的解决方案
  • (轉貼) 寄發紅帖基本原則(教育部禮儀司頒布) (雜項)
  • .[hudsonL@cock.li].mkp勒索加密数据库完美恢复---惜分飞
  • .cfg\.dat\.mak(持续补充)
  • .net core开源商城系统源码,支持可视化布局小程序
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .net实现客户区延伸至至非客户区
  • .NET中winform传递参数至Url并获得返回值或文件
  • .set 数据导入matlab,设置变量导入选项 - MATLAB setvaropts - MathWorks 中国
  • [ Linux ] Linux信号概述 信号的产生
  • [2019.3.5]BZOJ1934 [Shoi2007]Vote 善意的投票
  • [30期] 我的学习方法
  • [Angular] 笔记 16:模板驱动表单 - 选择框与选项
  • [Angularjs]ng-select和ng-options
  • [Ariticle] 厚黑之道 一 小狐狸听故事
  • [BUUCTF NewStarCTF 2023 公开赛道] week4 crypto/pwn
  • [BZOJ 2142]礼物(扩展Lucas定理)