说实话,Spring的IOC容器大家每天都在用,但问到底层Bean是怎么从配置变成可用的实例、容器启动时到底干了哪些事,不少人就卡壳了。我早期啃这块源码的时候也差点放弃——断点一打下去,调用链深得吓人,一个getBean能追出十几层方法。但摸清楚主干之后会发现,整套加载流程的设计脉络非常清晰,而且理清它对排查循环依赖、Bean生命周期顺序、AOP代理时机这些实际问题都特别有帮助。
这篇文章我准备按照自己当时的源码阅读路径来写,目标就是一句话:让你跟着走一遍从容器启动到Bean创建完成的完整加载流程,看懂每一步在做什么、为什么这么做。适合用过Spring但想深入源码、一直被Bean生命周期和循环依赖绕晕的开发者,也适合准备面试需要系统讲清IOC原理的学习者。
1. 加载流程全景:先建立宏观认知再动手读源码
很多同学读源码失败,不是因为看不懂某个方法,而是因为一上来就钻进了细节,结果丢失了全局感。我先带你把全景图铺开,后面每一步都能对号入座。
1.1 一次加载的起点和终点分别是什么
从使用者视角来看,一次完整的IOC加载通常是这样触发的:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class);第一行代码创建容器,第二行代码从容器拿Bean。但实际上,真正的Bean创建在第二行之前就已经发生了——AnnotationConfigApplicationContext的构造器里会自动执行容器的启动流程,把所有非懒加载的单例Bean全部创建好。第二行的getBean更多是从缓存里取值。
这就像开一家餐厅:你拿着菜单(配置类)进场,后厨(容器)会提前把常用菜(单例Bean)备好。客人点单(调用getBean)时,已经备好的菜直接端出来就行,而不是现场才开始洗菜切菜。
1.2 源码阅读的两条主干线和四个阶段
读IOC源码,我建议分成两条线来看。
第一条线是容器启动线,入口是AbstractApplicationContext.refresh()。这个方法被誉为Spring源码的"总指挥",12个步骤层层推进,完成整个容器的初始化。
第二条线是Bean创建线,入口是finishBeanFactoryInitialization(),往下走就是getBean → doGetBean → createBean → doCreateBean这条主链。
这两条线合在一起就是一个完整的加载周期,大致可以分成四个阶段:
- 配置加载阶段:把XML或注解配置类解析成一个个
BeanDefinition - 容器增强阶段:执行
BeanFactoryPostProcessor修改Bean定义,注册BeanPostProcessor - Bean创建阶段:实例化、属性填充、初始化,产出可用的Bean
- 容器完善阶段:完成事件广播、生命周期回调等收尾工作
1.3 三个必须先搞清楚的底层概念
读代码之前有三块基石必须打牢,否则后面每一步都是云里雾里。
第一个是BeanDefinition。它就是Bean的"图纸",里面记录了类名、作用域、是否懒加载、依赖关系、初始化方法等元信息。容器启动时先解析配置得到图纸,再根据图纸来施工创建Bean。注意它不是Bean本身,而是描述Bean的对象。
第二个是BeanFactory和ApplicationContext的区别。BeanFactory是最基础的容器接口,负责Bean的获取和管理;ApplicationContext在它之上增加了事件发布、国际化、环境抽象等能力。我们平时用的AnnotationConfigApplicationContext就是一个ApplicationContext实现,底层组合了一个DefaultListableBeanFactory。
第三个是后置处理器机制。Spring最核心的扩展点就是两类后置处理器:BeanFactoryPostProcessor操作BeanDefinition,BeanPostProcessor操作Bean实例。可以说,IOC容器的一切高级功能——占位符解析、注解注入、AOP代理——都是靠它们实现的。
可以把这几个概念放在一张对照表里方便记忆:
| 概念 | 操作对象 | 生命周期阶段 | 典型应用 |
|---|---|---|---|
| BeanDefinition | Bean的元数据定义 | 配置加载阶段 | 描述类的信息、依赖、作用域 |
| BeanFactoryPostProcessor | BeanDefinition | 容器启动阶段 | 修改Bean定义、解析占位符 |
| BeanPostProcessor | Bean实例 | Bean创建阶段 | 注解注入、AOP代理、初始化回调 |
2. 从refresh()开始:一步一步拆解容器启动骨架
refresh()是理解整个IOC的钥匙。这个方法在AbstractApplicationContext中定义,名字取得非常传神——容器的创建、增强、完善都在这一个方法里"刷新"完成。我把它的核心步骤拆给你看。
2.1 为什么refresh()是源码的总入口
AbstractApplicationContext.refresh()本身就是一个模板方法,它只定义骨架,部分步骤交给子类实现。这个设计保证了容器启动流程的稳定性——不管你是XML配置还是注解配置,启动顺序都一样,只有某些步骤的实现不同。
完整的方法体大致如下:
public void refresh() throws BeansException, IllegalStateException { // 1. 容器刷新前的准备,记录启动时间、设置容器状态 prepareRefresh(); // 2. 获取新的BeanFactory,并加载BeanDefinition(XML和注解路径不同) ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 3. 给BeanFactory补充Spring内置的组件,比如类加载器、表达式解析器 prepareBeanFactory(beanFactory); // 4. 子类可以在这里对BeanFactory做额外配置 postProcessBeanFactory(beanFactory); // 5. 执行BeanFactoryPostProcessor(重点:注解配置解析就在这) invokeBeanFactoryPostProcessors(beanFactory); // 6. 注册BeanPostProcessor(注意此时只是注册,还没执行) registerBeanPostProcessors(beanFactory); // 7. 初始化消息源组件(国际化) initMessageSource(); // 8. 初始化事件广播器 initApplicationEventMulticaster(); // 9. 模板方法,留给子类做特殊处理,Web容器在这里启动 onRefresh(); // 10. 注册事件监听器 registerListeners(); // 11. 实例化所有非懒加载的单例Bean(这条线最重要) finishBeanFactoryInitialization(beanFactory); // 12. 完成刷新,发布容器刷新完成事件 finishRefresh(); }我第一次看这段代码时,最大的感受是:Spring把"启动容器"这件事拆得特别细,每一步都有明确的职责边界。这也提醒我们,读大项目源码时先抓主方法的每一步做了什么,比逐行深挖某个方法重要得多。
2.2 注解配置类的BeanDefinition加载路径
拿AnnotationConfigApplicationContext来说,它在构造器中就注册了AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner两个组件。但真正把AppConfig配置类解析成BeanDefinition的动作,并不是在构造器中完成的,而是延迟到了refresh()的第五步。
这里有个容易困惑的点:AppConfig类本身也会被注册为一个BeanDefinition,然后由内置的ConfigurationClassPostProcessor去解析。它实现了BeanDefinitionRegistryPostProcessor接口,优先级最高,会先于其他后置处理器执行。它的核心工作包括:
- 解析
@Configuration类上的@ComponentScan注解,扫描指定包下的类 - 解析
@Import引入的类和@Bean方法 - 将扫描到的每一个类封装成
ScannedGenericBeanDefinition并注册到容器
所以你会看到beanDefinitionMap里既有你自己写的业务类,也有很多Spring内部类。可以打开调试器,在registerBeanDefinition方法上打断点,你会看到注册流程是怎么一条一条把BeanDefinition放入DefaultListableBeanFactory的beanDefinitionMap中的。
2.3 BeanFactoryPostProcessor的执行时机和典型应用
invokeBeanFactoryPostProcessors这一步专门用来执行所有BeanFactoryPostProcessor。它先处理BeanDefinitionRegistryPostProcessor(它属于子接口,比普通的后置处理器更早执行),再执行普通的后置处理器。
为什么设计两个层次?因为BeanDefinitionRegistryPostProcessor可以继续注册新的BeanDefinition,Spring需要先让它们有机会注册完,再让其他后置处理器统一处理所有BeanDefinition。
这一步最常见的实际应用就是PropertySourcesPlaceholderConfigurer,它负责把@Value("${db.username}")中的占位符解析成配置项的值,而它的实现机制就是在BeanDefinition层面替换掉占位符字符串。所以如果你遇到@Value注入为null,优先查看这一步是否有执行。
2.4 BeanPostProcessor的注册流程
第六步registerBeanPostProcessors只负责"注册",不负责"执行"。Spring会从beanDefinitionMap中找到所有实现BeanPostProcessor接口的BeanDefinition,然后按优先级排序注册到beanFactory的beanPostProcessors列表里。
排序规则是先处理PriorityOrdered,再是Ordered,最后是无顺序的。这一步注册的处理器,包括我们熟知的AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor等,会在后面Bean创建的各个阶段被依次调用。
有个细节要特别注意:这些BeanPostProcessor自身也是Bean,它们不是等到第11步统一创建,而是在这一步就被提前实例化了。原因很简单——如果不提前创建它们,后面创建普通Bean时就没有处理器可用了。所以Spring在代码中专门做了判断,beanPostProcessors里的Bean不会在后续的preInstantiateSingletons中重复创建。
3. Bean实例化全链路:从getBean到initializeBean
容器骨架搭好后,就到了最核心的部分——Bean的创建。这一步的方法是finishBeanFactoryInitialization,它会遍历所有非懒加载的单例Bean名称,逐个调用getBean来触发创建。这里才是真正考验你阅读耐心的地方。
3.1 preInstantiateSingletons如何决定Bean创建顺序
preInstantiateSingletons是第11步的核心方法。它先拿到所有BeanDefinition的名称列表,然后过滤出非抽象、非懒加载、单例的Bean,依次调用getBean。
这里有一个重要的实现细节:它不是先创建一个Bean再取下一个,而是先把所有单例Bean用getBean触发一遍(第一轮循环专注于创建),所有Bean创建完成之后再进行第二轮循环,调用SmartInitializingSingleton接口的回调。这个设计的目的是保证所有单例Bean都已经创建完毕,扩展接口可以安全地依赖任何Bean。
Bean的创建顺序有两个因素影响:一是beanDefinitionNames列表的顺序,它由BeanDefinition的注册顺序决定;二是@DependsOn注解显式声明的依赖关系,Spring会优先创建被依赖的Bean。如果你发现某个Bean创建顺序不符合直觉,多半就是这两个原因之一。
3.2 doGetBean的三级缓存查询与单例创建
getBean内部调用doGetBean,这个方法最核心的逻辑就在开头:
protected <T> T doGetBean(...) { // 第一关:尝试从缓存获取,这就是一级、二级、三级缓存查询的地方 Object sharedInstance = getSingleton(beanName); if (sharedInstance != null && args == null) { bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { // 缓存没命中,进入真正的创建逻辑 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } // 查找父容器... // 进入创建单例的模板方法 if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { return createBean(beanName, mbd, args); }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } } }getSingleton(beanName)这个方法里实现了三级缓存的读取逻辑,我先按下不表,放到第4节专门讲。
单例Bean的创建是通过getSingleton(String, ObjectFactory)方法完成的。这个方法在被调用时会先把当前Bean标记为"正在创建中",然后调用ObjectFactory.getObject()实际执行createBean,创建完成后把成品放入一级缓存,最后清除"正在创建"标记。这里用到了模板方法模式,createBean的具体实现在AbstractAutowireCapableBeanFactory中。
3.3 createBeanInstance:三种实例化方式与循环依赖标记时机
AbstractAutowireCapableBeanFactory.createBean是真正负责"造Bean"的方法。它前面有一小段扩展逻辑:先调用resolveBeforeInstantiation,让InstantiationAwareBeanPostProcessor有机会在实例化前直接返回一个代理对象。大多数场景下这个钩子不会生效,但AOP相关组件会利用这个机制在某些特定场景下提前代理。
之后进入doCreateBean,这才是主流程。createBeanInstance负责实例化,它里面有三种方式:
- 构造器自动装配:根据构造参数自动解析依赖,适合有参构造
- 工厂方法实例化:调用
@Bean方法或自定义工厂的静态方法创建对象 - 无参构造简单实例化:最普通的方式,直接调用默认构造器
对于注解配置的@Bean方法,实例化走的是SimpleInstantiationStrategy中的工厂方法逻辑。对于@Component类,如果没有构造参数就是无参构造。对于有参构造的,会进入ConstructorResolver去解析构造参数。
在实例化完成后,doCreateBean还会做一件事:判断当前Bean是否可能被循环依赖引用,如果满足"单例 && 允许循环引用 && 正在创建中"三个条件,就会提前把一个ObjectFactory放入三级缓存。注意一级缓存放的是singletonObjects,三级缓存放的是singletonFactories,两者放的内容完全不一样。这个准备动作的意义,我会在循环依赖那一节展开。
3.4 populateBean:属性填充的完整顺序
实例化只是创建了一个空壳对象,属性还是null,接下来进入populateBean。这个方法刚开始时会再次调用InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation,如果它返回false,就直接跳过属性填充阶段——这是给扩展点留下的"阻止自动注入"的开关。
正常情况继续往下走,核心逻辑是找到所有InstantiationAwareBeanPostProcessor并执行它们的postProcessProperties。这里最关键的实现是AutowiredAnnotationBeanPostProcessor,它会解析Bean内的@Autowired、@Value注解,并执行注入。
注入完成后才轮到XML配置里<property>标签或代码中手动设置的属性值,通过applyPropertyValues方法复制进去。所以你在使用Spring时要注意:@Autowired声明的注入优先级高于XML手动配置的property,如果两种方式配置了同一个字段,@Autowired注入的结果会被property覆盖。
3.5 initializeBean:初始化阶段那些容易被忽略的回调顺序
属性填充完毕,Bean还不是最终状态,需要执行初始化。initializeBean的完整执行顺序非常讲究,我按源码顺序列一下:
protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) { // 第一步:Aware接口回调 invokeAwareMethods(beanName, bean); // 第二步:BeanPostProcessor前置处理 Object wrappedBean = bean; wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); // 第三步:执行初始化方法 invokeInitMethods(beanName, wrappedBean, mbd); // 第四步:BeanPostProcessor后置处理(AOP代理的主要时机) wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); return wrappedBean; }invokeAwareMethods只处理三个Aware接口:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware。这里有很多人会困惑,ApplicationContextAware去哪了?其实它不在这个方法里,而是通过ApplicationContextAwareProcessor这个BeanPostProcessor在第二步的回调中执行的。Spring这样做的原因是,ApplicationContextAware需要访问ApplicationContext的完整能力,而这个能力容器启动后期才具备,所以放到后置处理器中执行更稳妥。
第三步invokeInitMethods也有顺序讲究:先判断Bean是否实现InitializingBean接口,是则调用afterPropertiesSet();随后再调用自定义的initMethod。这里有一个隐藏优先级:接口回调总是先于@Bean(initMethod = "...")指定的方法执行。如果你同时配了两种初始化方式,要先想清楚谁先谁后。
第四步的applyBeanPostProcessorsAfterInitialization是AOP代理的关键位置。AbstractAutoProxyCreator正是在这个方法中检查当前Bean是否有匹配的切面,如果匹配则生成代理对象返回。所以你现在应该能理解,为什么AOP代理发生在属性注入之后、Bean真正对外使用之前。
3.6 注册销毁方法与Bean生命周期对照
初始化完成后,registerDisposableBeanIfNecessary会检查Bean是否实现了DisposableBean接口或者配置了destroyMethod,如果满足条件,就注册到disposableBeans中,等待容器关闭时统一回调。
这个阶段完成后,单例Bean就被放入一级缓存singletonObjects,getBean就能直接拿到了。为了更直观,我把Bean从生到死的全过程整理成一个对照表:
| 阶段 | 关键扩展点 | 典型回调 |
|---|---|---|
| 实例化前 | InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation | 提前创建代理对象 |
| 实例化 | createBeanInstance | 构造器/工厂方法/无参构造 |
| 合并BeanDefinition | MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition | 预解析@Autowired注入点 |
| 属性填充 | InstantiationAwareBeanPostProcessor.postProcessProperties | @Autowired、@Value注入 |
| 初始化前 | BeanPostProcessor.postProcessBeforeInitialization | ApplicationContextAware回调 |
| 初始化 | InitializingBean.afterPropertiesSet / init-method | 自定义初始化逻辑 |
| 初始化后 | BeanPostProcessor.postProcessAfterInitialization | AOP代理创建 |
| 销毁 | DisposableBean.destroy / destroy-method | 资源清理、连接关闭 |
4. 循环依赖与三级缓存:整个加载流程中最硬核的设计
聊IOC加载流程绕不开循环依赖。什么是循环依赖?A依赖B,B又依赖A。如果Spring不做任何处理,创建A时需要B,创建B时需要A,双方互相等待,必然死锁。Spring用三级缓存巧妙化解了这个问题。
4.1 三级缓存各自存放什么
DefaultSingletonBeanRegistry中有三个成员变量:
// 一级缓存:存放成品Bean private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存:存放早期暴露的半成品Bean private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 三级缓存:存放ObjectFactory工厂 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);一级缓存大家最熟悉,getBean通常直接命中这里。二级缓存用于存放从三级缓存中提前取出的半成品Bean,它解决的是"同一个半成品只能被取一次"的问题。三级缓存存放的是ObjectFactory,这个工厂被调用时执行getEarlyBeanReference方法,让后置处理器有机会对早期引用进行包装(比如提前创建AOP代理)。
4.2 源码中循环依赖是如何一步步破解的
我拿A依赖B、B依赖A的场景走一遍完整流程:
创建A时,A的createBean执行完毕,实例化出A对象,然后A把getEarlyBeanReference包装为ObjectFactory放入三级缓存。接着进入populateBean填充属性,此时发现需要注入B,于是调用getBean(B)。
创建B时,和A一样,实例化后放入三级缓存,填充属性时发现需要A,再次调用getBean(A)。这次进入doGetBean时,先执行getSingleton("A"),查一级缓存没有,查二级缓存没有,查三级缓存——找到了!于是调用工厂方法得到A的早期引用,放入二级缓存,删除三级缓存条目。
B顺利拿到A的早期引用对象,完成属性填充和初始化,最终作为成品放入一级缓存。回到A的创建现场,它正在等着B,现在B已经创建完成,A获取到B的引用,继续完成自己的属性填充和初始化,最后也存入一级缓存。
整个过程的关键在于:三级缓存在A还没完成初始化时就能拿到A的引用(哪怕属性还没填),只要B消费的是同一个引用对象,最终就能保持一致。
4.3 为什么三级缓存必须是三级而不是两级
这是面试高频问题。你可以从代码中发现,二级缓存里存的是已经从三级工厂里取出的早期Bean,原因在于如果只有二级缓存,那么所有Bean在实例化后都要立刻执行AOP代理,即使它并未参与循环依赖,也会白白经历代理创建。
Spring这里采用懒惰策略:只有当真正发生循环引用、需要提前暴露对象时,才调用三级缓存的getEarlyBeanReference让后置处理器生成代理。没有循环依赖的Bean则等到最终阶段再走AOP代理。
如果去掉二级缓存直接使用三级,每次获取都要重新调用工厂方法,可能产生多个代理实例。二级缓存把早期引用缓存住,保证同一个Bean多次获取得到的是同一个对象。
4.4 构造器循环依赖为什么无法解决
三级缓存只能解决属性注入(setter/字段注入)的循环依赖,对构造器循环依赖无能为力。原因在于:构造器循环中,A实例化时因为构造参数需要B,B实例化时又因为构造参数需要A,可A在构造完成之前根本没有机会把三级缓存放进去。三级缓存里没有A,B就无法拿到A的引用。
用生活化的场景类比:你和室友互相等对方先出门拿钥匙,结果两人都堵在门口,谁也没法先挪一步。Spring对这个场景的处理就是抛异常,提示你调整设计。见过太多人在遇到BeanCurrentlyInCreationException时一头雾水,其实排查思路很简单:先看异常信息里提到的Bean,分析它是在构造器阶段还是属性注入阶段发生的循环引用,如果是前者,除了重构依赖关系没有别的路。
5. 源码阅读实操方法与问题排查
读完前面的流程拆解,你还需要一套能落地的调试方法和排查思路。我把自己实际调试源码时积累的经验和踩过的坑分享出来。
5.1 高效调试源码的断点配置清单
强烈建议用IDE的条件断点和表达式计算功能来辅助理解调用链。以下几个断点位置对于复现一次完整加载流程特别有价值:
| 断点位置 | 观察目标 | 调试价值 |
|---|---|---|
| refresh() 方法开头 | 容器启动整体步骤 | 观察12个步骤的顺序 |
| registerBeanDefinition | beanDefinitionMap的变化 | 观察每个类的注册情况 |
| invokeBeanFactoryPostProcessors | 后置处理器的执行顺序 | 看注解配置解析过程 |
| doCreateBean 方法内 | 当前创建的Bean名称 | 配合条件断点只看某个Bean |
| populateBean 方法内 | 属性填充的对象 | 观察依赖注入过程 |
| getSingleton 方法内 | 三级缓存的存取时机 | 剖析循环依赖核心逻辑 |
调试技巧方面,我有一个很实用的习惯:判断某个Bean到底在执行哪一步时,可以不用从头跑到尾,而是在doCreateBean上打条件断点,条件的写法类似beanName.equals("userService")。这样每次创建userService这个Bean时IDEA会精准停下,然后使用"Step Over"逐行走完整个方法,你就能看到这个Bean从实例化到初始化的全过程。
还有一个小技巧是善用IDE的"Evaluate Expression"功能。断点在populateBean停下后,你可以直接执行表达式查看当前beanFactory的beanDefinitionMap中有哪些BeanDefinition,或者查看getSingleton(beanName, true)的返回值,省去逐层翻找堆栈的时间。
5.2 高频问题排查思路
我给自己带过的开发者总结了几个最高频的排查方向,写成速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| @Value注入为null | 占位符未被解析 | 检查PropertySourcesPlaceholderConfigurer是否被注册且位于正确的priority |
| BeanPostProcessor未生效 | 注册时机太晚 | 确认它本身被Spring管理,且不是懒加载 |
| BeanCurrentlyInCreationException | 构造器循环依赖 | 查看异常中Bean名称,分析构造器依赖链 |
| AOP代理未生效 | 代理时机不对 | 确认方法调用是从容器获取的Bean,而非内部this调用 |
| Bean初始化方法没执行 | init-method拼写错误 | 检查BeanDefinition中的initMethodName与方案名是否一致 |
有一个真实案例我记得很清楚。某同学在项目里定义了一个BeanPostProcessor,想在Bean初始化前修改某个字段,但调试时发现回调从未执行。后来发现这个后置处理器是在@Configuration类里通过@Bean方式注册的,而配置类本身在扫描阶段才被处理,注册后置处理器时它还不在beanDefinitionMap中,导致Spring根本没发现它。把他改成@Component放在扫描路径下,或者手动register到容器,问题才解决。
5.3 一条简单规则记住完整加载流程
最后分享一个帮助记忆的口诀:配置变图纸,图纸变定义,定义变实例,实例变代理。
配置类里的每一个类最终被解析成BeanDefinition,这一步对应"配置变图纸";BeanDefinitionRegistry把图纸注册到容器,对应"图纸变定义";finishBeanFactoryInitialization阶段按图纸创建实例并完成属性填充,对应"定义变实例";最后通过BeanPostProcessor完成AOP等增强,对应"实例变代理"。
只要你能把这条串联讲清楚,再往里填细节就水到渠成了。
我个人在实际阅读源码时最大的体会是:千万不要试图一次读透所有分支,盯住主链路就够了。先跑通一个最简单的配置,把refresh方法完整跑一遍,把每个阶段打印出来,再逐步深入二级细节。这样下来,你不仅能画出从AnnotationConfigApplicationContext到DefaultListableBeanFactory的调用路径,还能真正理解为什么Spring能把"创建对象"这一件事做得如此优雅。
最后再分享一个建议:源码学习一定要搭配调试实战。找个周末,打开IDE,建一个最简单的@Configuration加一个@Component,然后跟着refresh()的12个步骤走一遍,亲眼看一次beanDefinitionMap从空到满、singletonObjects从无到有的过程。这种直观感受,比读任何文章都更值得。