BeanDefinition 详解:Bean 的户口本与注册表
新手最容易把 Spring 想成「一个会批量帮我 new 对象的工厂」。真相是:容器在启动阶段压根不造对象,它先攒了一本「怎么做」的说明书——记类名、记作用域、记初始化方法名。这本说明书就叫 BeanDefinition;真正的对象(Bean)要等到 refresh() 后半段才照着它生产出来。所以本篇所有话题都围绕一句话:管的是定义,不是对象。
先把五个词说清楚(全文反复出现):
- 容器:帮你创建和管理对象的那个「大管家」对象(
ApplicationContext/BeanFactory),你向它要东西(getBean),它按名字找 - Bean:交给容器创建和管理的那个对象,通常就是你写的一个普通 Java 类的实例
- BeanDefinition:关于「怎么造某个 Bean」的一份数据(不是代码),字段包括用哪个类、单例还是原型、懒不懒加载、初始化/销毁方法叫什么
- 注册表:存放所有定义的那张 Map——
beanName → BeanDefinition,容器查谁都先翻这张表 - 反射:程序在运行时「看着类名去调构造器」的技术;容器没编译期绑定你的类,全靠反射按定义里的
beanClass把对象造出来
把容器想成一家餐厅的后厨。BeanDefinition 是墙上贴的菜谱卡:菜名(beanName)、用料(class)、份量和火候(scope、lazyInit、initMethodName)。顾客点菜(getBean)时厨房才照卡做菜(Bean)。菜谱可以先贴好、可以互相抄改(父子继承)、也可以被行政总厨统一改写(BeanFactoryPostProcessor)——这些动作全部发生在任何一道菜做出来之前。这就是「定义先于对象」的全部含义。

学完这一篇,你应该能回答三个问题:
- 我在类上写的
@Scope("prototype"),究竟落到了哪张表的哪个字段上? - 「XML 配置」和「注解配置」到底是两套机制,还是同一套机制的两个入口?
- 为什么想改「有哪些 Bean」必须在
refresh()第 5 步之前动手,而想改「对象长什么样」得用另一个接口?
如果问"容器里装的是什么",直觉回答是"装的是对象"。但这恰恰是个常见误解——容器启动过程中真正被登记、被检索、被修改的东西,是"对象的定义",而不是对象本身。
用户籍打个比方:Bean 是户籍系统里那个具体的"人",而 BeanDefinition 是 TA 的档案——姓名(beanName)、住址(class)、家庭成员(dependsOn)、备注(作用域、懒加载、初始化方法……)。你要查一个人,先翻的是档案;真正被复制、被合并、被改写的,也是档案。对象只在最后一刻才按档案"生产"出来。

这带来两个关键能力:
- 延迟与条件:有了定义,容器就能先"看清全局再决定做什么"——合并父子定义、判断条件装配、提前为 AOP 建代理,全都发生在对象诞生之前
- 可编程:只要往注册表里塞一份定义,容器就会像对待任何 Bean 一样对待它。这正是自动配置、
@Bean、@Import能"无中生有"造出 Bean 的底层原因
BeanDefinition 是一个接口,真正的载体是 AbstractBeanDefinition 及其子类。核心字段如下:
| 字段 | 一句人话 |
|---|---|
beanClass | 这个 Bean 要用哪个类来实例化(也可能是工厂方法所在类) |
scope | singleton / prototype,决定是"一个实例"还是"一堆实例" |
lazyInit | 为 true 时不参与启动预实例化,第一次用到才创建 |
primary | 同类型多个候选时,优先选它 |
dependsOn | 必须先于本 Bean 创建的名字列表(不参与注入,只保证顺序) |
initMethodName | 初始化回调方法名(等价 XML 的 init-method) |
destroyMethodName | 销毁回调方法名(等价 XML 的 destroy-method) |
autowireMode | 自动装配模式(byName / byType / constructor / no) |
abstract | 抽象定义,只作为父模板,永远不会被实例化 |
parentName | 父定义的 beanName,用于合并 |
代码里看它长什么样:
// 用建造者风格创建一份定义(Spring 5.0+ 推荐方式)AbstractBeanDefinition bd = BeanDefinitionBuilder .genericBeanDefinition(HelloServiceImpl.class) .setScope(BeanDefinition.SCOPE_SINGLETON) .setLazyInit(true) .setDependsOn("dataSource") .setInitMethodName("warmUp") .setDestroyMethodName("clear") .addConstructorArgValue("Hello") .getBeanDefinition();System.out.println(bd.getBeanClassName()); // com.example.HelloServiceImplSystem.out.println(bd.getScope()); // singletonBeanDefinitionBuilder是官方提供的建造者,避免直接 new 具体子类addConstructorArgValue("Hello")对应 XML 里的<constructor-arg value="Hello"/>- 注意
setDependsOn只保证创建顺序,并不会有对象被注入进来——这点极易混淆 - 再补一个真实坑:上面那行
// singleton是「你显式设置过」的情形。没设置过时getScope()返回的是空串而不是"singleton"(默认单例靠isSingleton()判断),所以读定义请用isSingleton() / isPrototype(),别比字符串——第十三节练习会让你亲眼看到这一格是空的
提示:dependsOn 与"依赖注入"是两码事。前者是"请你先把它建好我再用",后者是"请把它塞进我的构造器 / setter"。前者管顺序,后者管传值。
上面这十个字段只是「纸上信息」。它们要变成你手里那个对象,中间还隔着六步——扫描、生成定义、注册、被改写、实例化、注入初始化。先花十秒把这条路线看熟,后面每个小节都在这条线上找一个位置:


定义从哪里来?最常见的三条路径:
其一,XML:
// XmlBeanDefinitionReader:把 <bean> 标签读成定义DefaultListableBeanFactory factory = new DefaultListableBeanFactory();XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);reader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml"));其二,注解扫描:
// ClassPathBeanDefinitionScanner:扫包 + 读注解,生成 ScannedGenericBeanDefinitionAnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();ctx.scan("com.example"); // 内部就是 ClassPathBeanDefinitionScannerctx.refresh();其三,@Bean 方法:
@Configurationpublic class AppConfig { @Bean // 由 ConfigurationClassBeanDefinitionReader 解析 public HelloService helloService() { return new HelloServiceImpl("Hello"); }}- 三条路径产出的都是
BeanDefinition,只是实现子类不同,进入的都是同一个BeanDefinitionRegistry - XML 走
XmlBeanDefinitionReader,注解扫描走ClassPathBeanDefinitionScanner,@Bean方法走ConfigurationClassBeanDefinitionReader - 所以"用 XML 还是注解"只在读取阶段不同,之后的注册、合并、实例化流程完全统一
把「同一段配置分别用注解和 XML 写」并排画出来,两边字段一一对得上——这正是上面那句结论的可视化证据:

像一家餐厅同时开了小程序点单和前台手写单两个入口。前厅收单方式完全不同,但两张单子最后都打印成后厨那台机器里的同一种工单(BeanDefinition)——后厨根本不知道也不关心客人是从哪个入口下单的。所以「XML 派 vs 注解派」的争论其实是个伪问题:争的是前厅,后厨只有一套流程。
既然定义才是核心,那我们完全可以在运行时手动塞一份进去——这就是"我自己也可以当容器":
package com.example;import org.springframework.beans.factory.support.RootBeanDefinition;import org.springframework.context.support.GenericApplicationContext;public class ManualRegistryDemo { public static void main(String[] args) { // 1. 空容器:不自动扫描、不自动加载 XML GenericApplicationContext ctx = new GenericApplicationContext(); // 2. 手动登记一份定义:beanName = "dynamicHelloService" RootBeanDefinition bd = new RootBeanDefinition(HelloServiceImpl.class); bd.getConstructorArgumentValues().addIndexedArgumentValue(0, "Hi"); ctx.registerBeanDefinition("dynamicHelloService", bd); // 3. 必须先 refresh,容器才会真正实例化注册过的定义 ctx.refresh(); // 4. 和任何 Bean 一样取用 HelloService hello = ctx.getBean("dynamicHelloService", HelloService.class); System.out.println(hello.sayHello("Manual")); ctx.close(); }}控制台输出:
Hi, ManualGenericApplicationContext是一个"干净"的容器:不自动扫描、不自动加载 XML,一切由你显式登记registerBeanDefinition(name, bd)之后还需要refresh()——注册只是"上户口",refresh才是"开始生产"- 输出
Hi, Manual说明构造器参数真的被注入生效了——整个过程你没写一行 XML 或注解
坑:在 refresh() 之后再调用 registerBeanDefinition 会抛 IllegalStateException(容器已激活)。运行时要动态加 Bean,应改用 BeanDefinitionRegistryPostProcessor,或在 refresh() 前完成登记;非要后置添加,可借助 DefaultListableBeanFactory 直接操作,但必须自己处理已实例化的单例缓存。
六个子类名字长得劝退,背是背不住的——但其实只有一件事值得记:这份定义是从哪扇门进来的。玩一局,先点实现类,再点它的出身:
这些子类的差异集中在"元数据怎么来、父定义怎么设",最终都会被规整进 AbstractBeanDefinition 的字段上。日常开发不必记名字,但读框架源码时,看到 ScannedGenericBeanDefinition 就知道"这是扫描来的"。
父子定义在真正实例化前会合并成一份完整的 RootBeanDefinition,这个过程叫 MergedBeanDefinition:
<bean id="baseService" abstract="true"> <property name="timeout" value="3000"/></bean><!-- child 继承 baseService:拿到 timeout=3000,同时可以覆盖 --><bean id="userService" class="com.example.UserService" parent="baseService"> <property name="timeout" value="5000"/></bean>- 子定义只写差异;未声明的字段从父定义继承,声明了的字段覆盖父定义
abstract="true"的父定义本身永远不会被实例化,它只是一份模板- 合并细节:
class不可被覆盖(否则报错),属性列表按名字覆盖而非"合并"
更强大的是:定义在实例化前还能被后置处理器批量改写。BeanFactoryPostProcessor 运行在 refresh() 的第 5 步,此时所有定义已注册、但尚无任何单例被创建——这正是"改定义的最后窗口":
@Componentpublic class LazyUpgrader implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { BeanDefinition bd = beanFactory.getBeanDefinition("userService"); bd.setLazyInit(true); // 把某个 Bean 改成懒加载 System.out.println("已改写定义: " + bd.getBeanClassName()); }}ConfigurationClassPostProcessor就是最重量级的一个:它扫描@Configuration、处理@Bean与@Import,把注解世界"翻译"成定义- 因为它在预实例化之前运行,所以它能决定"到底有哪些 Bean、每个 Bean 长什么样"
说明:BeanFactoryPostProcessor 改的是定义,BeanPostProcessor 改的是实例。前者在 refresh 第 5 步、对象诞生之前;后者在对象初始化前后。名字只差一个词,作用时机差一大截,是面试高频区分点。
这两个「只差一个词」的处理器,在时间轴上其实隔着半条命。动画里 ①→④ 那四格就是「档案还能改」的窗口,第 ④ 格一过,容器再没人去读定义;后面 ⑤ ⑥ 两格发生的都是对象层面的事:

@Bean 与 @Component 生成的定义并不等价。@Bean 方法默认是 full 模式(@Configuration 类被 CGLIB 增强,方法之间互相调用会返回同一个单例),而写在普通类里的 @Bean 是 lite 模式(不增强,方法直接当普通方法调用,可能产生多个实例)。把 @Bean 从 @Configuration 类挪到 @Component 类上,对象数量可能就从 1 变成了 2——定义没变,语义变了。
这条坑值得动图说明,因为「定义一模一样、结果差一个对象」是新手最难接受的一类差异。注意第 ② 格:full 模式下你调自己的 @Bean 方法会被拦下来先查池子;lite 模式没有这一拦,方法就是普通方法,调一次真 new 一次:

@Lazy 只改一个标志位。它并不"移动"任何代码,只是在定义上把 lazyInit 置为 true。所以:懒加载的 Bean 如果在启动时真的被别的 Bean 依赖,它照样会被创建——@Lazy 不是"永不创建",而是"没人用时才不创建"。
父子定义里最容易被误解的规则。父定义写了 class、子定义又写了不同的 class,启动直接报 Cannot override bean class。记住:可继承的是属性 / 构造器参数 / 作用域等配置,不可覆盖的是 class 本身。
下面这个演示把"Beans 列表里每个定义的状态变化"可视化出来——从注册、被后置处理器改写,到最终实例化:
不过 ioc 看的是「装配结果」。要真正看懂本篇的主线——注解是怎么变成一份定义的——得回到扫描现场。下面的实验就是这条链本身:先选「扫描与解析」看 @Component 如何被翻译成 AnnotatedGenericBeanDefinition,再选「scope / lazy 属性」看你写的注解值究竟落到哪个字段:
XML 那条路径同理,只是入口换成一个文件读取器。下面这个实验把 XML 容器的全过程摊开,重点看最后一个参数「写错 class 会怎样」——它对应第十三节速查表里的第一行:
定义不是写完就定了:refresh() 第 5 步上还有一批处理器专门负责「加定义」和「改定义」。自动配置之所以能凭空造出 Bean,靠的就是在这里塞定义。先用十二步实验定位这一步:
条件装配(@Conditional)则决定了一份定义要不要进注册表。Boot 里成千上万个自动配置 Bean 就是这样被筛掉的:
还有一个定义层面的特例必须在这里认识:有些定义写的类,和你 getBean 拿到的对象根本不是一个类型。FactoryBean 就是这样一份「说自己是工厂、容器却当它是产品」的定义——第二节那句「beanClass 也可能是工厂方法所在类」说的正是它。先看第一条结论:
想知道这算哪个类型,容器靠的是 getObjectType()——这也是 @ConditionalOnBean、按类型注入能不能命中的依据。这一格配错,报错会写成「定义明明在,却说没有这个类型的 Bean」:
上面这些实验回答的是「定义从哪来」,而这一段回答「定义怎么变成对象」。下面这台单步调试台左边是 getBean 的真实骨架,右边同步刷新此刻的变量与调用栈——连点「下一步」,盯住第 ④ 步:档案是在那一刻被合并、然后才被消费的:
ctx.getBean("userService"); // 你写的只有这一行String beanName = transformedName(name); // 别名与 & 前缀在这里被摊平Object cached = getSingleton(beanName); // 先问单例池:已经造好了吗RootBeanDefinition mbd = mergedDefinition(name); // 档案在这一刻被合并Object raw = createBeanInstance(mbd, args); // 反射调构造器,参数取自档案populateBean(raw, mbd); // 装配:把依赖填进字段/setterinitializeBean(raw, mbd); // 回调:初始化,然后交给池子| 你手上 | : 一行调用 |
| 容器状态 | refresh() 已跑完 |
ApplicationContext.getBean实验做到这里,可以换成自己敲命令了。这台控制台连着浏览器里那个真容器,beans 打出来的是注册表此刻的内容(名字、scope、是否懒加载),di 打出来的是装配关系。逐条敲,把本篇的结论一条条验出来:
lab xmlbean typo 与 beans 连着敲最有收获——前者把一份写坏的定义放进注册表,后者让你看清「注册成功」和「能用」是两个时刻。第十一节沙盘里那档 sigletion 拼写错误,就是这个组合的字符串版。
新手对 BeanDefinition 最没感觉的地方在于:你在类上写的注解,其实只是往这张表的某个格子里填了个值。填对了省内存;填错了容器不会当场拦你——它只照着格子里的字符串办事,要等到第一次取用才炸。下面这个沙盘把「作用域 + 是否懒加载」合成一个开关,切一档立刻看后果:
getScope() = ""(没显式设置时的默认值),isSingleton() = true启动阶段:preInstantiateSingletons 创建 1 个实例连续 3 次 getBean(userService) → == 比较:true true true内存中存活实例数:1单例池 singletonObjects: {userService=UserService@1a2b}
这个沙盘最值得记的是最后一档。「填错 scope」这件事在注册表层面完全合法——那只是一个字符串,没人校验它。所以看到 No Scope registered for scope name 'xxx',别去查 Bean 的代码,直接 getBeanDefinition(name).getScope() 把定义里的原值打出来,九成是个拼写错误。
热身题,考第二节的字段表和上面的沙盘:
重头戏,把第六节与决策卡串起来:
容器管理的从来不是对象,而是"对象的定义"。记住三件事——BeanDefinition 是蓝图,beanClass / scope / lazyInit / dependsOn 等字段描述它;定义有 XML、注解扫描、@Bean 三种来源,最终汇入同一个注册表;定义在实例化前可被父子合并与 BeanFactoryPostProcessor 改写,而这正是自动配置与动态注册的立足点。
这一列「报错原文」都能整段复制去搜索。它们有个共同点:都在说「这张档案表出了状况」——要么没这张卡,要么两张卡撞了名字,要么卡上填错了格子。
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 |
|---|---|---|---|
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.UserService' available | 注册表里根本没有这份定义:类不在扫描范围内、忘了 @Service/@Component、或它是 @Bean 方法产出但那个配置类没被加载 | 先确认「定义存在不存在」而不是「对象能不能建」:打印 ctx.getBeanDefinitionNames() 全量名单搜一下;按类型找不到就试 getBean("userService") 按名取 | 本篇第三节 · #5 第一个 Spring 工程 |
NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.Notifier' available: expected single matching bean but found 2: smsNotifier,mailNotifier(实际抛的是其子类 NoUniqueBeanDefinitionException) | 同类型有多个候选,注入点上又没有 @Qualifier / @Primary 来消歧义 | 给注入点加 @Qualifier("smsNotifier"),或给首选实现加 @Primary;用 bd 实验的 multi 参数可以看清两个定义如何并存 | 本篇第二节字段表 primary · #6 IoC 与 DI |
org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'userService' defined in URL [file:beans.xml]: There is already [Generic bean: class [com.example.UserService]] bound. | 同一个 beanName 被注册了两次且定义不兼容。Boot 2.1 起 spring.main.allow-bean-definition-overriding 默认为 false,直接拦下 | 九成是 XML 与注解混用或重复 @ComponentScan。用 -Ddebug=true 看是谁注册了两次;确有需要再显式打开覆盖开关(不建议长期开) | 本篇第四节 · #19 条件装配 |
java.lang.IllegalStateException: No Scope registered for scope name 'sigletion' | 定义里的 scope 拼错了。这个字符串在写入时完全合法(容器不校验注解值),直到第一次取用该 Bean、要按名字找作用域时才失败 | 报错点离病因很远,别翻业务代码:直接 ctx.getBeanDefinition("userService").getScope() 把定义里的原值打出来,九成是少写或多写了字母 | 本篇第十节沙盘 · #9 Bean 生命周期 |
org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userController' for bean class [com.bee.web.UserController] conflicts with existing, non-compatible bean definition of same name and class [com.bee.api.UserController] | 两个包里同名类都被扫到了,默认 beanName 都是首字母小写的简单类名,于是撞车 | 给其中一个显式命名 @RestController("apiUserController"),或收窄 @ComponentScan 范围 | 本篇第七节 · #17 SpringApplication |
java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext | 你在 refresh() 之前取了 Bean,或者容器已经 close() 之后还在用;也可能是自定义容器只 register 了定义却忘了调 refresh() | 顺序必须是「登记 → refresh() → 取用」。手写 GenericApplicationContext 时最容易漏掉那一句 ctx.refresh() | 本篇第四节代码 · #8 refresh() 十二步 |
这六行里最值得新手反复练的是 BeanDefinitionOverrideException 与 ConflictingBeanDefinitionException 那两条——报错都很长,但真正信息量只有两处:冲突的名字和冲突的两个来源。读这种异常时用 Ctrl+F 找 defined in 和 There is already,两句话就能定位到具体文件。
表格第一行是这一篇最典型的「档案缺失」现场。别看结论,先在下面这段真堆栈里点出你认为是凶手的那一帧:
本地 mvn spring-boot:run 一切正常,一跑集成测试就报「没有这个类型的 Bean」,而这个类确实在项目里躺着。
目标:亲手往注册表里塞两份定义,一份单例一份原型,然后把「定义里的字段」和「对象的数量」这两件事同时验证出来。
第一步,pom.xml 只要一个依赖:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.8</version> </dependency></dependencies>第二步,一个极简业务类(src/main/java/com/example/demo/HelloServiceImpl.java):
package com.example.demo;public class HelloServiceImpl { private final String greeting; public HelloServiceImpl(String greeting) { this.greeting = greeting; System.out.println(" 构造 HelloServiceImpl@" + Integer.toHexString(hashCode())); } public String sayHello(String name) { return greeting + ", " + name; }}第三步,主程序(src/main/java/com/example/demo/DefinitionLab.java):
package com.example.demo;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.support.BeanDefinitionBuilder;import org.springframework.beans.factory.support.RootBeanDefinition;import org.springframework.context.support.GenericApplicationContext;public class DefinitionLab { public static void main(String[] args) { GenericApplicationContext ctx = new GenericApplicationContext(); // ① 手写一份「单例」定义:构造器参数 Hi RootBeanDefinition singletonBd = new RootBeanDefinition(HelloServiceImpl.class); singletonBd.getConstructorArgumentValues().addIndexedArgumentValue(0, "Hi"); ctx.registerBeanDefinition("hiSingleton", singletonBd); // ② 用建造者再来一份「原型」定义:构造器参数 Hello BeanDefinition prototypeBd = BeanDefinitionBuilder .genericBeanDefinition(HelloServiceImpl.class) .setScope(BeanDefinition.SCOPE_PROTOTYPE) // ← 唯一区别就在这一行 .addConstructorArgValue("Hello") .getBeanDefinition(); ctx.registerBeanDefinition("hiPrototype", prototypeBd); // ③ 必须先刷新:登记只是"上户口",refresh 才是"开始生产" ctx.refresh(); // ④ 读回定义本身,证明字段真的被存进去了 BeanDefinition s = ctx.getBeanDefinition("hiSingleton"); BeanDefinition p = ctx.getBeanDefinition("hiPrototype"); System.out.println("hiSingleton scope=[" + s.getScope() + "] isSingleton=" + s.isSingleton() + " isLazy=" + s.isLazyInit()); System.out.println("hiPrototype scope=[" + p.getScope() + "] isSingleton=" + p.isSingleton() + " isLazy=" + p.isLazyInit()); System.out.println("注册的定义总数 = " + ctx.getBeanDefinitionCount()); // ⑤ 再看对象身份:单例恒定,原型每次都换 Object a1 = ctx.getBean("hiSingleton", HelloServiceImpl.class); Object a2 = ctx.getBean("hiSingleton", HelloServiceImpl.class); System.out.println("singleton == ? " + (a1 == a2)); Object b1 = ctx.getBean("hiPrototype", HelloServiceImpl.class); Object b2 = ctx.getBean("hiPrototype", HelloServiceImpl.class); System.out.println("prototype == ? " + (b1 == b2)); System.out.println(((HelloServiceImpl) a1).sayHello("Manual")); ctx.close(); }}运行 main,预期输出(@xxxxxx 是哈希地址,你的机器会不同;singleton 只应打印一行构造日志,prototype 的两行地址必须互不相同):
构造 HelloServiceImpl@5f2108b5 ← 这一行夹在 refresh() 内部:单例已被预实例化hiSingleton scope=[] isSingleton=true isLazy=falsehiPrototype scope=[prototype] isSingleton=false isLazy=false注册的定义总数 = 2singleton == ? true ← 两次取用都命中缓存,没有新的构造日志 构造 HelloServiceImpl@71bbf57e 构造 HelloServiceImpl@7f13d6eprototype == ? false ← 每取一次多打一行构造日志 (删掉 setScope 那一行后重跑:两行构造日志消失,这里变成 true)Hi, Manual说明三点:① 第一行构造日志出现在 refresh() 内部而不是任何 getBean 之后,正好印证「登记只是上户口,refresh 才开始生产」;② scope=[] 是空串——默认单例并不会把字符串 "singleton" 写进定义,只有你显式设置过才有值,所以判断作用域要用 isSingleton() 而不是比字符串;③ 这个容器里定义总数就是 2,GenericApplicationContext 不会自动塞内建定义,这与 AnnotationConfigApplicationContext 一刷新就冒出十几个内建 BeanName 的情况不同。
验收清单:① 把 .setScope(SCOPE_PROTOTYPE) 那一行删掉再跑,观察 prototype == ? 变成 true——你就手动改了一次「定义」并立刻看到行为变化;② 说出 hiSingleton 是在哪一步被创建的(答案见第四节与上一篇的第 11 步);③ 故意把一个 beanName 注册两次且 class 不同,记下你看到的完整异常原文。
每条只改一处,先猜结论再跑:
- 把
singletonBd.setLazyInit(true)加上。你会观察到:hiSingleton的构造日志从refresh()期间挪到了第一次getBean那一刻——@Lazy/lazyInit只改时机,不改作用域,它仍是单例。 - 在
ctx.refresh()之后调用ctx.registerBeanDefinition("late", bd)。你会观察到:不再抛出「已激活」的IllegalStateException,但这个定义不会被预实例化,只能靠手动getBean取到——这就是第四节的坑为什么强调「要动态加 Bean 请用BeanDefinitionRegistryPostProcessor」。 - 再加一份
hiPrototype2,与hiPrototype同为HelloServiceImpl类型但 beanName 不同,然后调用ctx.getBean(HelloServiceImpl.class)。你会观察到:NoUniqueBeanDefinitionException: ... expected single matching bean but found 3,对应速查表第二行;给其中一份加singletonBd.setPrimary(true)后立刻恢复。 - 用
ctx.getBeanFactory().getBeanDefinition("hiSingleton")拿到定义,读它的getResourceDescription()。你会观察到:手写的定义这里是null,而 XML 或扫描来的定义会带文件名/类路径——排「谁注册了我」时最好用的一招。
提示:第 3 条做完再回去跑第八节的 bd 实验(multi 参数),框架视角和你手工复现的现象应当严丝合缝。
做一个「迷你注册表可视化工具」DefinitionDumper,让任何项目的定义一览可查——这也是你自己第一篇「框架级」工具。
需求:
- 提供一个静态方法
dump(ConfigurableApplicationContext ctx),遍历getBeanDefinitionNames(),逐条打印:beanName | scope | lazyInit | primary | beanClassName | 来源描述(resourceDescription) - 统计并打印摘要:定义总数、单例数 / 原型数 / 其他作用域数、被标记懒加载的数量、有父定义(
getParentName() != null)的数量 - 支持一个过滤参数:只列出类名包含给定关键字的定义(例如
dump(ctx, "Tx")) - 额外挑战:识别出「同名但来源不同」的可疑定义并单独告警(比较
resourceDescription),这就是 BeanDefinitionOverride 的前置排查
验收清单:① 在一个空 Spring Boot 工程里跑 dump(ctx),能打出上千条且包含自动配置类产生的定义;② 加 --filter=DataSource 后只剩个位数条目;③ 人为制造一次同名冲突,工具能在启动失败前给出告警;④ 全程没有反射之外的私有 API 依赖(用 ConfigurableListableBeanFactory 的公开方法即可)。
不看上文,用一句话说清 BeanDefinition 和 Bean 的区别,并说明容器在哪一刻才真正 new 出对象。
XML、注解扫描、@Bean 三种来源分别由哪个类读取?它们在流程上从哪一步开始合流?
想批量修改「有哪些 Bean」该用哪个接口、在 refresh() 第几步?想修改「某个对象长什么样」又该用什么?
scope 写成拼错的字符串为什么不报错?此时你应该用哪一行代码把定义里的原值打出来?
registerBeanDefinition 为什么必须在 refresh() 之前?如果一定要在运行期动态加 Bean,正确姿势是什么?
容器认卡不认菜——beanName 是抽屉,BeanDefinition 是卡;改卡找 BFPP(第 5 步),改菜找 BPP(出生以后),卡撞名字就报 Override。