专注Java领域分享、成长,拒绝浅尝辄止。关注公众号【BAT的乌托邦】开启专栏式学习,拒绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,里面一并有Spring技术栈、MyBatis、中间件等小而美的专栏供以学习哦。

前言

各位小伙伴大家好,我是A哥。这是继上篇文章:真懂Spring的@Configuration配置类?你可能自我感觉太良好 的原理/源码解释篇。按照本公众号的定位,原理一般跑不了,虽然很枯燥,但还得做,毕竟做难事必有所得,真的掌握了才有底气谈涨薪嘛。

Tips:鉴于经常有些同学无法区分某个功能/某项能力属于Spring Framework的还是Spring Boot,你可以参考文章里的【版本约定】目录,那里会说明本文的版本依赖,也就是功能所属喽。比如本文内容它就属于Spring Framework,和Spring Boot木有关系。


版本约定

本文内容若没做特殊说明,均基于以下版本:

  • JDK:1.8
  • Spring Framework:5.2.2.RELEASE

正文

Spring的IoC就像个“大熔炉”,什么都当作Bean放在里面。然而,虽然它们都放在了一起,但是实际在功能上是有区别的,比如我们熟悉的BeanPostProcessor就属于后置处理器功能的Bean,还有本文要讨论的@Configuration配置Bean也属于一种特殊的组件。

判断一个Bean是否是Bean的后置处理器很方便,只需看它是否实现了BeanPostProcessor接口即可;那么如何去确定一个Bean是否是@Configuration配置Bean呢?若是,如何区分是Full模式还是Lite模式呢?这便就是本文将要讨论的内容。


如何判断一个组件是否是@Configuration配置?

首先需要明确:@Configuration配置前提必须是IoC管理的一个组件(也就是常说的Bean)。Spring使用BeanDefinitionRegistry注册中心管理着所有的Bean定义信息,那么对于这些Bean信息哪些属于@Configuration配置呢,这是需要甄选出来的。

判断一个Bean是否是@Configuration配置类这个逻辑统一交由ConfigurationClassUtils这个工具类去完成。


ConfigurationClassUtils工具类

见名之意,它是和配置有关的一个工具类,提供几个静态工具方法供以使用。它是Spring 3.1新增,对于它的作用,官方给的解释是:用于标识@Configuration类的实用程序(Utilities)。它主要提供了一个方法:checkConfigurationClassCandidate()用于检查给定的Bean定义是否是配置类的候选对象(或者在配置/组件类中声明的嵌套组件类),并做相应的标记


checkConfigurationClassCandidate()

它是一个public static工具方法,用于判断某个Bean定义是否是@Configuration配置。

ConfigurationClassUtils:

	public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
...
// 根据Bean定义信息,拿到器对应的注解元数据
AnnotationMetadata metadata = xxx;
... // 根据注解元数据判断该Bean定义是否是配置类。若是:那是Full模式还是Lite模式
Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
} else if (config != null || isConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
} else {
return false;
} ... // 到这。它肯定是一个完整配置(Full or Lite) 这里进一步把@Order排序值放上去
Integer order = getOrder(metadata);
if (order != null) {
beanDef.setAttribute(ORDER_ATTRIBUTE, order);
} return true;
}

步骤总结:

  1. 根据Bean定义信息解析成为一个注解元数据对象AnnotationMetadata metadata

    1. 可能是个AnnotatedBeanDefinition,也可能是个StandardAnnotationMetadata
  2. 根据注解元数据metadata判断是否是个@Configuration配置类,有如下三种可能case:
    1. 标注有@Configuration注解并且该注解的proxyBeanMethods = false,那么mark一下它是Full模式的配置。否则进入下一步判断
    2. 标注有@Configuration注解或者符合Lite模式的条件(上文有说一共有5种可能是Lite模式,源码处在isConfigurationCandidate(metadata)这个方法里表述),那么mark一下它是Lite模式的配置。否则进入下一步判断
    3. 不是配置类,并且返回结果return false
  3. 能进行到这一步,说明该Bean肯定是个配置类了(Full模式或者Lite模式),那就取出其@Order值(若有的话),然后mark进Bean定义里面去

这个mark动作很有意义:后面判断一个配置类是Full模式还是Lite模式,甚至判断它是否是个配置类均可通过beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE)这样完成判断


方法使用处

知晓了checkConfigurationClassCandidate()能够判断一个Bean(定义)是否是一个配置类,那么它在什么时候会被使用呢?通过查找可以发现它被如下两处使用到:

  • 使用处:ConfigurationClassPostProcessor.processConfigBeanDefinitions()处理配置Bean定义阶段。
ConfigurationClassPostProcessor:

	public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {

		// 拿出当前所有的Bean定义信息,一个个的检查是否是配置类
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
// 如果该Bean定义不是配置类,那就继续判断一次它是否是配置类,若是就加入结果集合里
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
...
}

ConfigurationClassPostProcessor是个BeanDefinitionRegistryPostProcessor,会在BeanFactory 准备好后执行生命周期方法。因此自然而然的,checkConfigurationClassCandidate()会在此阶段调用,用于区分出哪些是配置Bean。

值得注意的是ConfigurationClassPostProcessor的执行时期是非常早期的(BeanFactory准备好后就执行嘛),这个时候容器内的Bean定义很少。这个时候只有主配置类才被注册了进来,那些想通过@ComponentScan扫进来的配置类都还没到“时间”,这个时间节点很重要,请注意区分。为了方便你理解,我分别把Spring和Spring Boot在此阶段的Bean定义信息截图展示如下:



以上是Spring环境,对应代码为:

new AnnotationConfigApplicationContext(AppConfig.class);



以上是Spring Boot环境,对应代码为:

@SpringBootApplication
public class Boot2Demo1Application {
public static void main(String[] args) {
SpringApplication.run(Boot2Demo1Application.class, args);
}
}

相比之下,Spring Boot里多了internalCachingMetadataReaderFactory这个Bean定义。原因是SB定义了一个CachingMetadataReaderFactoryPostProcessor把它放进去的,由于此Processor也是个BeanDefinitionRegistryPostProcessor并且order值为Ordered.HIGHEST_PRECEDENCE,所以它会优先于ConfigurationClassPostProcessor执行把它注册进去~

  • 使用处:ConfigurationClassParser.doProcessConfigurationClass() 解析 @Configuration配置类阶段。所处的大阶段同上使用处,仍旧是ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry()阶段
ConfigurationClassParser:

	@Nullable
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
... // 先解析nested内部类(内部类会存在@Bean方法嘛~)
... // 解析@PropertySource资源,加入到environment环境
... // 解析@ComponentScan注解,把组件扫描进来
scannedBeanDefinitions = ComponentScanAnnotationParser.parse(componentScan, ...);
// 把扫描到的Bean定义信息依旧需要一个个的判断,是否是配置类
// 若是配置类,就继续当作一个@Configuration配置类来解析parse() 递归嘛
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
...
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
... // 解析@Import注解
... // 解析@ImportResource注解
... // 解析当前配置里配置的@Bean方法
... // 解析接口默认方法(因为配置类可能实现接口,然后接口默认方法可能标注有@Bean )
... // 处理父类(递归,直到父类为java.打头的为止)
}

这个方法是Spring对配置类解析的最核心步骤,通过它顺带也能够解答你的疑惑了吧:为何你仅需在类上标注一个@Configuration注解即可让它成为一个配置类?因为被Scan扫描进去了嘛~

通过以上两个使用处的分析和对比,对于@Configuration配置类的理解,你至少应该掌握了如下讯息:

  1. @Configuration配置类肯定是个组件,存在于IoC容器里
  2. @Configuration配置类是有主次之分的,主配置类是驱动整个程序的入口,可以是一个,也可以是多个(若存在多个,支持使用@Order排序)
  3. 我们平时一般只书写次配置类(而且一般写多个),它一般是借助主配置类的@ComponentScan能力完成加载进而解析的(当然也可能是@Import、又或是被其它次配置类驱动的)
  4. 配置类可以存在嵌套(如内部类),继承,实现接口等特性

聊完了最为重要的checkConfigurationClassCandidate()方法,当然还有必要看看ConfigurationClassUtils的另一个工具方法isConfigurationCandidate()


isConfigurationCandidate()

它是一个public static工具方法,通过给定的注解元数据信息来判断它是否是一个Configuration

ConfigurationClassUtils:

	static {
candidateIndicators.add(Component.class.getName());
candidateIndicators.add(ComponentScan.class.getName());
candidateIndicators.add(Import.class.getName());
candidateIndicators.add(ImportResource.class.getName());
} public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
// 不考虑接口 or 注解 说明:注解的话也是一种“特殊”的接口哦
if (metadata.isInterface()) {
return false;
}
// 只要该类上标注有以上4个注解任意一个,都算配置类
for (String indicator : candidateIndicators) {
if (metadata.isAnnotated(indicator)) {
return true;
}
}
// 若一个注解都没标注,那就看有木有@Bean方法 若有那也算配置类
return metadata.hasAnnotatedMethods(Bean.class.getName());
}

步骤总结:

  1. 若是接口类型(含注解类型),直接不予考虑,返回false。否则继续判断
  2. 若此类上标注有@Component、@ComponentScan、@Import、@ImportResource任意一个注解,就判断成功返回true。否则继续判断
  3. 到此步,就说明此类上没有标注任何注解。若存在@Bean方法,返回true,否则返回false。

需要特别特别特别注意的是:此方法它的并不考虑@Configuration注解,是“轻量级”判断,这是它和checkConfigurationClassCandidate()方法的最主要区别。当然,后者依赖于前者,依赖它来根据注解元数据判断是否是Lite模式的配置。


Spring 5.2.0版本变化说明

因为本文的讲解和代码均是基于Spring 5.2.2.RELEASE的,而并不是所有小伙伴都会用到这么新的版本。关于此部分的实现,以Spring 5.2.0版本为分界线实现上有些许差异,所以在此处做出说明。


proxyBeanMethods属性的作用

proxyBeanMethods属性是Spring 5.2.0版本为@Configuration注解新增加的一个属性:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(annotation = Component.class)
String value() default "";
// @since 5.2
boolean proxyBeanMethods() default true;
}

它的作用是:是否允许代理@Bean方法。说白了:决定此配置使用Full模式还是Lite模式。为了保持向下兼容,proxyBeanMethods的默认值是true,使用Full模式配置。

Spring 5.2提出了这个属性项,是期望你在已经了解了它的作用之后,显示的把它置为false的,因为在云原生将要到来的今天,启动速度方面Spring一直在做着努力,也希望你能配合嘛。这不Spring Boot就“配合”得很好,它在2.2.0版本(依赖于Spring 5.2.0)起就把它的所有的自动配置类的此属性改为了false,即@Configuration(proxyBeanMethods = false)


Full模式/Lite模式实现上的差异

由于Spring 5.2.0新增了proxyBeanMethods属性来控制模式,因此实现上也有些许诧异,请各位注意甄别:

Spring 5.2.0+版本判断实现:

ConfigurationClassUtils:

		Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
} else if (config != null || isConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
} else {
return false;
}

Spring 5.2.0-版本判断实现:

ConfigurationClassUtils:

		if (isFullConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
} else if (isLiteConfigurationCandidate(metadata)) {
beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
} else {
return false;
}

思考题?

  1. 既然isConfigurationCandidate()判断方法是为checkConfigurationClassCandidate()服务,那Spring为何也把它设计为public static呢?
  2. ConfigurationClassUtils里还存在对@Order顺序的解析方法,不是说Spring的Bean是无序的吗?这又如何理解呢?

总结

本文作为上篇文章的续篇,解释了@Configuration配置的Full模式和Lite模式的判断原理,同时顺带的也介绍了什么叫主配置配和次配置类,这个概念(虽然官方并不这么叫)对你理解Spring Framework是非常有帮助的。如果你使用是基于Spring 5.2.0+的版本,在了解了这两篇文章内容的基础上,建议你的配置类均采用Lite模式去做,即显示设置proxyBeanMethods = false

另外关于此部分内容,有些更为感兴趣的小伙伴问到:为什么Full模式下通过方法调用指向的仍旧是原来的Bean,保证了只会执行一次呢?开启的是Full模式这只是表象原因,想要回答此问题需要涉及到CGLIB增强实现的深水区内容,为了满足这些好奇(好学)的娃子,计划会在下篇文章继续再拿一篇专程讲解(预计篇幅不短,万字以上),你可订阅我的公众号持续保持关注。

配置类需要标注@Configuration却不知原因?那这次就不能给你涨薪喽的更多相关文章

  1. spring5 源码深度解析----- 被面试官给虐懵了,竟然是因为我不懂@Configuration配置类及@Bean的原理

    @Configuration注解提供了全新的bean创建方式.最初spring通过xml配置文件初始化bean并完成依赖注入工作.从spring3.0开始,在spring framework模块中提供 ...

  2. 配置类为什么要添加@Configuration注解呢?

    配置类为什么要添加@Configuration注解呢? 本系列文章: 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 推荐阅读: Spring官网阅读 | 总结篇 Spring ...

  3. Spring源码解析 – @Configuration配置类及注解Bean的解析

    在分析Spring 容器创建过程时,我们知道容器默认会加载一些后置处理器PostPRocessor,以AnnotationConfigApplicationContext为例,在构造函数中初始化rea ...

  4. 你知道Spring是怎么解析配置类的吗?

    彻底读懂Spring(二)你知道Spring是怎么解析配置类的吗? 推荐阅读: Spring官网阅读系列 彻底读懂Spring(一)读源码,我们可以从第一行读起 Spring执行流程图如下: 如果图片 ...

  5. Spring boot 配置文件参数映射到配置类属性

    [参考文章]:SpringBoot之@EnableConfigurationProperties分析 [参考文章]:在Spring Boot中使用 @ConfigurationProperties 注 ...

  6. 拦截器配置类使用继承写法导致jackson的全局配置失效

    问题描述 项目中需要一个拦截器用于拦截请求,在没有请求中生成requestId.然后写了一个配置类,这个类继承了 WebMvcConfigurationSupport类,重写了addIntercept ...

  7. Spring Boot中只能有一个WebMvcConfigurationSupport配置类

    首先将结论写文章的最前面,一个项目中只能有一个继承WebMvcConfigurationSupport的@Configuration类(使用@EnableMvc效果相同),如果存在多个这样的类,只有一 ...

  8. Spring配置类深度剖析-总结篇(手绘流程图,可白嫖)

    生命太短暂,不要去做一些根本没有人想要的东西.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习 ...

  9. SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的

    系列文章目录和关于我 一丶什么是SpringBoot自动装配 SpringBoot通过SPI的机制,在我们程序员引入一些starter之后,扫描外部引用 jar 包中的META-INF/spring. ...

随机推荐

  1. Spring系列.依赖注入配置

    依赖注入的配置 Spring的依赖注入分为基于构造函数的依赖注入和基于setter方法的依赖注入. 基于构造函数的依赖注入 <!-- 通过构造器参数索引方式依赖注入 --> <bea ...

  2. Python使用wxpy模块实现微信两两群组消息同步

    python使用wxpy模块提供的微信API接口实现两两群组的消息同步 安装模块: pip install wxpy 注意:需要同步的微信群需要保存到通讯录中 以下是自己闲来无事写的代码,暂时还存在以 ...

  3. C Primer Plus(三)

    重读C Primer Plus ,查漏补缺 重读C Primer Plus,记录遗漏的.未掌握的.不清楚的知识点 文件输入/输出 1.fgets函数在读取文件内容时会将换行符读入,但gets不会,fp ...

  4. [每日一题2020.06.15]P1226 【模板】快速幂取余运算

    我是题目 快速幂就是快速求 \(a^b\)的一种算法 快速幂 思想 : 比如我要求 \(6^9\) 首先将幂转化为二进制形式 : \[6^9 = 6^{1001} \tag{1} \] 可以得到 : ...

  5. TensorFlow从0到1之TensorFlow优化器(13)

    高中数学学过,函数在一阶导数为零的地方达到其最大值和最小值.梯度下降算法基于相同的原理,即调整系数(权重和偏置)使损失函数的梯度下降. 在回归中,使用梯度下降来优化损失函数并获得系数.本节将介绍如何使 ...

  6. 个人工作用SQL短句,不定时更新

    表字段操作 --一.修改字段默认值 alter table 表名 drop constraint 约束名字 ------说明:删除表的字段的原有约束 alter table 表名 add constr ...

  7. 基于 abp vNext 和 .NET Core 开发博客项目 - 终结篇之发布项目

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  8. 线程间配合:Condition、Semaphore、CountDownLatch、CyclicBarrier

    1 重入锁的好搭档:Condition条件 如果大家理解了Object.wait()和Object.notify()方法的话,那么就能很容易理解Condition接口了.它和wait()和notify ...

  9. Centos 7使用systemctl补全服务名称

    使用jsw将程序打包成服务后,发现不能使用service + 服务名前几个字母 + tab 快捷键补全服务名,但是tab键可以补全文件夹名,翻阅了各个文档后,最终还是找到了问题所在. 本人安装的是Ce ...

  10. 【树形dp】 Bzoj 4472 Salesman

    题目 某售货员小T要到若干城镇去推销商品,由于该地区是交通不便的山区,任意两个城镇 之间都只有唯一的可能经过其它城镇的路线. 小T 可以准确地估计出在每个城镇停留的净收 益.这些净收益可能是负数,即推 ...