安顺市网站建设_网站建设公司_AJAX_seo优化
2025/12/28 13:19:44 网站建设 项目流程

装饰后的bean定义用来做什么?

回顾上一章节

在上一篇文章中,我们深入探索了 Spring 如何处理 Bean 定义的装饰:

  • decorateBeanDefinitionIfRequired方法负责对BeanDefinitionHolder进行装饰处理
  • 装饰过程包括属性装饰和子元素装饰两部分
  • Spring 通过命名空间处理器来处理自定义命名空间的标签和属性
  • 装饰过程遵循不可变性原则,通过创建新对象而非修改原有对象来保证递归调用的安全性

💡关键理解:在processBeanDefinition方法中,当我们完成对BeanDefinitionHolder的装饰后,接下来会做什么呢?装饰后的 Bean 定义最终会被用于什么地方?

统一术语

在开始之前,我们先明确几个关键术语:

  • BeanDefinitionRegistry:Bean 定义注册表,用于注册和获取 Bean 定义
  • registerBeanDefinition:注册 Bean 定义的方法
  • BeanName:Bean 的名称,用于在容器中唯一标识一个 Bean
  • Alias:Bean 的别名,允许一个 Bean 有多个名称
  • 注册时机:Bean 定义被注册到容器的时机

问题场景

processBeanDefinition方法中,我们可以看到以下代码流程:

protectedvoidprocessBeanDefinition(Elementele,BeanDefinitionParserDelegatedelegate){// 1. 解析 bean 标签为 BeanDefinitionHolderBeanDefinitionHolderbdHolder=delegate.parseBeanDefinitionElement(ele);if(bdHolder!=null){// 2. 修饰 BeanDefinitionHolder(如果需要进行装饰)bdHolder=delegate.decorateBeanDefinitionIfRequired(ele,bdHolder);// 3. 注册 BeanDefinition// 这里会做什么呢?}}

那么,装饰后的 Bean 定义会被用来做什么呢?这个过程涉及:

  1. 注册过程:装饰后的 Bean 定义如何被注册到容器中?
  2. 注册内容:除了 Bean 定义本身,还会注册哪些信息?
  3. 注册时机:Bean 定义的注册发生在什么时候?
  4. 后续使用:注册后的 Bean 定义会被如何利用?

💡关键问题:Spring 容器需要将解析和装饰后的 Bean 定义保存起来,以便后续创建 Bean 实例时使用。那么,这个过程是如何实现的?注册过程中又需要注意哪些问题?

源码探索

接下来,我们重点关注processBeanDefinition这个方法的后半部分:

  1. 通过 BeanDefinitionReaderUtils 注册
  2. 通知 bean 定义注册完成

这里主要看第一部分,第二部分和之前的通知机制类似。

registerBeanDefinition 方法的主要逻辑

BeanDefinitionReaderUtils.registerBeanDefinition方法主要做了两件事:

  1. 注册 bean 定义:调用registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition())
  2. 注册别名:遍历别名数组,调用registry.registerAlias(beanName, alias)注册每个别名

让我们深入看看DefaultListableBeanFactory实现的registerBeanDefinition方法。

第一步:Bean 定义的校验

首先,Spring 会检查入参的有效性。如果入参的beanDefinition是一个AbstractBeanDefinition的实现类,则调用其校验方法:

if(beanDefinitioninstanceofAbstractBeanDefinition){((AbstractBeanDefinition)beanDefinition).validate();}

🔍发现一:校验方法在不同的实现类中会有不同的作用:

  • 默认校验AbstractBeanDefinition.validate()):
    • methodOverrideFactoryMethodName不能并存
    • 检查 Override 方法个数:如果 0 个则报错,1 个则setOverloaded(false)
  • ChildBeanDefinition的额外校验
    • 检查是否存在parentName(因为子 Bean 定义必须要有父 Bean 定义)

💡思考时刻:为什么 Spring 有这个约束?

  • methodOverrideFactoryMethodName不能并存factory-method用于指定工厂方法创建 Bean,而lookup-methodreplaced-method用于方法注入或方法替换,这两种机制在 Bean 创建过程中的作用不同,不能同时使用
  • Override 方法个数的检查:确保方法注入的配置是正确的,如果配置了方法注入但没有指定方法,或者在只有一个方法时标记为"重载",都是不合理的配置

💡类比理解:这就像建造房子时的设计图纸检查,在建房前先检查图纸是否有矛盾或不合理的地方。

第二步:检查 Bean 定义是否已存在

接下来,Spring 会从 bean 定义的缓存beanDefinitionMap中根据beanName获取已存在的 Bean 定义:

BeanDefinitionexistingDefinition=this.beanDefinitionMap.get(beanName);

场景一:Bean 定义已存在

如果缓存中已经存在该 Bean 定义,Spring 会检查是否开启了 bean 定义覆盖的配置:

if(!this.allowBeanDefinitionOverriding){thrownewBeanDefinitionStoreException(...);}
  • 未开启覆盖:抛出BeanDefinitionStoreException异常
  • 开启覆盖:记录警告日志,日志中会详细说明覆盖的细节,包括:
    • 原始 Bean 定义的来源
    • 新 Bean 定义的来源
    • Bean 定义的差异(如类名、作用域等)

然后将缓存中的 Bean 定义替换为最新的:

this.beanDefinitionMap.put(beanName,beanDefinition);

💡关键理解:Bean 定义覆盖是一个重要的配置选项,在开发阶段可能允许覆盖以便于调试,但在生产环境通常禁止覆盖以保证配置的稳定性。

场景二:Bean 定义不存在,但 beanName 存在于别名中

这是一种比较特殊的场景:要注册的beanNamebeanDefinitionMap中不存在,但在别名映射aliasMap中存在。

StringactualBeanName=null;if(isAlias(beanName)){actualBeanName=canonicalName(beanName);}

🔍发现二:这种情况下,Spring 会:

  1. 获取原始 Bean 名称:通过canonicalName方法获取别名对应的原始 Bean 名称
  2. 检查覆盖配置
    • 不支持覆盖:无论如何都会报错,但会判断是否存在原始beanName的 Bean 定义,根据情况报不同的错误
    • 支持覆盖:打印日志,并且清理掉别名的映射,当前beanName不再作为别名,而是作为新的 Bean 名称

⚠️注意:这里这个机制很绕,建议在开发过程中不是万不得已不要使用此类机制。这种场景通常出现在复杂的配置场景中,容易导致配置混乱。

💡类比理解:这就像一个人以前用的是"小名"(别名),现在要正式登记为"大名"(Bean 名称),需要先取消"小名"的登记,然后才能用这个名字正式登记。

第三步:更新 Bean 名称列表

在将 Bean 定义放入beanDefinitionMap后,Spring 还需要更新beanDefinitionNames列表。这里有两种情况:

情况一:Bean 正在创建中

如果存在 Bean 正在创建(hasBeanCreationStarted()返回true),说明容器已经进入 Bean 实例化阶段,此时需要加锁操作:

synchronized(this.beanDefinitionMap){this.beanDefinitionMap.put(beanName,beanDefinition);List<String>updatedDefinitions=newArrayList<>(this.beanDefinitionNames.size()+1);updatedDefinitions.addAll(this.beanDefinitionNames);updatedDefinitions.add(beanName);this.beanDefinitionNames=updatedDefinitions;removeManualSingletonName(beanName);}

🔍发现三:这里需要:

  1. 加锁:对beanDefinitionMap加锁,保证线程安全
  2. 创建新的列表:由于beanDefinitionNames可能正在被其他线程使用(如遍历列表创建 Bean),不能直接修改,需要创建新的列表
  3. 合并元素:将旧的元素和当前beanName放入新列表
  4. 替换列表:用新列表替换旧的列表
  5. 清理手工注册的单例:如果该名称之前是手工注册的单例 Bean,需要清理掉

💡关键理解:在 Bean 创建过程中动态注册 Bean 定义是线程敏感的,必须保证线程安全。创建新列表而不是直接修改,可以避免并发遍历时的ConcurrentModificationException

情况二:Bean 未开始创建

如果不存在 Bean 正在创建,则直接添加到列表即可:

this.beanDefinitionNames.add(beanName);

💡关键理解:在 Bean 创建之前注册 Bean 定义是安全的,可以直接修改列表,不需要加锁。

第四步:清理相关缓存

到了方法的最后,Spring 需要根据情况清理相关的缓存:

情况一:存在旧的 Bean 定义或包含单例 Bean

if(existingDefinition!=null||containsSingleton(beanName)){resetBeanDefinition(beanName);}

🔍发现四resetBeanDefinition方法会:

  1. 清理 Bean 定义:从beanDefinitionMap中移除旧的 Bean 定义
  2. 清理单例 Bean:如果该名称已经创建了单例 Bean,会销毁该 Bean
  3. 广播事件:触发BeanDefinitionRegistryListenerpostProcessBeanDefinitionRegistry方法,通知 Bean 定义已重置

情况二:配置已冻结

elseif(isConfigurationFrozen()){clearByTypeCache();}

如果配置已经冻结(freezeConfiguration()已调用),则清理类型相关的缓存:

  • allBeanNamesByType:按类型查找所有 Bean 名称的缓存
  • singletonBeanNamesByType:按类型查找单例 Bean 名称的缓存

💡关键理解:配置冻结后,Bean 定义不应该再改变。如果此时动态注册新的 Bean 定义,类型缓存可能会过时(新的 Bean 定义的类型信息没有在缓存中),需要清理缓存,防止获取到旧的配置。

第五步:处理主要 Bean

最后,如果 Bean 定义是主要的(isPrimary()返回true),则将beanName放入primaryBeanNames

if(beanDefinition.isPrimary()){this.primaryBeanNames.add(beanName);}

🔍发现五primaryBeanNames用于存储所有标记为primary的 Bean 名称。当存在多个相同类型的 Bean 时,Spring 会优先选择primarytrue的 Bean。

涉及的缓存整理

registerBeanDefinition方法中,涉及了多个缓存,让我们整理一下它们的作用:

1. beanDefinitionMap
privatefinalMap<String,BeanDefinition>beanDefinitionMap=newConcurrentHashMap<>(256);

作用:存储 Bean 名称到 Bean 定义的映射关系,是 Spring 容器的核心存储结构。

特点

  • 线程安全:使用ConcurrentHashMap实现
  • 主存储:所有注册的 Bean 定义都存储在这里
  • 快速查找:通过 Bean 名称可以快速查找对应的 Bean 定义
2. beanDefinitionNames
privatevolatileList<String>beanDefinitionNames=newArrayList<>(256);

作用:存储所有已注册的 Bean 名称列表,按照注册顺序排列。

特点

  • 有序列表:保持 Bean 定义的注册顺序
  • 遍历使用:用于按顺序遍历所有 Bean 定义(如初始化所有 Bean)
  • 线程安全:使用volatile关键字,在 Bean 创建过程中会创建新的列表来保证线程安全
3. allBeanNamesByType
privatefinalMap<Class<?>,String[]>allBeanNamesByTypeCache=newConcurrentHashMap<>(64);

作用:按类型查找所有 Bean 名称的缓存(包括单例和原型 Bean)。

使用场景:当调用getBeanNamesForType方法时,如果配置已冻结,会使用这个缓存来提高查找效率。

4. singletonBeanNamesByType
privatefinalMap<Class<?>,String[]>singletonBeanNamesByTypeCache=newConcurrentHashMap<>(64);

作用:按类型查找单例 Bean 名称的缓存。

使用场景:当调用getBeanNamesForType方法且只查找单例 Bean 时,如果配置已冻结,会使用这个缓存。

💡关键理解:类型缓存只有在配置冻结后才会被使用。配置冻结前,Spring 会实时查找;配置冻结后,为了提高性能,会使用缓存。

5. primaryBeanNames
privatefinalSet<String>primaryBeanNames=newLinkedHashSet<>();

作用:存储所有标记为primary的 Bean 名称。

使用场景:当存在多个相同类型的 Bean 时,Spring 会优先选择primarytrue的 Bean 进行注入。

特点

  • 有序集合:使用LinkedHashSet保持插入顺序
  • 快速判断:可以快速判断某个 Bean 是否是primary
6. aliasMap(别名缓存)
privatefinalMap<String,String>aliasMap=newConcurrentHashMap<>(16);

作用:存储别名到原始 Bean 名称的映射关系。

特点

  • 别名映射:key 是别名,value 是原始 Bean 名称
  • 线程安全:使用ConcurrentHashMap实现
  • 循环检测:支持别名指向别名的情况,但会检测循环引用

💡类比理解:这些缓存就像图书馆的管理系统:

  • beanDefinitionMap:主目录(书名→书的位置)
  • beanDefinitionNames:总目录列表(所有书名的列表)
  • allBeanNamesByTypesingletonBeanNamesByType:分类索引(按类型查找)
  • primaryBeanNames:推荐书目列表
  • aliasMap:别名索引(别名→正式书名)

设计思想

1. 分层缓存设计

Spring 使用多层次的缓存结构来存储和管理 Bean 定义信息:

  • 主存储beanDefinitionMap是核心存储,所有 Bean 定义都存储在这里
  • 辅助缓存beanDefinitionNames提供有序列表,allBeanNamesByTypesingletonBeanNamesByType提供类型索引
  • 特殊标记primaryBeanNames标记主要 Bean,aliasMap提供别名映射

💡设计优势

  • 快速查找:不同类型的查找可以使用不同的缓存,提高效率
  • 有序性:通过beanDefinitionNames保证 Bean 定义的注册顺序
  • 性能优化:配置冻结后使用类型缓存,避免重复的类型查找

2. 线程安全设计

Spring 在 Bean 定义的注册过程中,采用了多种线程安全策略:

  • 并发集合:使用ConcurrentHashMap作为主要存储结构
  • 锁机制:在 Bean 创建过程中,使用synchronized保证关键操作的原子性
  • 不可变列表:在并发场景下,创建新的列表而不是修改原有列表

💡关键理解:这种设计既保证了线程安全,又兼顾了性能。在单线程场景下(Bean 创建前),不需要加锁;在多线程场景下(Bean 创建中),使用锁保证安全。

3. 配置冻结机制

Spring 提供了配置冻结机制(freezeConfiguration()),冻结后:

  • 类型缓存生效:使用allBeanNamesByTypesingletonBeanNamesByType缓存提高查找效率
  • 动态注册需要清理缓存:如果动态注册新的 Bean 定义,需要清理类型缓存,避免缓存过时

💡设计优势

  • 性能优化:配置冻结后,Spring 假设 Bean 定义不会再改变,可以使用缓存
  • 灵活扩展:仍然支持动态注册,但需要清理相关缓存

4. 校验机制

Spring 在注册 Bean 定义时,会进行多层校验:

  • 接口层校验AbstractBeanDefinition.validate()进行基础校验
  • 实现层校验:子类可以重写校验方法,添加特定类型的校验
  • 覆盖控制:通过allowBeanDefinitionOverriding控制是否允许覆盖

💡关键理解:多层校验确保了 Bean 定义的完整性和正确性,在注册阶段就发现问题,而不是等到 Bean 实例化时才发现。

5. 别名处理机制

Spring 对别名的处理非常灵活,但也增加了复杂性:

  • 别名转 Bean 名称:支持将别名转换为正式的 Bean 名称
  • 循环检测:检测别名之间的循环引用
  • 覆盖控制:通过allowAliasOverriding控制别名覆盖行为

⚠️注意:虽然 Spring 提供了灵活的别名机制,但在实际开发中,建议谨慎使用,避免配置复杂化。


大白话总结

让我们用大白话再梳理一遍:

1. 这篇文章讲的是什么?

📝简单说:装饰后的 Bean 定义会被注册到 Spring 容器中,成为后续创建 Bean 实例的依据。注册过程包括校验、存储、缓存更新等多个步骤。

💡类比:就像把一个人的档案(Bean 定义)登记到人事系统中(Spring 容器),需要检查档案是否完整(校验),然后存入档案库(beanDefinitionMap),并更新各种索引(各种缓存),方便后续查找和使用。

2. 注册过程是怎么样的?

📝简单说

  1. 校验 Bean 定义:检查 Bean 定义的配置是否合理(如methodOverrideFactoryMethodName不能并存)
  2. 检查是否已存在
    • 如果已存在,检查是否允许覆盖,允许则替换,不允许则报错
    • 如果不存在但名称是别名,需要处理别名的转换
  3. 存储 Bean 定义:将 Bean 定义存入beanDefinitionMap
  4. 更新名称列表:将 Bean 名称添加到beanDefinitionNames列表(如果 Bean 正在创建,需要加锁)
  5. 清理相关缓存:如果存在旧的 Bean 定义或配置已冻结,清理相关缓存
  6. 标记主要 Bean:如果 Bean 定义是primary,添加到primaryBeanNames

💡类比:就像登记档案的流程:先检查档案是否完整(校验)→ 看看是否已经登记过(检查已存在)→ 存入档案库(存储)→ 更新目录(更新列表)→ 清理旧索引(清理缓存)→ 标记重要档案(标记 primary)。

3. 为什么需要这么多缓存?

📝简单说

  • beanDefinitionMap:主存储,所有 Bean 定义都在这里,通过名称快速查找
  • beanDefinitionNames:名称列表,保持注册顺序,用于按顺序遍历所有 Bean
  • allBeanNamesByTypesingletonBeanNamesByType:类型索引,配置冻结后使用,提高按类型查找的效率
  • primaryBeanNames:主要 Bean 列表,快速判断哪个 Bean 是primary
  • aliasMap:别名索引,通过别名快速找到原始 Bean 名称

💡类比:就像图书馆的索引系统,有主目录(beanDefinitionMap)、总目录列表(beanDefinitionNames)、分类索引(类型缓存)、推荐书目列表(primaryBeanNames)、别名索引(aliasMap),不同的查找需求使用不同的索引。

4. 为什么在 Bean 创建过程中需要加锁?

📝简单说:在 Bean 创建过程中,可能有多个线程同时在操作 Bean 定义相关的数据结构。如果直接修改beanDefinitionNames列表,可能会导致并发遍历时出现ConcurrentModificationException。因此,Spring 采用创建新列表的方式,保证线程安全。

💡类比:就像在图书馆还在开放时(Bean 创建中),如果要更新目录列表,不能直接修改正在使用的目录(避免读者找不到书),而是创建一份新的目录(新列表),然后用新目录替换旧目录。

5. 配置冻结是什么?

📝简单说:配置冻结是 Spring 的一个优化机制。当配置冻结后,Spring 认为 Bean 定义不会再改变,可以使用类型缓存来提高查找效率。如果此时动态注册新的 Bean 定义,需要清理类型缓存,避免缓存过时。

💡类比:就像图书馆闭馆整理(配置冻结),这时候可以建立完整的索引系统(类型缓存),提高查找效率。但如果这时候有新书上架(动态注册),需要更新索引(清理缓存)。


总结

通过本文的学习,我们深入了解了装饰后的 Bean 定义会被用来做什么:

核心流程

  1. Bean 定义校验:调用AbstractBeanDefinition.validate()进行基础校验,子类可以重写添加特定校验
  2. 检查是否已存在:从beanDefinitionMap中查找,如果存在则检查是否允许覆盖
  3. 处理别名场景:如果 Bean 名称是别名,需要获取原始名称并处理别名转换
  4. 存储 Bean 定义:将 Bean 定义存入beanDefinitionMap
  5. 更新名称列表:将 Bean 名称添加到beanDefinitionNames(Bean 创建中需要加锁)
  6. 清理相关缓存:如果存在旧的 Bean 定义或配置已冻结,清理相关缓存
  7. 标记主要 Bean:如果 Bean 定义是primary,添加到primaryBeanNames

关键数据结构

  • beanDefinitionMapConcurrentHashMap<String, BeanDefinition>,主存储,Bean 名称到 Bean 定义的映射
  • beanDefinitionNamesList<String>,Bean 名称列表,保持注册顺序
  • allBeanNamesByTypeMap<Class<?>, String[]>,按类型查找所有 Bean 名称的缓存(配置冻结后使用)
  • singletonBeanNamesByTypeMap<Class<?>, String[]>,按类型查找单例 Bean 名称的缓存(配置冻结后使用)
  • primaryBeanNamesSet<String>,标记为primary的 Bean 名称集合
  • aliasMapMap<String, String>,别名到原始 Bean 名称的映射

设计亮点

  • 分层缓存设计:多层次缓存结构,不同类型的查找使用不同的缓存,提高效率
  • 线程安全设计:使用并发集合和锁机制,保证多线程环境下的安全性
  • 配置冻结机制:优化机制,配置冻结后使用类型缓存提高查找效率
  • 校验机制:多层校验确保 Bean 定义的完整性和正确性
  • 别名处理机制:灵活的别名处理,支持别名转换和循环检测

注意事项

⚠️开发建议

  • 在开发过程中,建议谨慎使用别名转 Bean 名称的机制,避免配置复杂化
  • 配置冻结后,如果需要动态注册 Bean 定义,需要注意清理相关缓存
  • Bean 定义覆盖在生产环境通常应该禁止,以保证配置的稳定性

思考题

  1. 为什么 Spring 要在注册 Bean 定义时进行校验?校验的时机选择有什么考虑?
  2. 为什么beanDefinitionNames使用List而不是Set保持顺序有什么好处?
  3. 配置冻结后,如果动态注册 Bean 定义不清理类型缓存会有什么问题?
  4. 为什么在 Bean 创建过程中需要创建新的beanDefinitionNames列表,而不是直接修改?
  5. 别名转 Bean 名称的机制在什么场景下会用到?这种设计的优缺点是什么?

这些问题留给大家思考,我们会在后续的文章中继续深入探索。


参考资料

  • Spring Framework 源码:org.springframework.beans.factory.support.BeanDefinitionReaderUtils
  • Spring Framework 源码:org.springframework.beans.factory.support.DefaultListableBeanFactory
  • Spring Framework 源码:org.springframework.beans.factory.config.AbstractBeanDefinition
  • 上一篇文章:BeanDefinitionHolder还要修饰吗?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询