开始之前
- 需要SpringBoot的demo项目,用于验证自动配置
- 需要熟悉java注解的相关的知识
- SpringBoot 2.2.13RELEASE的源码调试环境(该文章所使用的源码都来自该版本)
下载地址为:https://github.com/spring-projects/spring-boot/tree/v2.2.13.RELEASE
SpringBoot源码目录结构说明
- spring-boot-build:项目根目录
- spring-boot-build/Spring-boot-tests:这个模块SpringBoot的测试模块,跟部署测试和集成测试有关。
- spring-boot-build/spring-boot-project:SpringBoot框架的源码目录,因此我们重点分析该目录
- spring-boot: 该模块是SpringBoot项目的核心,包含启动类
SpringApplication
,外部传参支持java -jar test.jar --server.port=80
,以及各种启动的初始化逻辑 - spring-boot-parent: 这个模块是SpringBoot的父项目,被其他模块依赖,该模块下没有代码
- spring-boot-actuator:
- spring-boot: 该模块是SpringBoot项目的核心,包含启动类
Spring Boot自动配置原理
条件注解
SpringBoot的自动配置是需要满足一定条件才会进行自动配置的,这与注解ConditionalOnXXX
密切相关。下面先以一个具体的例子进行说明,再探究原理。
ConditionalOnAuto
首先开发一个Spring Boot的demo项目,
我们自定义一个注解ConditionalOnAuto
,当该注解的method字段为auto
时自动配置bean
ConditionalOnAuto
注解代码如下:
|
|
该注解有两个属性:method
和value
- 定义类
AutoCondition
实现Condition
接口,实现其matches()
方法,代码如下:
|
|
这个map的key为注解的字段,value为注解的值,类型是LinkedList
这里我们定义的字段为字符串,使用getFirst
获取第一个值即可,当method值为auto
返回true,否则返回false
matches
方法返回true时才会创建对应的bean
- 给
ConditionalOnAuto
注解添加Conditional
注解,绑定Condition接口的实现类,ConditionalOnAuto
注解修改后代码如下:
|
|
此时注解@ConditionalOnAuto
是注解@Conditional
的派生注解,与@Conditional(AutoCondition.class)
是等价的
- 下面测试自定义注解是否生效,定义资源如下:
首先创建任意类A
,代码如下:
|
|
创建配置类AConfig
,添加@ConditionalOnAuto
注解,代码如下:
|
|
测试时我们会修改@ConditionalOnAuto
注解的method
的值
创建测试接口TestController
,代码如下:
|
|
当注解@ConditionalOnAuto
的method
的值为auto
时,访问接口控制台打印为a的地址为:com.cx.config.A@619f2afc
当注解@ConditionalOnAuto
的method
的值不为auto
时,访问接口控制台打印为a的地址为:null
,符合我们的预期。
- 我们来梳理下上述测试代码的逻辑:
AutoCondition
实现了Condition
接口的matches
方法,获取注解ConditionalOnAuto
的对应值判断是否创建bean(返回true创建)- 根据注解的定义形式,
@ConditionalOnAuto
与@Conditional(AutoCondition.class)
是等价的 - 最后我们定义了一个配置类
AConfig
,对应方法添加注解@ConditionalOnAuto(value = "aaaaa", method = "auto")
- 所以
@ConditionalOnAuto
注解真正起作用的是Condition接口的具体实现类AutoCondition
的matches
方法 - Springboot在启动时执行了
Condition
接口的实现类AutoCondition
的matches
方法
上面提到,
@ConditionalOnAuto
与@Conditional(AutoCondition.class)
是等价的。既然等价,当然可以直接替换。但是,上面实现的
matches
方法依赖@ConditionalOnAuto
的字段的值,无法直接进行替换,倘若matches
方法是从yaml中获取值,两个注解就可以正常替换了根据上述代码,我们能模糊地感受到为什么springboot引入中间件后只需要yaml中进行简单配置就能直接用
@AutoWire
进行使用了
SpringBootCondition
上一节我们自定义注解并最终实现了Condition
接口,Spring Boot有没有内置Condition接口实现类呢?有的,SpringBootCondition
类
SpringBootCondition
类位于spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/SpringBootCondition.java
有很多OnXXXCondition
类继承了SpringBootCondition
类,它们对应的注解为@ConditionalOnXXX
SpringBootCondition
的matches
方法源码如下:
|
|
SpringBootCondition
是一个抽象类,matches
的实现代码较少,有五个方法:getClassOrMethodName
获取类名和方法名,逻辑为如果是ClassMetadata
类型则返回类名,否则返回类名+方法名,具体实现如下:
1 2 3 4 5 6 7 8
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(); }
getMatchOutcome
是SpringBootCondition
的抽象方法,具体实现在子类OnXXXCondition
中,具体逻辑是判断自身配置类的条件注解@ConditionalOnXXX
是否满足条件,然后记录到ConditionOutcome中logOutcome(classOrMethodName, outcome);
作用是打印Condition是否满足条件的日志,如匹配会打印matched
,不匹配会打印did not match
recordEvaluation(context, classOrMethodName, outcome);
是否匹配的评估信息记录到ConditionEvaluationReport
中outcome.isMatch()
直接调用了outcome的match字段,满足条件则返回true,否则返回false,子类实现getMatchOutcome
时会给该字段赋值- 即
SpringBootCondition
封装了一个模板方法getMatchOutcome(context, metadata)
供子类OnXXXCondition
实现具体的判断逻辑,因此SpringBootCondition
的matches
方法的作用除了调用getMatchOutcome(context, metadata)
外, 就是打印和记录是否满足匹配条件的评估信息
OnResourceCondition
OnResourceCondition
继承SpringBootCondition
实现了其getMatchOutcome
方法,对应的注解为@ConditionalOnResource
它们的源代码都位于spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/
下
- 注解
@ConditionalOnResource
的源代码如下:
|
|
注解中有个resources
,根据前面的经验,该字段要满足一定条件才会创建对应的bean
OnResourceCondition
的源码实现了getMatchOutcome
,代码如下:
|
|
OnResourceCondition
的代码逻辑与我们自定义实现大同小异,实现逻辑也比较简单
OnResourceCondition
的判断逻辑是拿到@ConditionalOnResource
注解指定的资源路径后,然后用ResourceLoader根据指定路径去加载看资源存不存在
注解中的资源路径是一个List,所有路径都存在是返回true
- 源码中的其他条件注解举例:
|
|
上面是springboot中mongo的配置源码,根据我们的学习,基本能够直接明白@ConditionalOnClass({ MongoClient.class, ReactiveMongoTemplate.class })
、@ConditionalOnBean(MongoClient.class)
和@ConditionalOnMissingBean(ReactiveMongoDatabaseFactory.class)
的作用,它们的区别只是有些作用于类上,有些作用于方法上
我们自定义的条件类也可以不直接实现Condition接口,而直接继承SpringBootCondition
Springboot中有大量的形如
@ConditionalOnXXX
的条件注解,它们的实现类为OnXXXCondition
Springboot就是根据这些派生的条件注解进行自动配置的,Springboot是在何时执行那些Condition接口实现类的matches方法呢?我们在后面分析
@SpringBootApplication注解
先看一下下面这段代码:
|
|
我们对上面代码非常熟悉,它是SpringBoot应用的启动类,并标有@SpringBootApplication
注解
@SpringBootApplication
是SpringBoot的重要注解,与自动配置息息相关,源代码如下:
|
|
该注解包含多个其他注解,顾名思义,@EnableAutoConfiguration
该注解与自动配置有关
@EnableAutoConfiguration
注解源码如下:
|
|
该注解上标有@AutoConfigurationPackage
和@Import(AutoConfigurationImportSelector.class)
这两个注解
这里可以猜测自动配置可能与类AutoConfigurationImportSelector
有关
下面我们来分析AutoConfigurationImportSelector
类和@AutoConfigurationPackage
注解
-
AutoConfigurationImportSelector
实现了DeferredImportSelector
接口,DeferredImportSelector
接口又继承了ImportSelector
接口,即实现了该接口的selectImports
方法,selectImports
的源代码如下:1 2 3 4 5 6 7 8 9 10
public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader .loadMetadata(this.beanClassLoader); AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); }
selectImports
方法的具体逻辑由四个方法组成,我们来进行逐个分析:- 首先isEnabled方法返回false,
selectImports
方法直接返回NO_IMPORTS
字符串数组,即不导入,isEnabled
方法代码如下:
1 2 3 4 5 6
protected boolean isEnabled(AnnotationMetadata metadata) { if (getClass() == AutoConfigurationImportSelector.class) { return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true); } return true; }
判断是否是AutoConfigurationImportSelector类型,如果不是返回true,如果是则从environment获取
spring.boot.enableautoconfiguration
,为null则取默认值true即当我们显示指定
spring.boot.enableautoconfiguration
为false时关闭自动配置 2. loadMetadata方法会获取META-INF/spring-autoconfigure-metadata.properties
的值 3. getAutoConfigurationEntry方法获取了需要导入的配置类集合和需要排除的配置类集合,该方法极为重要,请参照注释在自己的源码环境一行行调试阅读,代码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } //获取EnableAutoConfiguration中的参数,exclude()/excludeName() AnnotationAttributes attributes = getAttributes(annotationMetadata); //会获取META-INF/spring.factories文件中需要自动装配的配置类, //文件内容形如org.springframework.boot.autoconfigure.EnableAutoConfiguration=/com.xx.libs.middleware.DBSource //还会获取SpringBoot默认的自动配置类,如RabbitAutoConfiguration,JdbcTemplate,Redis等大概200多个 //具体逻辑是从一个map缓存中获取,key为org.springframework.boot.autoconfigure.EnableAutoConfiguration,第一次没有值,则去生成这两种自动配置类路径,并存入缓存 //该函数被多个地方调用,缓存cache不只与自动配置有关,还有各种Listener、Initializer等等,attributes参数传入后并没有使用,不知道原因 //这里获取的自动配置类包括SpringBoot默认的+META-INF/spring.factories中的 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); //通过set去重再转为list configurations = removeDuplicates(configurations); //获取exclude和excludeName的值,返回set是两者可能有重复 Set<String> exclusions = getExclusions(annotationMetadata, attributes); //检查需要排除的类是否再configurations中,不在的筛选到一个集合,此集合不为空的话抛出异常,异常中说明这些类不需要排除,因为它们不会自动配置 //即不能排除一个不会自动配置的类 checkExcludedClasses(configurations, exclusions); //从configurations去除需要排除的类集合exclusions configurations.removeAll(exclusions); //autoConfigurationMetadata是外面在META-INF/spring-autoconfigure-metadata.properties获取并传入的 //filter对configurations进行过滤,剔除掉不满足 spring-autoconfigure-metadata.properties所写条件的配置类 configurations = filter(configurations, autoConfigurationMetadata); //监听器import事件回调,向每个监听者发送configurations和exclusions组合成event的评估信息? fireAutoConfigurationImportEvents(configurations, exclusions); //返回configurations和exclusions组, //注意此时configurations已经排除了exclusions并过滤了不满足META-INF/spring-autoconfigure-metadata.properties的配置类 return new AutoConfigurationEntry(configurations, exclusions); }
filter
方法的作用也是过滤,源码如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) { long startTime = System.nanoTime(); String[] candidates = StringUtils.toStringArray(configurations); boolean[] skip = new boolean[candidates.length]; boolean skipped = false; //分割线上部分只是处理需要过滤的路径,逻辑较简单,请自行阅读 //============================================================================================= for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) { invokeAwareMethods(filter); boolean[] match = filter.match(candidates, autoConfigurationMetadata); for (int i = 0; i < match.length; i++) { if (!match[i]) { skip[i] = true; candidates[i] = null; skipped = true; } } } //============================================================================================== if (!skipped) { return configurations; } List<String> result = new ArrayList<>(candidates.length); for (int i = 0; i < candidates.length; i++) { 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); }
回顾一下,configurations中是自动配置的类路径
for循环getAutoConfigurationImportFilters()获取了一个filter集合,并调用了match方法返回一个布尔数组,对每个类路径而言,必须匹配所有filter才能不被过滤,即返回的布尔数组都为true。
getAutoConfigurationImportFilters()
的外层源码如下:1 2 3
protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() { return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader); }
- 如果仔细阅读过前面从缓存cache中获取自动配置类的源码,也是调用的
SpringFactoriesLoader.loadFactories()
- 这里传入了一个
AutoConfigurationImportFilter.class
,内层代码会获取类路径为org.springframework.boot.autoconfigure.AutoConfigurationImportFilter
,以该路径为key,也从缓存cache中进行获取 - 这时cache已经有值了,类型为LinkedList,有三个值:
org.springframework.boot.autoconfigure.condition.OnBeanCondition
、org.springframework.boot.autoconfigure.condition.OnClassCondition
、org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
,终于看到了我们熟悉的地方,这三个不就是条件注解的实现类嘛!通过源码搜索发现又有不一样,他没有直接继承SpringBootCondition
类或实现Condition
接口,而是继承FilteringSpringBootCondition
类,而FilteringSpringBootCondition
类继承了SpringBootCondition
类并实现了AutoConfigurationImportFilter
接口的match
方法 - 后面调用
instantiateFactory()
通过反射创建了实例,最终返回List<AutoConfigurationImportFilter>
,这样我们就清楚了for循环中getAutoConfigurationImportFilters()
获取的三个filter到底是什么了
我们来回顾一下之前的条件注解,条件注解类开始通过实现
Condition
接口进行使用,后来在SpringBoot源码中发现条件注解类通过继承SpringBootCondition
类,SpringBootCondition
类实现了Condition
接口的matches
方法,并提供给一个抽象方法,getMatchOutcome
供子类实现,matches
方法中调用了getMatchOutcome
,还增加了记录评估日志的能力(扩展了子类功能),这就是模板方法模式。OnBeanCondition
、OnClassCondition
、OnWebApplicationCondition
继承了FilteringSpringBootCondition
类,FilteringSpringBootCondition
类也提供了一个抽象方法getOutcomes
,并在实现了AutoConfigurationImportFilter
的match
方法中调用了getOutcomes
,这里也打印和记录了评估日志,并返回布尔数组,熟悉的模板方法模式- 回顾一下
ConditionOutcome
对象有两个字段,match布尔值记录是否满足条件,message记录评估日志,FilteringSpringBootCondition
的match
方法源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory); ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata); 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]); } } } return match; }
代码逻辑比较简单,入参autoConfigurationClasses即是最外层
filter.match()
传入,为SpringBoot默认的+META-INF/spring.factories中的自动配置类,
调用getOutcomes
获取ConditionOutcome
数组,返回所有的match值,getOutcomes
由子类实现,这里只对其中一个进行分析,OnClassCondition
顾名思义某些类存在时才会创建对象,源码如下:1 2 3 4 5 6 7 8 9 10 11 12
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { // 如果有多个处理器可用,则拆分工作并在后台线程中执行一半。使用一个额外的线程似乎可以提供最佳的性能。线程越多,情况就越糟 if (Runtime.getRuntime().availableProcessors() > 1) { return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata); } else { OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0, autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader()); return outcomesResolver.resolveOutcomes(); } }
- 该源码逐条分析的话,又会是一大段,直接说结论会从一个Properties中查询某些值,比如SpringBoot默认加载自动配置
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
,这是 configurations中的一个值,即OnClassCondition
中查询个Properties的key为org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.OnClassCondition
,如果不存在则返回null, 如果存在则返回字符串org.springframework.amqp.rabbit.core.RabbitTemplate,com.rabbitmq.client.Channel
,两个类路径用逗号分割,如果你要问我值是怎么来的,细心的同学会发现SpringBoot源码RabbitAutoConfiguration
类上有注解@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
, 再结合代码上下文即可猜出。 - 与此同时,我们也能推断出什么时候会返回null,该类没有
OnWebApplicationCondition
对应的注解,即返回null,这与FilteringSpringBootCondition
的match
方法中的match[i] = (outcomes[i] == null || outcomes[i].isMatch());
一致, ,为空或match为true都会返回true - 拿到具体的值后以逗号分隔符拆分开,并去搜索类是否存在,不存在时评估日志message会有这样的记录:
@ConditionalOnClass did not find required class 'com.rabbitmq.client.Channel
@ConditionalOnClass did not find required class 'org.springframework.amqp.rabbit.core.RabbitTemplate
, 如果你要问什么时候不存在,大概是maven没有导入相关依赖时,这时我们对SpringBoot为什么导入依赖,配置yaml就能直接@Autowire
使用有了更深的了解,这就是自动配置的原理
- 终于分析完了
filter
方法的过滤逻辑,``StringUtils.toStringArray(autoConfigurationEntry.getConfigurations())`直接返回了最终的configurations的字符串集合
- 首先isEnabled方法返回false,
-
总结一下,本节分析了从
@SpringBootApplication
到@EnableAutoConfiguration
到@Import(AutoConfigurationImportSelector.class)
,最终学习了类AutoConfigurationImportSelector
的selectImports
方法,该方法获取SpringBoot内置自动配置类+META-INF/spring.factories中需要自动配置的类,通过第一次exclude
和excludeName
过滤,再通过第二次filter过滤,得到了最终需要自动配置的类路径
新的问题:
SrpingBoot在启动时是如何执行到selectImports
的呢?META-INF/spring.factories中需要自动配置的类通过读取文件获取,SpringBoot内置自动配置类的类路径是如何获取的呢?(上文没有对这个细节具体分析)
第二次filter过滤如何执行到条件注解的实现方法的?(我们可以推测,通过反射+类路径,可以获取类上的注解和注解的值,该值是一个class,即条件注解实现类,通过反射创建实例,通过执行实例方法返回的布尔值判断是否需要创建原实例)
自动配置测试
本节对上一节的内容进行测试
- 创建
B
、C
、D
、E
四个类,形如下面的代码:
|
|
- 在
resources
目录下创建META-INF/spring.factories
,在spring.factories
文件中添加如下内容:
|
|
路径与自己的保持一致
- 创建测试代码如下:
|
|
启动SpringBoot项目访问接口,控制台打印如下:
|
|
与期望一致
- 设置
EnableAutoConfiguration
注解的exclude
和excludeName
,发现启动类只使用了SpringBootApplication
注解,@EnableAutoConfiguration
注解是@SpringBootApplication
注解的元注解(注解的注解称为元注解),@SpringBootApplication
源码如下:
|
|
@AliasFor
用于定义注解属性别名,此时相当于@SpringBootApplication
的exclude
和excludeName
分别是@EnableAutoConfiguration
的exclude
和excludeName
的别名,因此设置
@SpringBootApplication
的属性即可,修改代码如下:
|
|
我们通过两种方式分别排除了类D
和类E
的自动配置,此时重启SpringBoot项目打印类D
和类E
的实例地址应该为空,控制台打印如下:
|
|
与期望一致,这种方式是通过第一次过滤实现的
- 我们能不能通过第二次过滤实现呢?答案是可以的。
回忆一下filter
函数,我们获取了三个AutoConfigurationImportFilter
,必须全部为true才不会过滤,它们的实际类型分别是OnBeanCondition
、OnClassCondition
、OnWebApplicationCondition
, 它们配置在SpringBoot源码spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
下,内容如下:
|
|
我们也能通过扩展AutoConfigurationImportFilter
接口并配置实现过滤,在SpringBoot的demo项目添加类MyFilter
,代码如下:
|
|
这段代码会获取resources
下application.yml
中c-auto-configuration.enabled
的值,只有显示指定false时才不会自动注入
还需要添加一些配置
resources/META-INF/spring.factories
修改后如下所示:
|
|
resources/application.yml
增加如下配置
|
|
启动demo程序,访问接口控制台打印如下:
|
|
符合我们的期望,该方式是通过第二次过滤实现的
- 倘若现在有新的需求,只有当有类
C
的实例时才自动配置类B
,可以使用注解@ConditionalOnBean
,修改类B
代码如下:
|
|
该注解只有使用在@Configuration
修饰的类和@bean
修饰的方法上才生效,启动demo程序控制台打印如下:
|
|
上一步我们已经过滤了类C
的自动配置,因此类B
不会创建实例,符合我们的期望