《Spring Boot 高级》1.2 Bean 定义注册与合并

BeanDefinition 是容器持有的元数据契约:本节拆开 AbstractBeanDefinition 的字段结构、BeanDefinitionRegistry 的注册与覆盖判定、组件扫描与 @Configuration 解析如何产出定义,讲清父子定义合并成 RootBeanDefinition 的过程,以及 @Primary 与别名在候选筛选里的作用。

本节目标:看清容器里到底存了什么——BeanDefinition 的字段、注册与覆盖判定、扫描器与配置类解析器的产出方式,以及父子定义如何合并成可实例化的 RootBeanDefinition。
适用版本:Spring Boot 4.1.x(Java 21)

1.2 Bean 定义注册与合并

1.1 讲到 refresh() 的第 5 步 invokeBeanFactoryPostProcessors 会产出大量定义。本节要回答:这些「定义」到底是什么?为什么改了定义就能改变 bean 的行为,而不用改类?

关键认知:容器在实例化之前,世界里没有对象,只有 BeanDefinition。@Component、@Bean、@Configuration 这些注解,最终都只是被翻译成 BeanDefinition 的字段值。理解了定义的结构和注册路径,就能解释「为什么同名的两个定义只有一个生效」「为什么 @Bean 方法返回的代理对象和普通对象不一样」。

1.2.1 BeanDefinition 是容器的元数据契约

org.springframework.beans.factory.config.BeanDefinition 是一个接口,定义了一组 getter/setter。实现基类是 org.springframework.beans.factory.support.AbstractBeanDefinition,它的核心字段(已从本机 spring-beans-7.0.9.jar 核实):

字段类型含义
beanClassObject(Class 或 String)类名或已解析的 Class
scopeStringsingleton / prototype,默认 "" 表示单例
lazyInitBoolean是否延迟初始化,null 表示沿用默认
primaryboolean是否为首选候选
autowireCandidateboolean是否参与自动装配候选
dependsOnString[]强制先初始化的 bean 名
factoryBeanName / factoryMethodNameString工厂方法来源
constructorArgumentValuesConstructorArgumentValues构造器参数值
propertyValuesMutablePropertyValuessetter 注入值
initMethodNames / destroyMethodNamesString[]生命周期回调方法
autowireModeint自动装配模式
qualifiersMap<String, AutowireCandidateQualifier>@Qualifier 等限定符

注意 initMethodNames / destroyMethodNames 是数组——这解释了为什么一个 bean 可以声明多个 destroyMethod。定义里几乎不含「值」以外的逻辑,isSingleton() 这类方法只是对 scope 的派生判断。

定义是可复制的。AbstractBeanDefinition.overrideFrom(BeanDefinition) 把另一个定义的值覆盖到当前定义,applyDefaults(BeanDefinitionDefaults) 则补默认值——父子合并用的正是这套机制。

1.2.2 定义的家族:谁在生产定义

定义有多个实现,各司其职:

实现类来源特点
RootBeanDefinition合并结果、@Bean 方法无父定义,可直接实例化
ChildBeanDefinition早期 XML 的 <bean parent="...">有 parentName,需合并
GenericBeanDefinition通用容器可设 parentName,XML 时代主力
ScannedGenericBeanDefinition组件扫描携带 AnnotationMetadata
AnnotatedGenericBeanDefinitionAnnotatedBeanDefinitionReader.register()携带注解元数据
ConfigurationClassBeanDefinition@Bean 方法记录工厂方法与 @Bean 元信息

最后一个是 ConfigurationClassBeanDefinitionReader 的静态内部类(ConfigurationClassBeanDefinitionReader$ConfigurationClassBeanDefinition),它记录了「哪个配置类的哪个方法负责产出这个 bean」,是 @Bean 方法 factoryBeanName / factoryMethodName 的来源。

除了注解与 XML,定义还可以用代码注册。GenericApplicationContext 提供 registerBean(Class<T>, Object...) 与 registerBean(String, Class<T>, Object...)(已核实),后者的第一个参数就是 bean 名:

try (GenericApplicationContext ctx = new GenericApplicationContext()) {
    ctx.registerBean("bookRepository", BookRepository.class);
    ctx.registerBean("loanService", LoanService.class, BookRepository.class);
    ctx.refresh();
    LoanService loanService = ctx.getBean(LoanService.class);
}

这条路径跳过了扫描,直接往注册表里塞定义,是理解「注解只是定义的一种来源」的最短方式。

1.2.3 BeanDefinitionRegistry 的注册过程

org.springframework.beans.factory.support.BeanDefinitionRegistry 继承 org.springframework.core.AliasRegistry,核心方法:

void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException;
void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
boolean containsBeanDefinition(String beanName);
String[] getBeanDefinitionNames();
int getBeanDefinitionCount();

默认实现是 DefaultListableBeanFactory,它内部用两个字段存定义(已核实为 private final Map<String, BeanDefinition> beanDefinitionMap 与 private volatile List<String> beanDefinitionNames)。注册时若发现同名定义已存在,会走覆盖判定:是否允许覆盖取决于 allowBeanDefinitionOverriding。Boot 4.x 默认 false,重复定义直接抛 org.springframework.beans.factory.support.BeanDefinitionOverrideException(已核实)。

BeanDefinitionReaderUtils.registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry) 是扫描器与解析器共用的注册入口,它顺带处理别名注册。定义一旦注册,beanDefinitionNames 的顺序就被固定下来——这也是 @Order 之外,bean 初始化的隐含顺序来源之一。

覆盖判定本身也在 DefaultListableBeanFactory.registerBeanDefinition 内完成:若 beanDefinitionMap 已有同名项且 allowBeanDefinitionOverriding 为 false,抛 BeanDefinitionOverrideException;为 true 时用新定义替换旧定义,但不会重置已经存在的单例。这常导致「改了定义但运行时还是老对象」的困惑——改定义必须在实例化之前(即 refresh 的第 5 步)才可靠。

想确认谁注册了什么,最直接的办法是打开 debug 日志或在注册方法上下断点,观察 beanDefinitionNames 的增量。生产里遇到 BeanDefinitionOverrideException,通常是自己 @Bean 的名字与自动配置撞车,此时要么改名字,要么显式 spring.main.allow-bean-definition-overriding=true(不推荐,会掩盖真实冲突)。

1.2.4 组件扫描如何产出定义

@ComponentScan 的解析链是:ClassPathScanningCandidateComponentProvider.findCandidateComponents(String) 找出候选,ClassPathBeanDefinitionScanner.doScan(String...) 把候选注册进 BeanDefinitionRegistry。

findCandidateComponents 做的是类路径扫描 + 候选过滤:默认过滤规则由受保护的 registerDefaultFilters() 设定(已核实),包含 @Component、@ManagedBean、@Named。每个命中类被包装成 ScannedGenericBeanDefinition,其 AnnotationMetadata 保留了类上的全部注解,供后续 @Scope、@Lazy、@Primary 读取。

bean 名的生成走 BeanNameGenerator:默认 AnnotationBeanNameGenerator 用类名首字母小写;若注解显式给了 value 则用该值。@Service("loanService") 就是把名字写死在这个环节。

@Service("loanService")
public class LoanService {
    // 扫描时产出名为 loanService 的 ScannedGenericBeanDefinition
}

也可以自定义过滤器,让扫描器认一个自定义注解:

@Component
public class ScanConfig {

    @Bean
    static ClassPathBeanDefinitionScanner customScanner(BeanDefinitionRegistry registry) {
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
        scanner.addIncludeFilter((metadataReader, factory) ->
            metadataReader.getAnnotationMetadata().hasAnnotation(Audited.class.getName()));
        return scanner;
    }
}

1.2.5 @Configuration 与 @Bean 的定义产出

@Configuration 类本身也是一个 @Component,同样被扫描成定义。真正解析 @Bean 方法的是 ConfigurationClassPostProcessor(实现 BeanDefinitionRegistryPostProcessor + PriorityOrdered),它内部:

  1. ConfigurationClassParser 递归解析 @ComponentScan / @Import / @Bean,构建出 ConfigurationClass 集合;
  2. ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(Set<ConfigurationClass>) 把每个 @Bean 方法翻译成一个 ConfigurationClassBeanDefinition,factoryBeanName 指向配置类,factoryMethodName 指向方法。

@Import 是另一条产出定义的通道,三种形态(类均已在 spring-context 核实):

形态行为
@Import(SomeConfig.class)直接把该类作为配置类处理
@Import(SomeSelector.class)调 ImportSelector.selectImports(AnnotationMetadata) 拿到类名数组再处理
@Import(SomeRegistrar.class)调 ImportBeanDefinitionRegistrar.registerBeanDefinitions(...),可手工注册任意定义

第三种形态最灵活,可以完全绕开注解,用代码往注册表里塞定义:

public class AuditedBeanRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {
        BeanDefinition bd = BeanDefinitionBuilder
                .rootBeanDefinition(AuditLogService.class)
                .setScope(BeanDefinition.SCOPE_SINGLETON)
                .getBeanDefinition();
        registry.registerBeanDefinition("auditLogService", bd);
    }
}

这也是 @Configuration 默认 proxyBeanMethods=true 的意义:配置类会被 CGLIB 增强,@Bean 方法之间互相调用时返回同一个单例,而不是每次 new。改成 @Configuration(proxyBeanMethods = false) 就退化成普通工厂方法,方法间调用不再保证单例——这正是 @Component 类里放 @Bean 时的行为。

1.2.6 父子定义的合并

ChildBeanDefinition 或带 parentName 的 GenericBeanDefinition 不能直接实例化,必须先与父定义合并。合并入口是 AbstractBeanFactory.getMergedLocalBeanDefinition(String),它调用 getMergedBeanDefinition(name, bd, parentBd) 产出一个全新的 RootBeanDefinition,逐字段把父定义的值填进子定义的缺口。

合并结果缓存在 AbstractBeanFactory 的 private final Map<String, RootBeanDefinition> mergedBeanDefinitions 里。缓存失效靠 clearMergedBeanDefinition(String)——当某个定义被重新注册、或父定义变化时调用。判断「要不要重新合并」依赖 RootBeanDefinition.stale 标志。

要点:合并是「复制」不是「引用」。合并后的 RootBeanDefinition 与原始定义相互独立,后续对合并结果做的后处理(例如 MergedBeanDefinitionPostProcessor 写注解元数据)不会污染原定义。这也解释了为什么 @Bean 方法的返回类型、@Scope 元数据能在实例化前被一次性解析并缓存。

一个可直接观察的现象:同名 bean 若既有父定义又有子定义,最终实例化用的是合并后的 RootBeanDefinition;在 getMergedBeanDefinition 上下断点,能看到它把父的 scope、initMethodNames 等字段搬过来,子定义里显式设过的字段则保持不变。

1.2.7 @Primary 与别名的解析

按类型注入遇到多个候选时,DefaultListableBeanFactory.doResolveDependency 会先 findAutowireCandidates 收集候选,再 determineAutowireCandidate 选一个。筛选顺序大致是:

优先级判据相关方法
1候选名与注入点名字匹配matchesBeanName
2标了 @PrimaryisPrimary
3@Priority 数值更高依赖 AutowireCandidateResolver
4都无法区分抛 NoUniqueBeanDefinitionException

@Primary 最终写到定义字段的 primary 上,isPrimary(String, Object) 读取它。注意它与 @Qualifier 的分工:@Qualifier 是「按名字挑」,@Primary 是「多个都行时挑默认那个」。

@Configuration
public class RepositoryConfig {

    @Bean
    @Primary
    public BookRepository jdbcBookRepository() {
        return new JdbcBookRepository();
    }

    @Bean
    public BookRepository inMemoryBookRepository() {
        return new InMemoryBookRepository();
    }
}

别名则由 org.springframework.core.AliasRegistry 管理:registerAlias(beanName, alias)、getAliases(beanName)、canonicalName(alias)。注入时 AbstractBeanFactory.transformedBeanName(String) 会先把别名(含 & 前缀的 FactoryBean 解引用)规范成真实 bean 名。所以 @Bean({"a", "b"}) 里除第一个之外的名字都是别名,它们指向同一个定义:

@Bean({"bookRepository", "repo"})
public BookRepository bookRepository() {
    return new JdbcBookRepository();
}

上例中 bookRepository 是 bean 名,repo 是别名;getBean("repo") 与 getBean("bookRepository") 返回同一实例,getAliases("bookRepository") 返回 ["repo"]。

1.2.8 @Conditional 在定义阶段的介入

自动配置之所以能「按需装配」,靠的是 @Conditional 家族。它的判定发生在定义产出阶段:ConfigurationClassParser 在每个配置类上调用 ConditionEvaluator.shouldSkip(AnnotatedTypeMetadata, ConfigurationPhase),返回 true 就整类跳过,根本不产定义。

这与「先产定义再实例化」是两回事:条件不满足的自动配置类,连 BeanDefinition 都不会注册。想知道某个自动配置为什么没生效,可以在 ConditionEvaluator.shouldSkip 上下断点,或在开启 debug=true 后看条件评估报告(正负匹配都会打印)。第 2 章会专门展开条件家族与自动配置顺序。

1.2.9 知道之后能做什么

排查「同名 bean 冲突」。 4.x 默认禁止覆盖,报 BeanDefinitionOverrideException 时,用 containsBeanDefinition 与 getBeanDefinitionNames 打印注册顺序,就能定位是谁先注册、谁被拒。

动态改定义。 写一个 BeanDefinitionRegistryPostProcessor,在 postProcessBeanDefinitionRegistry 里遍历 getBeanDefinitionNames(),把某个定义的 setLazyInit(true) 或 setScope("prototype"),验证改定义确实能改变运行时行为。

验证合并缓存。 在 getMergedLocalBeanDefinition 里下断点,连续两次 getBean("loanService"),第二次不会再进合并逻辑——说明命中 mergedBeanDefinitions 缓存。

验证别名归一。 给一个 bean 注册别名后,getBean(alias) 与 getBean(beanName) 返回同一实例;在 transformedBeanName 下断点,能看到别名在进入解析前就被转成了真实名。

小结

  • BeanDefinition 是实例化前的唯一真相,注解最终都翻译成它的字段。
  • 定义有多个实现,扫描产 ScannedGenericBeanDefinition,@Bean 产 ConfigurationClassBeanDefinition,合并结果一律是 RootBeanDefinition。
  • DefaultListableBeanFactory 用 beanDefinitionMap + beanDefinitionNames 存定义,同名覆盖默认被禁止。
  • 父子定义合并是「复制」,结果缓存在 mergedBeanDefinitions,靠 stale 标志判断失效。
  • @Primary 是定义字段,别名由 AliasRegistry 管理,注入前经 transformedBeanName 归一化。
  • @Conditional 在定义产出阶段就决定「要不要有这个定义」,不满足条件的类连定义都不会注册。

定义备齐了,下一步就是按定义把对象造出来、把依赖接上。下一节讲 getBean() 的解析流程与循环依赖的三种处理方式。

阅读导航:上一节:1.1 ApplicationContext 启动流程 · 下一节:1.3 依赖解析与循环依赖 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计