BeanDefinition、扫描与配置类:元数据怎样注册到容器
一个类明明带 @Component 却没有 Bean,另一个配置类没被显式扫描却注册了几十个定义。要解释这种差异,必须把“发现 Java 类”和“注册 BeanDefinition”分开。Spring 不会先实例化所有类再决定保留谁;它先读取元数据、处理配置类与 Import,生成定义,经过注册表后处理器修改,最后才允许普通单例进入创建阶段。
BeanDefinition 参考文档 描述容器保存的配置元数据;工程上还要继续追踪定义来源、合并结果和修改时序,才能还原最终注册图。
BeanDefinition 是可合并的运行配方
定义记录 bean class 或 factory method、scope、lazy、dependsOn、autowire candidate、primary、构造参数、属性、init/destroy 与 source。父子定义、配置类 factory bean 和 merged definition 会让最终配方不同于最初扫描结果。排障必须查看最终定义及来源,而不是只看类注解。
ClassPathBeanDefinitionScanner 用 metadata reader 在尽量不加载类的情况下判断候选,随后应用 include/exclude filter、名称生成器与 scope resolver。扫描包过宽不仅增加启动成本,还会把测试配置、内部实现或第三方组件意外注册。默认包与应用根包位置因此是架构边界。
配置类解析器处理 @ComponentScan、@Import、@ImportResource、@Bean 和条件元数据,可能递归发现新配置类。ImportSelector 返回导入类名,ImportBeanDefinitionRegistrar 直接操作 registry;它们适合框架集成,也能在缺少审计时制造大量隐式定义。
注册阶段不能偷偷创建业务对象
BeanDefinitionRegistryPostProcessor 可以先注册/修改定义,随后 BeanFactoryPostProcessor 修改 BeanFactory 与定义属性。此时普通 BeanPostProcessor 尚未完整注册,调用 getBean() 会让业务 Bean 提前创建,错过自动代理、注入处理或完整生命周期。
examples/backend-development/spring-framework/bean-definition-registration/ConfigurationParsingDemo.java 把阶段边界输出为:
javac --release 17 -Xlint:all -Werror examples/backend-development/spring-framework/bean-definition-registration/DefinitionRegistryDemo.java examples/backend-development/spring-framework/bean-definition-registration/ConfigurationParsingDemo.java
java -cp examples/backend-development/spring-framework/bean-definition-registration ConfigurationParsingDemophases=[parse-config, register-imports, invoke-registry-post-processors, freeze-definitions] beanInstantiatedDuringDefinitionPhase=false这是应守住的不变量。真实 refresh() 还处理 Environment、消息源、事件与特殊 Bean,但定义阶段不应为了普通配置计算启动完整业务图。框架扩展需要配置时优先读取 Environment、Binder 或 definition property,而不是取业务 Bean。
名称冲突必须暴露,而不是静默覆盖
同名 BeanDefinition 来自扫描、@Bean、XML、Import registrar 或父子 Context。允许 override 时,后注册者替换前者,顺序改变便可能切换实现。公共库不应用常见名称抢占业务 Bean,也不应要求全局开启覆盖;默认定义要有明确条件与退出方式。
DefinitionRegistryDemo.java 用 putIfAbsent 模拟拒绝重复:
java -cp examples/backend-development/spring-framework/bean-definition-registration DefinitionRegistryDemodefinitions=1 duplicateRejected=true class=DefaultAuditClientSpring registry 的真实覆盖策略由 BeanFactory/应用配置决定。工程门禁应导出 bean name、class/factory method、resource description、role、scope 和条件来源,与基线比较。只比较 Bean 数会漏掉同名实现被替换。
Bean 名称也参与按名注入、Qualifier、JMX、Actuator 与测试替换。随意改变 @Bean 方法名或 NameGenerator 可能是兼容性变更。FactoryBean 产品与 &factoryName 又共享命名空间,诊断时必须区分。
配置类增强影响方法调用语义
完整 @Configuration 配置类可被增强,使配置类内部调用另一个 @Bean 方法时从容器取得受 scope 管理的对象,而不是普通 Java 方法每次 new。lite 模式组件中的 @Bean 方法调用则按普通 Java 语义执行。把配置类从 full 改为 lite,可能悄悄产生容器外对象。
更稳的配置方式是让 @Bean 方法通过参数声明依赖,不依赖配置类方法互调。这样依赖图对容器和测试都显式,也减少代理限制。配置类/Bean 方法若因 final、private 或语言特性无法按预期增强,应在升级测试中直接比较对象 identity。
AOT 与原生镜像放大元数据边界
反射扫描、运行时动态注册和基于任意类名的 SpEL 在 AOT 环境需要提前生成 hints 或改为显式注册。即使不使用原生镜像,AOT 约束也能暴露“运行时才知道定义”的隐式设计。框架扩展应尽量让定义来源稳定、类型可推导、条件可重放。
定义阶段的观测包括扫描候选数/耗时、配置类数量、Import 链、registry post-processor 耗时、覆盖/别名冲突和定义基线 diff。启动失败时先看 BeanDefinitionStoreException 的 resource/source 与配置解析栈,不要跳到 Bean 生命周期。只有定义正确进入 registry,后续注入与代理才有讨论基础。
Factory Method、父子定义与合并结果影响真正配方
BeanDefinition 可以描述构造器创建、静态工厂方法和实例工厂方法。实例工厂方式还依赖另一个 Bean,类型预测与创建顺序因此更复杂;重载工厂方法若只靠运行时参数猜测,错误可能直到实例化才出现。框架扩展应尽量记录解析后的工厂 Bean、方法签名和返回类型,而不是只保留一个方法名。
容器在创建前会得到 merged BeanDefinition,将父定义、子定义、scope、构造参数、属性值和方法覆盖等元数据合并。后处理器常缓存这个合并结果的注入信息。definition 被重置、父定义变化或配置类重新解析时,相关缓存必须一起失效;只按 bean name 缓存会把旧类或旧 ClassLoader 的成员带进新对象。
abstract definition 可以只作为模板,不应被预实例化;lazy 只影响普通创建时机,不阻止定义注册和后处理;dependsOn 增加初始化顺序约束,却不等于依赖注入。把这些属性混用会出现“明明 lazy 为什么启动仍失败”或“写了 dependsOn 为什么字段仍为空”的误解。诊断输出要展示原始 definition 和 merged definition 的差异。
Alias 只是同一规范名称的别名,不是复制一个定义。注册、覆盖和查找前要做 canonical name 归一化,并检测别名环。业务若用多个历史名字兼容迁移,应有删除时间和引用统计;无限保留别名会让日志、指标和依赖图对同一对象出现多个身份。
条件化注册需要解释未命中路径
组件扫描、配置类条件和 ImportSelector 会让定义集合随 classpath、属性、资源和父容器变化。同一源码在测试与生产产生不同 Bean 数,往往不是随机,而是条件输入不同。每个条件化扩展应能回答:读取了什么输入、匹配结果、未匹配原因、注册了哪些名称,以及是否被用户定义替代。
条件判断阶段不应连接数据库或探测远程服务。远程抖动若决定 definition 是否存在,会让容器结构不可重复,也让滚动发布的实例产生不同能力。外部可用性属于运行期健康和降级;定义条件应依赖版本化配置、classpath 或确定的本地元数据。
扫描范围是架构边界也是性能预算。根包过宽会把测试夹具、内部配置和第三方注解类注册进来;多个扫描器重复遍历同一 jar 又会增加启动时间。应统计每个扫描入口访问资源数、候选数、排除数、重复数与耗时,并为不该注册的包写负例。发现 definition 数量突增时,先比较来源分布,而非立即提高启动超时。
自定义 BeanDefinitionRegistrar 应生成可追踪名称和 resource description,正确设置 role、source、scope、primary 与懒加载等语义。使用随机名称可以绕过冲突,却让覆盖、监控和 AOT 输出无法稳定比对。基础设施定义应标记 infrastructure role,减少被普通业务后处理器误命中,但 role 不是安全隔离,处理器仍需做精确类型判断。
注册链的回归资产是一份可比较快照
完成 refresh 前,可以把 definition 快照归一化为名称、bean class/factory method、scope、role、dependsOn、primary、lazy、resource 和关键属性摘要,去掉不稳定 identity 后纳入集成测试。升级框架或 starter 时对比新增、删除和变化项,能够提前发现隐式自动注册。
快照不是为了锁死所有内部 Bean 名称。应将业务契约定义、团队扩展和框架内部定义分层:业务层严格审查,基础设施层按前缀/角色统计,框架内部只关注数量与关键能力。否则补丁升级的合法内部变化会制造大量噪声。
注册阶段的完成条件有三个:相同输入得到确定定义图;冲突和覆盖可解释;普通 Bean 尚未因扩展依赖而提前创建。满足这三个条件,后续生命周期、代理和事务才建立在稳定配方上。
