第七节 ConfigurationClassParser 源码分析

tips: ConfigurationClassParser 是 Springframework 中的重要类。

本章主要是源码理解,有难度和深度,也枯燥乏味,可以根据实际情况选择阅读。

位置:org.springframework.context.annotation.ConfigurationClassParser

ConfigurationClassParser 它是解密 configuration 的关键。理解 ConfigurationClassParser 对理解整个 Spring 框架至关重要。

一、作用是什么

ConfigurationClassParser是一个非常重要的类,它主要用于解析带有@Configuration注解的类。

@Configuration注解表明该类用作配置类,其中可以定义bean和Spring容器应如何初始化和管理这些bean。

ConfigurationClassParser的作用可以从以下几个方面详细阐述:

  1. 解析导入的配置@Import注解允许一个配置类导入另一个配置类。ConfigurationClassParser解析这些@Import注解,确保所有导入的配置也被处理和应用。
  2. 处理属性注入:通过@PropertySource注解,可以指定一些属性文件,这些属性文件中的属性可以被注入到Spring管理的bean中。ConfigurationClassParser负责解析这些注解,并确保属性文件被加载且其值可用于注入。
  3. 处理@Conditional注解: Spring框架允许在bean的注册过程中使用条件逻辑,@Conditional注解及其派生注解(例如@ConditionalOnClass@ConditionalOnProperty等)使得只有在满足特定条件时,才会进行bean的注册。ConfigurationClassParser负责解析这些条件注解并应用其逻辑。
  4. processDeferredImportSelectors#processImports 处理扩展配置( Starter 能够被处理的核心分支)

二、触发时机

SpringBoot 应用启动过程中,通过后置处理器去触发 ConfigurationClassPostProcessor。 然后再调用 ConfigurationClassParser类解析

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
		PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
        ....
}

处理如下:

下面我们将详细分析源码流程。

三、ConfigurationClassPostProcessor

下面是 ConfigurationClassPostProcessor 部分核心代码。

入口方法 processConfigBeanDefinitions(BeanDefinitionRegistry registry) 。开始分析这段代码。

注意这里有一个 do...while

	do {
			.....
	}
	while (!candidates.isEmpty());

它将逐一识别和解析配置类,然后将配置类中定义的Bean注册到Spring容器中。这个过程通过不断循环直到没有新的配置类候选者出现为止,确保了所有相关的配置都被完整地处理。

// 创建一个配置类解析器,用于解析和处理配置类信息
ConfigurationClassParser parser = new ConfigurationClassParser(
        this.metadataReaderFactory, this.problemReporter, this.environment,
        this.resourceLoader, this.componentScanBeanNameGenerator, registry);

// 初始化一个集合,用于存储待处理的配置类候选者
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
// 初始化一个集合,用于跟踪已经解析过的配置类,以避免重复解析
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());

// 循环处理,直到没有新的配置类候选者
do {
    // 解析当前候选者中的配置类
    parser.parse(candidates);
    // 对解析结果进行验证
    parser.validate();

    // 从解析器中获取已解析的配置类集合
    Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
    // 移除已经处理过的,避免重复处理
    configClasses.removeAll(alreadyParsed);

    // 如果读取器未初始化,创建一个配置类Bean定义读取器
    if (this.reader == null) {
        this.reader = new ConfigurationClassBeanDefinitionReader(
                registry, this.sourceExtractor, this.resourceLoader, this.environment,
                this.importBeanNameGenerator, parser.getImportRegistry());
    }
    // 加载并注册配置类中定义的Bean
    this.reader.loadBeanDefinitions(configClasses);
    // 将这批配置类标记为“已解析”
    alreadyParsed.addAll(configClasses);
    // 清空候选者集合,为下一轮寻找新候选者做准备
    candidates.clear();

    // 检查是否有新的Bean定义被注册(可能由@Configuration类引入)
    if (registry.getBeanDefinitionCount() > candidateNames.length) {
        // 重新获取所有Bean定义的名称
        String[] newCandidateNames = registry.getBeanDefinitionNames();
        // 创建一个旧候选名称的集合,用于辨识新的候选者
        Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
        // 创建一个集合,用于跟踪已经解析的配置类的类名
        Set<String> alreadyParsedClasses = new HashSet<>();
        for (ConfigurationClass configurationClass : alreadyParsed) {
            alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
        }
        // 遍历新的Bean定义名称,寻找新的配置类候选者
        for (String candidateName : newCandidateNames) {
            if (!oldCandidateNames.contains(candidateName)) {
                BeanDefinition bd = registry.getBeanDefinition(candidateName);
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                        !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                    candidates.add(new BeanDefinitionHolder(bd, candidateName));
                }
            }
        }
        // 更新候选名称列表,以反映新的Bean定义
        candidateNames = newCandidateNames;
    }
// 如果还有未处理的候选者,继续循环
} while (!candidates.isEmpty());

特别说明,对于 Bean 的加载和实例化不在本范围了,不进行讲解。感兴趣可以阅读相关章节。

上面的这段代码是 ConfigurationClassPostProcessor 核心。解析来的重头戏。ConfigurationClassParser

四、ConfigurationClassParser

从这行代码开始入手parser.parse(candidates);

  1. parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
  2. processDeferredImportSelectors(); // 处理前面推迟的ImportSelector,一些二方包的导入类,将在这个方法中实现。 例如,我们配置在 Starter Spring.factories 中的自动导入类,将在这一环境被加载
parse方法入口
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    // 如果这个配置类应该根据条件注解被跳过,则直接返回不进行处理
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
        return;
    }
    
    // 尝试从已处理的配置类映射中获取这个配置类
    ConfigurationClass existingClass = this.configurationClasses.get(configClass);
    // 如果找到了已存在的配置类
    if (existingClass != null) {
        // 如果当前处理的是一个导入的配置类
        if (configClass.isImported()) {
            // 如果已存在的配置类也是导入的,则合并导入来源
            if (existingClass.isImported()) {
                existingClass.mergeImportedBy(configClass);
            }
            // 如果已存在的配置类不是导入的,则忽略当前导入的配置类,保留现有的非导入类
            return;
        }
        else {
            // 如果找到显式的bean定义,可能是意在替换一个导入的类。
            // 移除旧的配置类,采用新的配置类。
            this.configurationClasses.remove(configClass);
            this.knownSuperclasses.values().removeIf(configClass::equals);
        }
    }
    
    // 递归处理配置类及其超类层次结构
    SourceClass sourceClass = asSourceClass(configClass);
    do {
        // 处理当前配置类并更新sourceClass为配置类的超类,准备下一轮处理
        sourceClass = doProcessConfigurationClass(configClass, sourceClass);
    } while (sourceClass != null); // 如果sourceClass为null,表示超类已经处理完毕
    
    // 将处理完的配置类放入配置类映射中,标记为已处理
    this.configurationClasses.put(configClass, configClass);
}

上面可以理解,解析 MyApplication 类所在工程中的类。最终的解析由 doProcessConfigurationClass 实现

processDeferredImportSelectors

负责处理那些被延迟的特殊接口,使用它来按需动态地导入配置。

这些常常依赖于某些条件才被执行,所以被延迟处理。

// 定义处理延迟的ImportSelector的方法
private void processDeferredImportSelectors() {
    // 获取之前收集的所有延迟处理的ImportSelector
    List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
    // 将引用置为null,表示开始处理过程,防止重复处理
    this.deferredImportSelectors = null;
    // 如果没有需要处理的延迟ImportSelector,则直接返回
    if (deferredImports == null) {
        return;
    }
    ...... 
    // 遍历所有的延迟ImportSelector
    for (DeferredImportSelectorHolder deferredImport : deferredImports) {
        // 获取与当前ImportSelector相关联的配置类
        ConfigurationClass configClass = deferredImport.getConfigurationClass();
        
        try {
            // 调用ImportSelector的selectImports方法,获取所有的导入类名
            String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
            // 处理这些导入的类,将它们作为配置类进行进一步的处理
            processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
        }
    }
}

这个方法的主要作用将是找出符合条件的 imports 类。最终还是由processImports() 处理。

到这里,spring.factories 符合条件的一些类将被加载。核心代码deferredImport.getImportSelector().selectImports(configClass.getMetadata());

到这里我们基本上了解了 Starter 是如何被引入进来的。

真正解析的方法 doProcessConfigurationClass

doProcessConfigurationClass

它递归地处理嵌套类、处理@PropertySource注解、处理@ComponentScan注解、处理@Import注解、处理@ImportResource注解、处理@Bean方法、处理接口上的默认方法,最后处理父类

  1. 处理成员类:方法首先递归地处理配置类中定义的任何成员类(嵌套类)。
  2. 处理@PropertySource注解:然后遍历配置类上的所有@PropertySource注解,这些注解用来指明属性文件的位置。如果当前的环境实现了ConfigurableEnvironment接口,则处理注解指定的属性源
  3. 处理@ComponentScan注解:接下来,处理配置类上的所有@ComponentScan注解,这些注解指示Spring扫描特定包下的组件(即带有@Component@Service等注解的类),并注册为Spring容器中的Bean。如果有条件注解指示在此阶段跳过处理,则不执行扫描。
  4. 处理@Import注解:处理配置类上的@Import注解,这些注解用来导入其他配置类或配置选择器,允许模块化地组织配置。
  5. 处理@ImportResource注解:处理配置类上的@ImportResource注解,这些注解用于导入XML配置文件。
  6. 处理@Bean方法:收集配置类中所有带有@Bean注解的方法的元数据,并将它们添加到配置类对象中。这些方法定义了应该由Spring容器管理的Bean。
  7. 处理接口上的默认方法:如果配置类实现了接口,并在这些接口上定义了默认方法,这些方法也会被处理。
  8. 处理父类:最后,如果配置类有超类,那么这个方法会检查超类是否也是一个配置类,不是Java内置类,并且还没有被处理过。如果满足条件,则递归地处理这个超类。

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
        throws IOException {
    // 首先,递归处理任何成员类(嵌套类)
    processMemberClasses(configClass, sourceClass);
    // 处理所有的@PropertySource注解
    for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), PropertySources.class,
            org.springframework.context.annotation.PropertySource.class)) {
        // 如果环境实现了ConfigurableEnvironment接口
        if (this.environment instanceof ConfigurableEnvironment) {
            // 处理@PropertySource注解
            processPropertySource(propertySource);
        } else {
            // 如果环境没有实现ConfigurableEnvironment接口
            logger.warn("忽略了[" + sourceClass.getMetadata().getClassName() +
                    "]上的@PropertySource注解。原因:环境必须实现ConfigurableEnvironment接口");
        }
    }
    // 处理所有的@ComponentScan注解
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    // 如果存在@ComponentScan注解,并且当前阶段不应该跳过
    if (!componentScans.isEmpty() &&
            !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
        // 循环处理每个@ComponentScan注解
        for (AnnotationAttributes componentScan : componentScans) {
            // 配置类上存在@ComponentScan注解 -> 立即执行扫描
            Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
            // 检查扫描结果中的定义集合,如果有进一步的配置类,递归解析
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(
                        holder.getBeanDefinition(), this.metadataReaderFactory)) {
                    parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
                }
            }
        }
    }
    // 处理所有的@Import注解
    processImports(configClass, sourceClass, getImports(sourceClass), true);
    // 处理所有的@ImportResource注解
    AnnotationAttributes importResource =
            AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
    // 如果@ImportResource注解存在
    if (importResource != null) {
        // 获取资源位置
        String[] resources = importResource.getStringArray("locations");
        // 获取资源的阅读器类
        Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
        // 循环处理每个资源
        for (String resource : resources) {
            // 解析资源位置中的占位符
            String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
            // 把解析后的资源添加到配置类
            configClass.addImportedResource(resolvedResource, readerClass);
        }
    }
    // 处理单独的@Bean方法
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
    // 循环处理每个@Bean方法
    for (MethodMetadata methodMetadata : beanMethods) {
        // 添加@Bean方法到配置类
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }
    // 处理接口上的默认方法
    processInterfaces(configClass, sourceClass);
    // 处理父类,如果存在的话
    if (sourceClass.getMetadata().hasSuperClass()) {
        // 获取父类名称
        String superclass = sourceClass.getMetadata().getSuperClassName();
        // 如果父类存在,并且父类不是java.*开头,并且尚未处理过
        if (superclass != null && !superclass.startsWith("java") &&
                !this.knownSuperclasses.containsKey(superclass)) {
            // 记录已知的父类
            this.knownSuperclasses.put(superclass, configClass);
            // 找到父类,返回其注解元数据并递归处理
            return sourceClass.getSuperClass();
        }
    }
    // 没有父类 -> 处理完成
    return null;
}

到这里,大体流程我们已经清楚。不再继续针对注解的解析进行讲解,感兴趣可以自行下载源码阅读理解。

五、本章小结

本章是对整 ConfigurationClassParser 进行讲解,它是 Spring framework 中的最核心类。

到这里,Starter 的整个过程已经分析完成,但是针对条件装配,我们将在下一章进行讲解。

 已同步发布到公众号:面汤放盐 第七节 ConfigurationClassParser 源码分析 (qq.com)

掘金账号:第七节 ConfigurationClassParser 源码分析 - 掘金 (juejin.cn)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/638648.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

景源畅信:小白做抖音运营难吗?

在数字化时代&#xff0c;社交媒体已成为人们生活的一部分&#xff0c;而抖音作为其中的翘楚&#xff0c;吸引了众多希望通过平台实现自我价值和商业目标的用户。对于刚入门的小白来说&#xff0c;运营抖音账号可能会遇到不少挑战。接下来&#xff0c;我们将详细探讨这一话题&a…

由于找不到mfc140u.dll怎么办,介绍5种靠谱有效的解决方法

当您的电脑显示“mfc140u.dll丢失”的错误时&#xff0c;通常是因为系统中缺少了某个必要的动态链接库文件。这个问题可能会导致某些应用程序无法正常运行&#xff0c;给用户带来困扰。下面我将详细介绍解决该问题的五种方法。 一&#xff0c;关于mfc140u.dll文件的概述 mfc14…

数据防泄漏系统哪个好用,给文件加密的软件

数据防泄露&#xff08;Data Leakage Prevention&#xff0c;DLP&#xff09;是指通过一定的技术手段&#xff0c;防止组织指定&#xff08;重要或敏感的&#xff09;数据或信息资产以违反安全策略规定的形式流出组织的一种策略。 信息防泄露以文档加密技术为核心&#xff0c;…

【代码随想录 二叉树】二叉树前序、中序、后序遍历的迭代遍历

文章目录 1. 二叉树前序遍历&#xff08;迭代法&#xff09;2. 二叉树后序遍历&#xff08;迭代法&#xff09;3. 二叉树中序遍历&#xff08;迭代法&#xff09; 1. 二叉树前序遍历&#xff08;迭代法&#xff09; 题目连接 &#x1f34e;因为处理顺序和访问顺序是一致的。所…

SpringBoot入门教程:Word模板生成docx文件

一:处理docx 自己写一个docx文档,然后另存为选择格式 Word XML 文档(*.xml),然后使用thymeleaf语法将实际值使用表达式代替。 二:pom <dependency><groupId>org.springframework.boot</groupId>

朴素贝叶斯+SMSSpamCollections

1. 打开 Jupyter 后&#xff0c;在工作目录中&#xff0c;新建一个文件夹命名为 Test01 &#xff0c;并且在文件夹中导入数据 集。在网页端界面点击 “upload” 按钮&#xff0c;在弹出的界面中选择要导入的数据集。然后数据集出现 在 jupyter 文件目录中&#xff0c;此时…

机器学习第四十周周报 WDN GGNN

文章目录 week40 WDN GGNN摘要Abstract一、文献阅读1. 题目2. abstract3. 网络架构3.1 问题提出3.2 GNN3.3 CSI GGNN 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程4.3.1 数据获取4.3.2 参数设置4.3.3 实验结果 5. 结论二、GGNN1. 代码解释2. 网络结构小结参考文献参考文…

211初试自命题复试线仅302分!延边大学计算机考研考情分析!

延边大学&#xff08;Yanbian University&#xff09;&#xff0c;简称“延大”&#xff0c;地处吉林省延边朝鲜族自治州&#xff0c;是国家“双一流”建设高校、国家“211工程”重点建设大学、西部开发重点建设院校、吉林省人民政府和教育部共同重点支持建设大学、吉林省人民政…

Redis基础篇

文章目录 2 Redis入门概述3 Redis10大数据类型3.1 Redis自字符串String3.2 Redis列表List3.3 Redis哈希Hash3.4 Redis集合Set3.5 Redis有序集合Sorted Set3.6 Redis地理空间 GEO3.7 Redis基数统计 HyperLogLog3.8 Redis位图bitmap3.9 Redis位域bitField3.10 Redis流Stream 4 Re…

开源大模型与闭源大模型:谁主沉浮?

目录 &#x1f349;引言 &#x1f349;数据隐私 &#x1f348;开源大模型的优势与挑战 &#x1f34d;优势&#xff1a; &#x1f34d;挑战&#xff1a; &#x1f348;闭源大模型的优势与挑战 &#x1f34d;优势&#xff1a; &#x1f34d;挑战&#xff1a; &#x1f34…

草图大师2024怎么保存低版本呢?插件怎么写?

草图大师是一款流行的绘图和设计软件&#xff0c;为了向后兼容&#xff0c;保存低版本文件时&#xff0c;可以采取以下步骤&#xff1a; su模型库 1.另存为旧版本格式&#xff1a; 在保存文件时&#xff0c;草图大师通常会提供一个选项&#xff0c;让你选择要保存的文件格式和…

vmware - 主机向虚拟机拷贝文件的临时方法

文章目录 vmware - 主机向虚拟机拷贝文件的临时方法概述笔记确认主机/虚拟机之间网络是通的在虚拟机中新建一个文件夹(e.g. c:\test), 将这个文件夹设为共享文件夹。查看虚拟机中的当前用户(远程登录要用)远程登录备注 - win8.1只能用mstscEND vmware - 主机向虚拟机拷贝文件的…

【传知代码】transformer-论文复现

文章目录 概述原理介绍模型架构 核心逻辑嵌入表示层注意力层前馈层残差连接和层归一化编码器和解码器结构 数据处理和模型训练环境配置小结 本文涉及的源码可从transforme该文章下方附件获取 概述 Transformer模型是由谷歌在2017年提出并首先应用于机器翻译的神经网络模型结构…

虚拟化技术[1]之服务器虚拟化

文章目录 虚拟化技术简介数据中心虚拟化 服务器虚拟化服务器虚拟化层次寄居虚拟化裸机虚拟化VMM无法直接捕获特权指令解决方案 服务器虚拟化底层实现CPU虚拟化内存虚拟化I/O设备虚拟化 虚拟机迁移虚拟机动态迁移迁移内容&#xff1a;内存迁移迁移内容&#xff1a;网络资源迁移迁…

小结5:朗读练习第二段

五、朗读练习2 2024-5-6始&#xff0c;5-14终&#xff0c;5-15写。 我渐渐体会到一些朗读的乐趣。但我还要考研&#xff0c;要写作业、期末考试。如果是在大一该多好。我可以就这样一天一天的写下去&#xff0c;慢慢地有一些自己的作品&#xff0c;还能录视频发到b站上。 上一篇…

codewars check_same_case 题解

题目 编写一个函数来检查两个给定的字符是否大小写相同。 如果任何字符不是字母&#xff0c;则返回-1如果两个字符大小写相同&#xff0c;则返回1如果两个字符都是字母且大小写不同&#xff0c;则返回0 例子 a并g返回1A并C返回1b并G返回0B并g返回00并?返回-1题解 1 此题主…

在Windows上创建RAM Disk

在Windows 10上创建一个与Linux中的tmpfs相似的内存文件系统&#xff08;一个文件系统&#xff0c;它使用主内存作为存储&#xff09;通常不是操作系统直接提供的功能。不过&#xff0c;有一些方法可以实现类似的效果。 使用软件创建RAM Disk 有一些第三方软件可以帮助在Wind…

自主创新助力科技强军,麒麟信安闪耀第九届军博会

由中国指挥与控制学会主办的中国指挥控制大会暨第九届北京军博会于5月17日-19日在北京国家会议中心盛大开展&#xff0c;政府、军队、武警、公安、交通、人防、航天、航空、兵器、船舶、电科集团等从事国防军工技术与产业领域的30000多名代表到场参加。 麒麟信安作为国产化方案…

2024年英国皇家学会新增院士名单和三位华人学者简介

近日&#xff0c;英国皇家学会公布了2024年新当选的院士及外籍院士名单&#xff0c;他们也是访问学者、博士后及联合培养博士们关注的目标导师。为此知识人网小编推出该文&#xff0c;以飨读者。 当地时间2024年5月16日&#xff0c;英国皇家学会&#xff08;The Royal Society&…

这台电脑无法运行Windows11问题解决方案

今天就记录一下我使用VMware安装Windows11遇到的问题&#xff1a;这台电脑无法运行Windows11的解决方案。 经过排查发现是VMware需要给虚拟机添加 可信平台模块 要注意直接添加 可信平台模块 是添加不上的&#xff0c;需要先给虚拟机加密 以下是给虚拟机加密的流程 1、点击虚…