本节目标:看清容器里到底存了什么——
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 核实):
| 字段 | 类型 | 含义 |
|---|---|---|
beanClass | Object(Class 或 String) | 类名或已解析的 Class |
scope | String | singleton / prototype,默认 "" 表示单例 |
lazyInit | Boolean | 是否延迟初始化,null 表示沿用默认 |
primary | boolean | 是否为首选候选 |
autowireCandidate | boolean | 是否参与自动装配候选 |
dependsOn | String[] | 强制先初始化的 bean 名 |
factoryBeanName / factoryMethodName | String | 工厂方法来源 |
constructorArgumentValues | ConstructorArgumentValues | 构造器参数值 |
propertyValues | MutablePropertyValues | setter 注入值 |
initMethodNames / destroyMethodNames | String[] | 生命周期回调方法 |
autowireMode | int | 自动装配模式 |
qualifiers | Map<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 |
AnnotatedGenericBeanDefinition | AnnotatedBeanDefinitionReader.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),它内部:
ConfigurationClassParser递归解析@ComponentScan/@Import/@Bean,构建出ConfigurationClass集合;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 | 标了 @Primary | isPrimary |
| 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 依赖解析与循环依赖 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。