Spring 配置类的解析
封面来源:碧蓝航线 飓风与沉眠之海 活动CG
0. 前言
0.1 我的近况
转眼间,又到八月底。最近两个月我一直想在一个月内完成 4 次周更,实际情况是仅仅依靠周末两天完成一篇博客还是太难。一方面,我自己也是“半桶水”,写的过程中也是在学习,反复地查阅各种资料是不可避免的,在这个过程,不仅要验证它们,还要把它们转换成自己的内容;另一方面,自己多年来的坏习惯也让我得到了反噬,坐立不安、心率加快时长发生,担心自己下一秒就会因此丧命,每当此时只得放下手中任何娱乐与学习,静躺在床上试图保持镇静以便缓解症状。
这周去了一趟华西,医师似乎对我这种症状见怪不怪,告知我应当每天花费一定的时间去运动,然后就开了一系列检查,这些检查也是需要预约的,运气还不错的我约到了第二天。我记得有个段子,一位外地游客到成都游玩,上了出租车后便让司机带他去成都最热闹的地方,而后司机把他带到了华西,这足以证明华西在西南人民心中的地位。尽管我去的并不是华西本部(但也是影分身),检查当天也早早到了医院,但几乎每项检查都要进行一段时间的排队。
昨晚查阅了检查的电子报告,似乎没有多大问题,但情况究竟如何还要在下周二复查后才能得知。
朋友,衷心祝愿你能够时刻保持身体健康,对了,不要忘记勤加运动、拒绝熬夜。(2024-08-18 记)
0.2 内容概述
言归正传,在 上一篇 中熟悉了 BeanDefinition
中的相关 API,BeanDefinition
是 Spring 中绕不开的一个类,熟悉它的 API 对后续的源码阅读是非常有帮助的,甚至我认为阅读 Spring 源码的第一步就应该是了解 BeanDefinition
中 API 的使用。
经历 BeanDefinition
这个插曲,本文将按计划完成讲解 SpringBoot 自动配置前的最后一块拼图 —— Spring 配置类的解析。
开门见山,Spring 配置类的解析涉及到 ConfigurationClassPostProcessor
类,这个类也与 SpringBoot 的自动配置机制密切相关,因此决定单独成文,深入理解 ConfigurationClassPostProcessor
的作用。
除此之外,先前已经介绍过 @Configuration
注解的 Full 模式和 Lite 模式,其实现也是由 ConfigurationClassPostProcessor
完成的,因此本文在涉及到这些内容时会直接跳过,详情参考 @Configuration 注解的那些事。
0.3 阅读建议
Spring 配置类的解析步骤涉及的类与方法的数量非常多,本文行文过程中经常会跳转到主线以外的类或方法,之后经过一番介绍后又回归主线,仅通过对阅读本文难以把握主线,因此推荐将本文作为阅读源码的辅助资料,结合对示例的 Debug 摸清行文逻辑,最终完成对 ConfigurationClassPostProcessor
类的进一步理解。
0.4 术语解释
为了行文的精简,对文中使用的部分术语解释如下:
- 配置类:被
@Configuration
注解标记的类 @Bean
方法、Bean 方法:被@Bean
注解标记的方法
1. 用法与示例
1.1 @Conditional 注解
1 |
|
@Conditional
只能够作用在类和方法上,并且在使用时需要指定一个 Condition
的 Class
数组。
只有当所有 Condition
指定的条件都满足时,对应的 Bean 才会被注册到 Spring 容器中。
可以通过以下方式来使用 @Conditional
注解:
- 作为类级别的注解,应用于任何直接或间接使用
@Component
注解的类,也包括配置类; - 作为元注解,用于组合自定义原型注解;
- 作为方法级别的注解,作用在任何
@Bean
方法上
如果一个配置类也被 @Conditional
注解标记,与该类关联的所有的 @Bean
方法、@Import
注解和 @ComponentScan
注解都将受到这些条件的约束。
注意: 不支持 @Conditional
注解的继承。来自父类或 被 重写方法的任何条件都不会被考虑。为了强制执行这些语义,@Conditional
注解为被元注解 @Inherited
修饰,此外,任何使用 @Conditional
作为元注解的自定义组合注解也不得被 @Inherited
修饰。
1.2 Condition 接口
1 |
|
Condition
是一个函数式接口,内部的 matches()
方法用于确定条件是否匹配,该方法接收两个参数,并返回一个 boolean
值。
如果返回 true
,对应的 Bean 将会被注册到 Spring 容器中,否则会跳过该 Bean 的注册。
接收的两个参数的含义如下:
context
:条件上下文,通过该参数可以获取到一些 Spring 容器等相关信息metadata
:用于获取正在校验的类或方法的元数据
Condition
的使用必须遵循与 BeanFactoryPostProcessor
相同的限制(简单来说就是 matches()
方法会在 BeanFactoryPostProcessor
所在的阶段执行),并且注意绝不能与 Bean 实例进行交互(重写的 matches()
方法内部不能与 Bean 实例交互,因为 matches()
方法的调用时机往往在 Bean 实例化之前)。如果要对配置类的注册条件进行更细粒度的控制,可以考虑实现 ConfigurationCondition
接口。
对于给定类或方法上的多个 Condition
,将根据实现的 Ordered
接口和使用 @Order
注解进行排序,排序规则参阅 AnnotationAwareOrderComparator
。
1.3 使用示例
示例参考:超详细分析Spring的@Conditional注解
项目结构如下:
1 | D:. |
也可以通过 链接 在 GitHub 上获取代码文件。
首先定义一系列
Condition
的实现类
1 |
|
除了 RepositoryCondition
返回 false
外,其余实现都是返回 true
。
通过
@Conditional
注解使用Condition
的实现类:
作为类级别的注解,作用于使用了 @Component
派生注解的类:
1 |
|
作为类级别的注解,作用在配置类上,观察内部的 @Bean
方法能够成功注册 Bean:
1 |
|
同样是在配置类中使用,作用在 @Import
的类上、@Bean
方法上:
1 |
|
作用在 @Bean
方法上有两种情况:
- 直接作用在对应的
@Bean
方法上,比如myService()
方法 - 作用在
@Bean
注册的类上,比如MyRepository
类
RepositoryCondition
实现的 matches()
方法返回的是 false
,那么 Spring 容器中应该不存在 MyRepository
类型的 Bean?
测试一下
1 |
|
indi.mofan.condition.ControllerCondition indi.mofan.condition.FurtherCondition indi.mofan.condition.ControllerCondition indi.mofan.condition.FurtherCondition indi.mofan.condition.DaoCondition indi.mofan.condition.ControllerCondition indi.mofan.condition.FurtherCondition indi.mofan.condition.DaoCondition indi.mofan.condition.ServiceCondition
运行测试方法后,发现 Spring 容器中存在 MyRepository
类型的 Bean,并且打印的日志中不存在 RepositoryCondition
的相关信息,也就是说,程序运行时,没有执行 RepositoryCondition
重写的 matches()
方法。在控制 @Bean
方法注册 Bean 时,在目标类上使用 @Conditional
注解是无效的,应当直接在 @Bean
方法上使用 @Conditional
注解。
2. 源码剖析
2.1 开门见山地说
Spring 配置类的解析由 ConfigurationClassPostProcessor
实现,除此之外,@Conditional
、@Component
及其派生注解、@Import
、@ComponentScan
、@Configuration
、@Bean
等与 Bean 注册相关的注解的实现都会在 ConfigurationClassPostProcessor
中完成。
简单回顾下 @Configuration 注解的那些事 中对 ConfigurationClassPostProcessor
的讲解:
ConfigurationClassPostProcessor
实现了BeanDefinitionRegistryPostProcessor
接口,后者是BeanFactoryPostProcessor
的子接口;BeanDefinitionRegistryPostProcessor
提供了动态注册新的BeanDefinition
的能力,与BeanFactoryPostProcessor
相比,它更早被调用;- 无论是对
BeanDefinitionRegistryPostProcessor
中独有方法的实现,还是对BeanFactoryPostProcessor
中方法的实现,内部都会调用名为processConfigBeanDefinitions()
的方法。
processConfigBeanDefinitions()
方法完成了各种与 Bean 注册相关注解的解析,该方法的实现较长,在 @Configuration 注解的那些事 一文中只对其中获取配置类信息的小部分代码进行了分析,而那剩下的部分则是本文的主要内容。
2.2 processConfigBeanDefinitions
1 | public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { |
processConfigBeanDefinitions()
方法首先会遍历当前 Spring 容器中所有的 BeanDefinition
,从中找出配置类相关的 BeanDefinition
,如果没找到,就立即返回。这些内容在 @Configuration 注解的那些事 一文中已经详细介绍过了,不再过多分析。
1 | public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { |
与配置相关的 BeanDefinition
可能有多个,将它们按照 @Order
的值进行排序。
之后还会探测在 ApplicationContext
(代码实现中使用的是 SingletonBeanRegistry
)内部是否存在自定义的 Bean
名称生成策略(bean name generation strategy),如果有的话,将它们设置给某些成员变量。
如果 environment
的值未被显式设置,会默认初始化为 StandardEnvironment
。
上面这些内容并不是重点,接下里的才是重头戏:解析每个配置类。 💣
2.3 解析每个配置类
继续阅读 processConfigBeanDefinitions()
方法的源码:
1 | public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { |
实例化一个 ConfigurationClassParser
对象,将先前得到的配置类信息传入其 parse()
方法中完成配置类的解析。
进入 parse()
方法内部:
1 | public void parse(Set<BeanDefinitionHolder> configCandidates) { |
通过判断配置类对应的 BeanDefinition
是哪种类型,然后调用不同的、重载的 parse()
方法。
在先前的示例代码中,配置类是通过 @Configuration
注解实现的,对应 AnnotatedBeanDefinition
,因此会直接进入第一个分支:
1 | protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException { |
将配置类的元数据信息和对应的 beanName
包装成 ConfigurationClass
实例,然后又调用了 processConfigurationClass()
方法:
1 | protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException { |
processConfigurationClass()
方法共有四步:
- 判断是否需要跳过当前配置类的解析, 这也是
@Conditional
注解的实现; - 如果已经解析过当前配置类,判断当前解析的配置类是否是被导入的:
- 如果不是被导入的,证明现在解析的配置类更加明确,移除先前解析过的同名配置类并重新解析;
- 如果当前配置类是被导入的,并且先前解析的配置类也是被导入的,那么合并这同名配置类;
- 如果当前配置类是被导入的,但是先前解析的配置类却不是被导入的,先前解析的配置类更加明确,直接
return
,忽略对当前配置类的解析。
- 递归处理配置类及其超类;
- 保存解析过的配置类信息。
重点放在第一步和第三步,其中:
- 第一步调用
conditionEvaluator
成员变量的shouldSkip()
方法来判断是否跳过配置类的解析 - 第三步的重点放在调用
doProcessConfigurationClass()
方法完成对配置类的解析
2.4 shouldSkip
@Conditional
注解的实现由 ConditionEvaluator#shouldSkip()
方法完成,成员变量 conditionEvaluator
的值是在实例化当前对象时一并实例化的:
1 | public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory, |
回到 shouldSkip()
方法中:
1 | public boolean shouldSkip( AnnotatedTypeMetadata metadata, |
shouldSkip()
方法的实现很简单,可以概括为三步:
- 通过反射获取
@Conditional
注解的value
属性对应的Condition
实例; - 对获取到的
Condition
实例排个序; - 遍历
Condition
实例,如果有一个条件不满足,就直接返回true
,表示跳过解析。
2.5 processConfigurationClass
回到 ConfigurationClassParser#processConfigurationClass()
方法中,配置类的解析由以下代码完成:
1 | protected void processConfigurationClass(ConfigurationClass configClass, |
首先利用 asSourceClass()
方法通过 ConfigurationClass
实例和过滤规则 filter
获取到 SourceClass
对象,解析配置类则是由 doProcessConfigurationClass()
方法完成。
asSourceClass()
方法
不详解 asSourceClass()
的实现,知道它是获取 SourceClass
实例的一个工厂方法就行了。
简单介绍下 SourceClass
:
1 | private class SourceClass implements Ordered { |
SourceClass
对类信息(类信息被封装到 Class
或 MetadataReader
中)进行了包装,使得带注解(注解信息被封装到 AnnotationMetadata
中)的类能够以统一的方式被处理,而不必关心这些类是怎么被加载的。
doProcessConfigurationClass()
方法
利用 asSourceClass()
构造出 SourceClass
对象后,会立即执行 doProcessConfigurationClass()
方法处理 SourceClass
对象(也就是配置类),而这个方法也会返回一个 SourceClass
对象,之后会判断返回值是否为 null
,如果不为 null
,又会执行该方法处理新返回的 SourceClass
对象。
在先前贴出的代码片段前有这样一段注释:
Recursively process the configuration class and its superclass hierarchy.
这段注释明确概括了这段代码的核心:递归 处理配置类和其超类层级结构(hierarchy 一词译为层级结构)。
也就是说 doProcessConfigurationClass()
返回的是当前处理类的超类对应的 SourceClass
对象?
该方法的源码的很长,每种处理前都有表明具体作用的注释,不如跟随这些注释来窥探其中的奥秘:
1 | if (configClass.getMetadata().isAnnotated(Component.class.getName())) { |
1 | // Process any @PropertySource annotations |
1 | // Search for locally declared @ComponentScan annotations first. |
1 | // Fall back to searching for @ComponentScan meta-annotations (which indirectly |
1 | // 5. 存在 @ComponentScan 注解,并且配置类在注册 Bean 期间不会被跳过 |
1 | // Process any @Import annotations |
1 | // Process any @ImportResource annotations |
1 | // Process individual @Bean methods |
1 | // Process default methods on interfaces |
1 | // Process superclass, if any |
doProcessConfigurationClass()
的主要作用就是完成配置类的解析,包括配置类里的嵌套类、声明的各种注解、内部的各种方法,甚至是超类中的信息。
本节对 doProcessConfigurationClass()
的认识只是管中窥豹,并不涉及具体的解析逻辑,这会在后文补充。
2.6 校验每个配置类
再回到 ConfigurationClassPostProcessor#processConfigBeanDefinitions()
方法中,
1 | // --snip-- |
先前解析出的配置类信息通过调用 parser.parse(candidates)
方法完成解析,前三节内容深入这个方法,大致介绍了其中的解析逻辑。
而后调用 parser.validate()
方法,对每个配置类(包括通过解析先前配置类得到的更多的配置类)进行校验。
1 | // org.springframework.context.annotation.ConfigurationClassParser#validate |
problemReporter
,顾名思义,用来汇报问题的。调用其内部的方法时,可能直接抛出异常,也可能打印一些日志,比如这里使用的默认实现 FailFastProblemReporter
:
1 | public class FailFastProblemReporter implements ProblemReporter { |
回到 configClass.validate(this.problemReporter)
方法中:
1 | void validate(ProblemReporter problemReporter) { |
简单来说,根据配置类上的 @Configuration
注解对配置类进行了校验,同时还校验了配置类中的 Bean 方法。
对 Bean 方法的校验如下:
1 |
|
2.7 源源不断地解析
继续回到 ConfigurationClassPostProcessor#processConfigBeanDefinitions()
方法中:
1 | Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates); |
parser.parse()
方法是对候选配置类的解析,之后调用 parser.validate()
对配置类、Bean 方法进行校验,如果校验不过,就抛出异常。
到目前为止,还仅仅是解析了单个配置类,以前文中的使用示例来说,现在只解析了 Config
配置类。
在 Config
配置类上使用了 @ComponentScan
注解,这会扫描 Config
类所在的包及其子包下的 @Component
注解和它的派生注解,并把那些类交由 Spring 管理。在扫描的范围内,MyController
类被 @Controller
标记,MyFurtherConfig
类被 @Configuration
标记,并且它们都满足配置的 @Conditional
条件。也就是说,当执行完 parser.parse()
方法后,当前 Spring 容器中有以下名称的 BeanDefinition
(不包括 Spring 内置的):
config
myController
myFurtherConfig
1 | Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); |
接下来,通过 parser.getConfigurationClasses()
获取解析得到的配置类信息,注意,这和 Spring 容器中的 BeanDefinition
不一定相同。比如就示例而言,获取到的配置类信息中还额外包含 MyDao
类,这个类是在 Config
上通过 @Import
注解导入的,但它并不在 Spring 容器中。
MyDao
最终肯定是会在 Spring 容器中的,那这是怎么做到的呢?
这来自 Spring 源源不断地解析配置类与加载 BeanDefinition
。
在这之前,需要明白源码中几个集合的含义:
-
String[] candidateNames
:每轮解析前,Spring 容器中存在的BeanDefinition
名称。其中每轮解析,表示执行一次do...while
循环 -
Set<BeanDefinitionHolder> candidates
:候选配置类对应的BeanDefinition
-
Set<ConfigurationClass> alreadyParsed
:已经解析过的配置类,初始值为空 -
Set<ConfigurationClass> configClasses
:调用parser.parse(candidates)
后得到的配置类信息
以执行到初始化 configClasses
为例,这些集合中的元素情况是:
集合名称 | 存在的元素 |
---|---|
candidateNames | config 与 Spring 容器中内置的 BeanDefinition 名称 |
candidates | 仅有 Config 类对应的 BeanDefinition |
alreadyParsed | 空 |
configClasses | Config 、MyController 、MyFurtherConfig 、MyDao |
继续阅读源码,接下来执行 configClasses.removeAll(alreadyParsed);
,由于 alreadyParsed
依旧为空,因此本行无事发生。
1 | if (this.reader == null) { |
接下来使用 reader
将 configClasses
对应的类加载为 BeanDefinition
。configClasses
中的 MyDao
会在此时被转换为 BeanDefinition
,仅此而已?
非也。
loadBeanDefinitions()
还会将配置类中的 Bean 方法也转换为 BeanDefinition
,也就是说,此时除 Spring 内置的 BeanDefinition
外,还有如下 BeanDefinition
:
BeanDefinition 的名称 |
名称来源 |
---|---|
config |
对应配置类名称首字母小写 |
myController |
同上 |
myFurtherConfig |
同上 |
myBean |
对应的 Bean 方法的方法名 |
myService |
同上 |
myRepository |
同上 |
indi.mofan.component.MyDao |
使用 @Import 注解导入 |
1 | alreadyParsed.addAll(configClasses); |
将这轮解析得到的配置类信息添加到 alreadyParsed
集合中,表示它们已经被解析过,并且容器中也存在对应的 BeanDefinition
。
1 | candidates.clear(); |
清空候选的配置类信息,为下一轮解析做准备。因为调用 loadBeanDefinitions()
方法后,可能会解析出其他配置类,需要将这些配置类作为候选的配置类信息。
获取新的候选配置类信息方式如下:
1 | // 当前容器中的 BeanDefinition 数量大于这轮解析前存在的 BeanDefinition 数量 |
candidates
和 candidateNames
均已被更新,怎么开始下一轮解析呢?
1 | do { |
如果存在新的候选配置类,那就开始下一轮解析,以此进行源源不断的解析。
2.8 解析后的完善
注册名为
IMPORT_REGISTRY_BEAN_NAME
的 Bean
processConfigBeanDefinitions()
方法还没有完,还需要做最后的完善。
1 | // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes |
这里的 singletonRegistry
其实就是表示 Spring 容器,判断容器中是否存在名称为 IMPORT_REGISTRY_BEAN_NAME
的 Bean(注意,是直接注册一个 Bean,而不是 BeanDefinition
),如果不存在,就注册一个。
这个 Bean 的类型是 ImportRegistry
,用于处理 ImportAware
接口。
1 | public interface ImportAware extends Aware { |
ImportAware
接口需要和 @Import
注解搭配使用,如果 @Import
注解导入的配置类实现了 ImportAware
接口,导入的配置类能够获取到 @Import
注解所在配置类的元数据。
以 @EnableAsync
注解为例:
1 |
|
@EnableAsync
注解用于启用 SpringBoot 对异步方法的支持,该注解的正确使用需要放在配置类上,常置于 SpringBoot 的主启动类上(主启动类也是一个配置类)。
导入的 AsyncConfigurationSelector
类如下:
1 | public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> { |
adviceMode
的值通常是 ProxyAsyncConfiguration
,因此 @Import
相当于导入了 ProxyAsyncConfiguration
。它是一个配置类,继承 AbstractAsyncConfiguration
抽象类,并实现了 ImportAware
接口。
在 AbstractAsyncConfiguration
类中:
1 |
|
存储
PropertySourceDescriptors
1 | // Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary |
将解析配置类过程中得到的 PropertySourceDescriptor
存储起来,以便将来 AOT 使用。
清除缓存
1 | if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory cachingMetadataReaderFactory) { |
清理外部提供的 MetadataReaderFactory
中的缓存,避免占用过高的内存,同时在配置信息发生变更后再次获取时能够获取到最新的信息。
对于共享缓存来说,这个操作是无用的,因为它将被 ApplicationContext
清理。
3. 详解处理配置类
配置类的处理由 ConfigurationClassParser#doProcessConfigurationClass()
方法完成,前文已经初步认识了这个方法,比如每个步骤的主要含义,但并未进行深入,本节将对其进行补充,抽丝剥茧,层层剖析。
3.1 递归处理成员类
1 | if (configClass.getMetadata().isAnnotated(Component.class.getName())) { |
如果正在处理的配置类 configClass
被 @Component
注解标记,则需要递归处理每个成员(嵌套)类。
进入 processMemberClasses()
方法内部:
1 | private void processMemberClasses(ConfigurationClass configClass, |
首先获取类中的成员类,如果成员类不为空,才继续执行。
1 | List<SourceClass> candidates = new ArrayList<>(memberClasses.size()); |
获取到所有成员类后,遍历每个成员类。
如果成员类是候选的配置类,并且排除与当前配置类同名的成员类(防止无限循环处理同一个配置类),将符合条件的成员类被添加到 candidates
列表中。
1 | OrderComparator.sort(candidates); |
之后对候选的成员配置类排个序(实现 PriorityOrdered
、Ordered
接口)。
1 | for (SourceClass candidate : candidates) { |
遍历候选的成员类:
-
如果导入栈中包含当前正在处理的配置类,说明存在循环导入,调用
problemReporter.error()
方法抛个异常。 -
否则将当前正在处理的配置类添加到导入栈中,之后以相同的方式处理候选的成员配置类,最终将先前添加的配置类弹出栈。
3.2 处理 @PropertySource
1 | // Process any @PropertySource annotations |
通过 AnnotationConfigUtils.attributesForRepeatable()
获取 sourceClass
元数据上的 @PropertySource
和 @PropertySources
注解,之后调用 propertySourceRegistry.processPropertySource()
方法来处理它们。
1 | void processPropertySource(AnnotationAttributes propertySource) throws IOException { |
在 processPropertySource()
方法中,先获取 @PropertySource
注解的每个属性值,然后将它们包装成 PropertySourceDescriptor
对象,接着调用 propertySourceProcessor.processPropertySource()
方法来处理。
1 | public void processPropertySource(PropertySourceDescriptor descriptor) throws IOException { |
在 processPropertySource()
方法中,又会把包装好的 PropertySourceDescriptor
对象再拆开,其中最重要的是 locations
,表示要加载的配置文件所在的位置。
从 Spring 6.1 开始,locations
的书写支持资源位置通配符,比如 classpath*:/ config/*.properties
。
为了处理通配符,需要对每个 location
进行处理,即调用 environment.resolveRequiredPlaceholders()
方法。
之后使用处理过的位置信息去获取资源(配置文件),调用 factory.createPropertySource()
方法将配置文件中的信息封装成 PropertySource
对象,最后调用 addPropertySource()
方法将该对象添加到 Environment
中,这就相当于是将配置文件中的信息绑定到应用上下文了。
怎么创建 PropertySource
对象可以深入 factory.createPropertySource()
方法查看,此处不再叙述。
简单介绍下 addPropertySource()
方法,该方法用于将 PropertySource
对象添加到 Environment
中,在之前会处理多个同名(相同的 name
)的 PropertySource
情况。处理方式很简单,将同名的 PropertySource
封装成一个 CompositePropertySource
,并保证新添加的 PropertySource
优先级更高。
那为什么会存在多个同名的 PropertySource
呢?
这与 @PropertySource
注解有关:
1 | public PropertySource { |
@PropertySource
注解可以指定一个 name
,但可以对应多个 value
。value
就是配置文件所处的位置(也就是 location
),再解析 @PropertySource
注解后,一个 name
就有可能对应多个 PropertySource
对象(即多个配置文件)。
3.3 处理 @ComponentScan
1 | // Search for locally declared @ComponentScan annotations first. |
和先前处理 @PropertySource
注解类似,处理 @ComponentScan
注解的第一步也是先找到配置类上的 @ComponentScan
注解元数据。
1 | if (!componentScans.isEmpty() && |
如果存在 @ComponentScan
注解,并且满足配置类上的 @Conditional
条件,那么就进一步解析 @ComponentScan
注解。
1 | for (AnnotationAttributes componentScan : componentScans) { |
遍历每个 @ComponentScan
配置的信息执行对应的扫描,再遍历扫描到的 BeanDefinition
,检查它们是否是候选的配置类,如果是,再进行递归解析。
重点放在 componentScanParser.parse()
方法,用于根据 @ComponentScan
配置的信息扫描得到 BeanDefinition
:
1 | public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) { |
和解析 @PropertySource
类似,先收集注解,再遍历注解并提取出其中的信息,然后执行对应的逻辑。
目前来看,扫描的逻辑由 scanner.doScan()
完成:
1 | protected Set<BeanDefinitionHolder> doScan(String... basePackages) { |
整体逻辑比较“朴实无华”:
- 遍历每个扫描路径,得到
BeanDefinition
- 遍历得到的
BeanDefinition
,对其内部信息进行一些补充 - 检查
BeanDefinition
是否需要被注册,检查通过才注册
重点在 findCandidateComponents()
,它是如何根据路径信息扫描得到 BeanDefinition
的呢?
findCandidateComponents()
方法内部包含许多异常信息,这里不再贴源码。
1 | Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); |
先根据路径信息得到一系列 Resource
对象。
1 | MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); |
根据 Resource
信息得到元数据信息。
1 | ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader); |
然后把 metadataReader
包装成 BeanDefinition
,最后返回即可。
更深层次的实现比较复杂,感兴趣可以自行查看,阅读源码也不用太过深入,以免陷入无底洞。
3.4 处理 @Import
再回到 ConfigurationClassParser#doProcessConfigurationClass()
方法中继续配置类的下一步解析,接下来将解析 @Import
注解:
1 | processImports(configClass, sourceClass, getImports(sourceClass), filter, true); |
getImports()
先看 getImports()
方法:
1 | private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException { |
这里有两个 Set
集合,imports
和 visited
,前者作为结果集返回,后者进一步传入了 collectImports()
方法。
如果刷过图相关的算法题,或者工作中处理过循环引用的情况,visited
这个名字应该会很熟悉。一般来说,它常用于标记已经访问过的信息,防止出现无限递归。
进入 collectImports()
方法:
1 | private void collectImports(SourceClass sourceClass, |
processImports()
@Import
通常有三种使用方式:
- 最简单的,直接导入配置类
- 导入
ImportSelector
的实现类 - 导入
ImportBeanDefinitionRegistrar
的实现类
对于第二点,ImportSelector
中的 selectImports()
返回了需要导入的配置类的全限定类名,这些名称通常会从文件中读取。
对于第三点,ImportBeanDefinitionRegistrar
中的 registerBeanDefinitions()
方法提供了另一种注册 BeanDefinition
的方法。
在 processImports()
方法中需要对这三种方式进行实现:
1 | private void processImports(ConfigurationClass configClass, |
导入
ImportSelector
1 | Class<?> candidateClass = candidate.loadClass(); |
首先获取 ImportSelector
实例,毕竟 Java 是极致面向对象的语言,没有对象实例,仅靠一个类咋玩呢?
1 | Predicate<String> selectorFilter = selector.getExclusionFilter(); |
将 processImports()
传入的过滤器和 ImportSelector
自带的过滤器进行合并(or
一下)。
1 | if (selector instanceof DeferredImportSelector deferredImportSelector) { |
如果 selector
是 DeferredImportSelector
实例,需要额外处理下。
Deferred
意为推迟、延迟,也就是说不立即导入对应的类,而是向后推迟,那推迟到什么时候呢?
额外处理的逻辑很简单,其实就是想需要推迟的导入添加到另一个集合中。
当所有的配置类都处理完之后,再处理这些推迟的导入。这在 ConfigurationClassParser#parse()
方法的最后一行能看到:
1 | public void parse(Set<BeanDefinitionHolder> configCandidates) { |
继续回到主逻辑中,如果 selector
不是 DeferredImportSelector
实例,就立即执行导入逻辑:
1 | // 获取导入的类的全限定类名 |
指定的全限定类名仍有可能是 ImportSelector
或 ImportBeanDefinitionRegistrar
的子类,因此进行递归导入。
导入
ImportBeanDefinitionRegistrar
1 | Class<?> candidateClass = candidate.loadClass(); |
和导入 ImportSelector
类似,同样需要先获取 ImportBeanDefinitionRegistrar
实例。
1 | configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata()); |
ImportBeanDefinitionRegistrar
提供了另一种注册 BeanDefinition
的方式,但并不会直接在这里注册,而是先保存下 ImportBeanDefinitionRegistrar
实例,后续统一执行实例方法进行注册。
先混个眼熟,这些实例是保存在 importBeanDefinitionRegistrars
属性中的。
至于到底是在哪注册的,后文会进一步介绍。
导入配置类
在这一步中,也只是先保存需要导入的配置类信息,而不是直接将配置类转换成 BeanDefinition
进行注册:
1 | this.importStack.registerImport( |
同样先混个眼熟,这些配置类信息是保存在 ConfigurationClassParser
实例的 importStack
字段中。
对于这些配置类自然也需要进一步解析,因此调用 processConfigurationClass()
方法完成:
1 | processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter); |
总结
到此,@Import
注解已经处理完毕,但可以看到的是,无论导入的是什么类,都没有立即将它们转换成 BeanDefinition
并进行注册,而是将需要导入的类保存起来,后续统一处理。
3.5 处理 @ImportResource
回归主线,进行解析配置类的下一步:处理 @ImportResource
注解。
@ImportResource
用于将外部的配置文件(比如 XML 配置文件)导入到基于 Java 配置的 Spring 应用程序中,这在将 Spring 从传统的 XML 配置向 Java 配置迁移的过程中发挥了重要的作用。
第一步同样是先获取 @ImportResource
注解的信息:
1 | AnnotationAttributes importResource = |
然后获取 @ImportResource
注解的属性值:
1 | if (importResource != null) { |
配置文件的位置会有多个,接下来自然是遍历这些位置信息。
这里没有执行获取配置文件信息的逻辑,而仅仅是将位置信息保存下来:
1 | for (String resource : resources) { |
继续混眼熟,配置文件的位置信息存放在 ConfigurationClass
实例的 importedResources
字段中。
3.6 处理 @Bean 方法
1 | Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); |
@Bean
方法的处理也是才去相同的策略:
- 先获取到
@Bean
方法 - 并不立即处理,而是将它们保存到
ConfigurationClass
实例的beanMethods
字段中
至于如何获取 @Bean
方法也很简单。现在已经有配置类对应的 Class
对象了,通过反射能够拿到内部所有的方法,再判断下哪些方法被 @Bean
注解标记就完事了。
3.7 处理 default 方法
default
方法的处理由 processInterfaces()
实现:
1 | processInterfaces(configClass, sourceClass); |
内部实现与处理 @Bean
方法类似:
1 | private void processInterfaces(ConfigurationClass configClass, |
default
方法的处理与 @Bean
方法类似,最后甚至也是将它们保存在 ConfigurationClass
实例的 beanMethods
字段中。
3.8 处理超类
配置类的解析终于来到最后一步,获取当前配置类的超类,如果有超类就让 doProcessConfigurationClass()
返回,否则返回 null
:
1 | // Process superclass, if any |
在调用 doProcessConfigurationClass()
方法的上层实现中,会判断是否返回了超类,如果存在,就进行递归处理,这在前文中也提到过:
1 | // ConfigurationClassParser#processConfigurationClass() |
3.9 总结
在 ConfigurationClassParser#doProcessConfigurationClass()
方法中对配置类的各种注解、各种方法都进行了处理,除了处理 @ComponentScan
注解是将目标类转换成 BeanDefinition
外,其他处理基本都采取了“延迟”策略,也就是先收集,后续统一处理,而 doProcessConfigurationClass()
方法中是不涉及统一处理的。
盘点一下收集了哪些内容:
- 导入的
DeferredImportSelector
类存放在ConfigurationClassParser
实例的deferredImportSelectorHandler
字段中 - 导入的
ImportBeanDefinitionRegistrar
类存放在ConfigurationClass
实例的importBeanDefinitionRegistrars
字段中 - 导入的普通配置类存放在
ConfigurationClassParser
实例的importStack
字段中 - 通过
@ImportResource
注解导入的配置文件存放在ConfigurationClass
实例的importedResources
字段中 @Bean
方法、default
方法存放在ConfigurationClass
实例的beanMethods
字段中
也就是说,如果需要统一处理这些信息,至少需要 ConfigurationClassParser
和 ConfigurationClass
两种实例。
先前已经分析过,导入的 DeferredImportSelector
最终会在 ConfigurationClassParser#parse()
方法最后执行,这也能划分到 doProcessConfigurationClass()
方法的一部分。
因此,如果需要统一处理这些信息,仅需要 ConfigurationClass
实例和 ConfigurationClassParser
实例中的 importStack
信息。
也真是因为 doProcessConfigurationClass()
方法内部更多的是对信息的收集,因此通过示例对调用 ConfigurationClassParser#parse()
方法进行 Debug 时发现并未增加多少 BeanDefinition
,除了配置类本身 Config
外,额外的两个是 MyFurtherConfig
和 MyController
,它们都是通过 @ComponentScan
扫描得到的。
那统一处理是在哪呢?
4. 统一处理
目光回到最外层的 ConfigurationClassPostProcessor#processConfigBeanDefinitions()
方法中。
以最初的示例代码而言,调用 ConfigurationClassParser#parse()
方法后,BeanDefinition
的数量并未显著增加,在调用 reader.loadBeanDefinitions()
方法加载 BeanDefinition
后,@Bean
方法、导入的配置类都出现在 BeanDefinition
中。
统一处理由 reader.loadBeanDefinitions()
方法完成。
可以看到,构造 reader
对象时传入了 ConfigurationClassParser
实例中的 importStack
信息,调用 loadBeanDefinitions()
方法时,传入了 ConfigurationClass
实例,基本满足先前分析的进行统一处理的要求。
1 | public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) { |
首先构造 TrackedConditionEvaluator
实例,用于处理 @Conditional
注解:
1 | private class TrackedConditionEvaluator { |
然后遍历每个配置类,即 ConfigurationClass
实例,经过先前的解析,该实例中的信息已经非常丰富了,怎么利用这些信息完成更多 BeanDefinition
的加载是由 loadBeanDefinitionsForConfigurationClass
方法完成。
4.1 概括
1 | private void loadBeanDefinitionsForConfigurationClass( |
根据以上信息可以总结出下表:
来源 | 存储位置 | 实现方法 |
---|---|---|
@Import |
ConfigurationClassParser 实例的 importStack 字段 |
registerBeanDefinitionForImportedConfigurationClass() |
@Bean 方法 |
ConfigurationClass 实例的 beanMethods 字段 |
loadBeanDefinitionsForBeanMethod() |
@ImportResource |
ConfigurationClass 实例的 importedResources 字段 |
loadBeanDefinitionsFromImportedResources() |
导入的 ImportBeanDefinitionRegistrar |
ConfigurationClass 实例的 importBeanDefinitionRegistrars 字段 |
loadBeanDefinitionsFromRegistrars() |
4.2 处理 @Import
统一处理 @Import
导入的配置类由 registerBeanDefinitionForImportedConfigurationClass()
方法完成。
1 | AnnotationMetadata metadata = configClass.getMetadata(); |
首先将导入的配置类包装成 BeanDefinition
。
1 | ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef); |
补充 scope
信息。
1 | String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry); |
处理导入配置类上一些常见的注解,比如 @Lazy
、@Primary
、@DependsOn
、@Role
、@Description
等。
1 | BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName); |
处理代理,最终注册 BeanDefinition
即可。
整个过程似曾相识,和处理 @ComponentScan
注解将扫描到的类转换成 BeanDefinition
并注册基本一样。
4.3 处理 @Bean 方法
统一处理 @Bean
方法由 loadBeanDefinitionsForBeanMethod()
方法完成。
1 | ConfigurationClass configClass = beanMethod.getConfigurationClass(); |
首先获取 @Bean
方法的元数据(即 metadata
)和方法名称,并判断当前 @Bean
方法是否需要跳过。
1 | AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class); |
获取 @Bean
注解的信息。
1 | // Consider name and any aliases |
处理别名。
1 | // Has this effectively been overridden before (e.g. via XML)? |
判断当前 @Bean
方法对应的 BeanDefinition
是否是要覆盖已有的 BeanDefinition
。
如果是覆盖了已有的,直接返回或者抛异常。
1 | ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName); |
构造 @Bean
方法对应的 BeanDefinition
。
接下来是对构造的 BeanDefinition
进行补充与处理代理,最终调用 BeanDefinitionRegistry
的 registerBeanDefinition()
方法完成注册。
4.4 处理 @ImportResource
统一处理 @ImportResource
注解导入的配置文件由 loadBeanDefinitionsFromImportedResources()
方法完成。
该方法接收的参数列表如下:
1 | Map<String, Class<? extends BeanDefinitionReader>> importedResources |
Map
的 key
是配置文件所在的位置,value
是解析文件使用的 Reader
。
1 | importedResources.forEach((resource, readerClass) -> { |
遍历传入的 Map
依次进行处理。
1 | // Default reader selection necessary? |
根据不同的文件类型,选择不同的 reader
。
1 | Map<Class<?>, BeanDefinitionReader> readerInstanceCache = new HashMap<>(); |
尝试从缓存中获取 BeanDefinitionReader
实例,如果获取失败,利用 Class
对象通过反射获取实例。
1 | reader.loadBeanDefinitions(resource); |
最后使用 reader
加载配置文件中的 BeanDefinition
。
4.5 处理 ImportBeanDefinitionRegistrar
ImportBeanDefinitionRegistrar
提供了另一种注册 BeanDefinition
的方式,它的处理由 loadBeanDefinitionsFromRegistrars()
方法完成,内部实现极其简单,直接调用 ImportBeanDefinitionRegistrar
里的 registerBeanDefinitions()
方法就完事了:
1 | private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) { |
5. 总结
ConfigurationClassPostProcessor
类主要完成了对 Spring 配置类的解析,在 @Configuration 注解的那些事 一文中主要介绍了配置类的 Full 模式和 Lite 模式,本文则是在此基础上对 Spring 配置类的详细解析步骤进行了介绍。
在解析配置类的过程中会涉及到多种注解的解析,包括 @Conditional
、@Component
及其派生注解、@Import
、@ComponentScan
、@Configuration
、@Bean
等与 Bean 注册相关的注解。
ConfigurationClassPostProcessor
以容器中初始存在的配置类作为起点,然后不断寻找更多的配置类,以此进行源源不断地解析。
配置类由 ConfigurationClass
实例表示,在解析过程中,通过对注解、方法的解析不断补充完善这个对象,最后通过 ConfigurationClassBeanDefinitionReader
实例的 loadBeanDefinitions()
方法加载导入的、@Bean
方法对应的 BeanDefinition
。
6. 写在最后
本文原计划在九月初发布,结果身体上总是有各种不适,胸闷、呼吸困难、胸痛的情况时有发生,给自己的学习和生产造成了极大的影响,每周不是在医院就是在去医院的路上,尽管最终并没有检查出什么,而自己却深陷疑病症的漩涡。
国庆过后,自己身体逐渐好转,胃部的不适成为主要症状,胸闷、呼吸困难的次数也基本屈指可数,对自己的影响大大减小,终于在临近 11 月完成了本文。
总之,拒绝熬夜,不要久坐,保持好心情,减少焦虑,快乐度过每一天。
愿看到这里的你身体永远健康,烦恼永远没有。