平时我给团队做 Spring 源码相关的分享时,问得最多的问题是:“你说 Spring 启动复杂,到底复杂在哪?”我一般会甩出AbstractApplicationContext.refresh()那几十行代码。这一行refresh()就像容器的电源键,按下去之后,BeanFactory 从“一个能装东西的盒子”变成“一个能生产、管理、关联所有 Bean 的完整容器”。今天这篇就把这行refresh()从里到外拆一遍:不只背出 12 个步骤的名字,而是把每个方法为什么出现在这个位置、执行后留下什么状态、以及它和你日常遇到的现象是怎么对应上的,全部讲透。适合刚准备啃 Spring 源码的同学,也适合写了好几年 Spring Boot 但始终觉得容器是个黑盒的开发。
如果你用 1.5 倍速回放 Spring 容器的整个启动过程,最后留在印象里的那个反复出现的方法名一定是refresh()。这一行调用撑起了整个 IoC 容器的启动全链路,所以我打算从它开始,一层层往下剥。
1. 为什么是 refresh():容器启动的“总开关”到底管了哪些事
1.1 从三行启动代码看 refresh() 的位置
很多同学第一次接触 Spring 的程序大概长这样:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService service = context.getBean(UserService.class); service.doSomething(); context.close();第二行getBean()是你业务上手的地方,但真正让容器“活过来”的是第一行的构造方法。点进AnnotationConfigApplicationContext(Class<?>... componentClasses),你会看到:
public AnnotationConfigApplicationContext(Class<?>... componentClasses) { this(); register(componentClasses); refresh(); }构造器里依次做了三件事:this()初始化了AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner,并注册一批内置处理器定义;register(componentClasses)把传入的配置类注册成 BeanDefinition;最后refresh()执行完整的容器启动流程。
这里最容易被忽略的事实是:在调用 refresh() 之前,BeanFactory 已经创建好了。AnnotationConfigApplicationContext的父类GenericApplicationContext在构造时就已经 new 了一个DefaultListableBeanFactory。所以refresh()不是“从零创建容器”,而是“把已经存在的 BeanFactory 初始化成一个可用的完整容器”。这个认知对理解整个链路非常重要,因为它决定了后面obtainFreshBeanFactory()的行为。
1.2 一份 12 步的“开机自检清单”
refresh()的整个方法体是一个由synchronized (this.startupShutdownMonitor)保护的同步块。我习惯把这十几步概括成一份“开机自检清单”:
| 步骤 | 方法 | 核心职责 | 你会感知到的产物 |
|---|---|---|---|
| 1 | prepareRefresh() | 启动状态、属性校验 | active=true、earlyApplicationEvents |
| 2 | obtainFreshBeanFactory() | 获取/刷新 BeanFactory | BeanFactory 就绪 |
| 3 | prepareBeanFactory() | 装配默认组件 | Aware 处理器、特殊依赖解析 |
| 4 | postProcessBeanFactory() | 子类扩展点 | Web 容器等定制 |
| 5 | invokeBeanFactoryPostProcessors() | 执行 BFPP | BeanDefinition 被修改 |
| 6 | registerBeanPostProcessors() | 注册 BPP | 后置处理链 |
| 7 | initMessageSource() | 初始化消息源 | messageSource Bean |
| 8 | initApplicationEventMulticaster() | 初始化事件多播器 | applicationEventMulticaster Bean |
| 9 | onRefresh() | 子类特殊初始化 | Spring Boot 的内嵌服务器 |
| 10 | registerListeners() | 注册监听器 | 监听器绑定到多播器 |
| 11 | finishBeanFactoryInitialization() | 实例化非懒加载单例 | singletonObjects 被填充 |
| 12 | finishRefresh() | 收尾、发布事件 | ContextRefreshedEvent |
这张表建议存下来。之后无论你是看源码还是排查启动异常,都可以先看栈顶落在哪一步,再回头看这张表,基本能判断问题属于哪个阶段。整个流程可以粗分为几段:前 3 步是“铺地基”,第 4~6 步是“激活扩展点”,第 7~10 步是“后勤就位”,第 11~12 步是“批量实例化与开业”。我接下来按这个顺序一段一段拆。
2. 准备阶段:prepareRefresh() 做了三件容易被忽略的事
很多人读refresh()时都会跳过prepareRefresh(),因为它的名字太朴素,好像只是“准备刷新一下”。但它对容器状态的管理非常关键,而且直接关系到后面的事件机制。
2.1 启动时间、状态标志与关闭钩子
先看prepareRefresh()开头的几行关键代码:
protected void prepareRefresh() { this.startupDate = System.currentTimeMillis(); this.active.set(true); this.closed.set(false); initPropertySources(); getEnvironment().validateRequiredProperties(); if (this.earlyApplicationListeners == null) { this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners); } else { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } this.earlyApplicationEvents = new LinkedHashSet<>(); }startupDate记录启动时间,active和closed是两个AtomicBoolean,标志容器的存活状态。注意这里用的是原子布尔量而不是普通布尔量,因为refresh()虽然是同步的,但close()、getBean()等操作可能来自不同线程,容器的活跃状态必须对多线程可见。在真实故障排查中,我见过有人因为自定义线程里反复关闭和重刷容器,导致isActive()判断出现“灵异现象”,追到最后就是状态标记没有被正确维护。
这里还有一个容易被忽略的模板方法:initPropertySources()。它留给子类往环境属性源里补充内容,比如 Web 环境中把 Servlet 相关属性塞进 Environment,就发生在这里。如果你在自定义容器里需要往 Environment 里提前加入默认属性,重写这个方法比直接改环境对象更优雅,因为它的调用时机就在 refresh 的最开头,能保证后续所有步骤都拿到这份属性。紧接着的validateRequiredProperties()会校验环境里标记为 required 的属性是否都配置了。很多人启动时遇到 “Property ... is required” 的异常,第一反应去翻业务代码,其实这个异常往往在容器刚启动、还没加载任何 Bean 之前就抛出来了,顺着这个方法往下查很容易定位。
2.2 earlyApplicationListeners 和 earlyApplicationEvents 是什么
这段代码里真正值得细看的是earlyApplicationListeners和earlyApplicationEvents这两个集合。
applicationListeners是容器在 prepareRefresh 之前就已经注册的“静态监听器”,比如你通过context.addApplicationListener(...)手动加进去的。Spring 用earlyApplicationListeners把它备份起来,是为了避免每次 refresh 时把之前手动注册的监听器弄丢。如果你在某次热刷新后发现以前手动注册的监听器突然不生效了,大概率就是没处理好这个备份集合。
earlyApplicationEvents则是一个LinkedHashSet,用来暂存“过早发生”的事件。这里有个关键矛盾:事件多播器要到第 8 步initApplicationEventMulticaster()才会初始化完成,但容器启动过程中,一些 BeanFactoryPostProcessor 在实例化时可能已经发布事件了。为此 Spring 先开一个临时集合,把这些事件存起来,等registerListeners()阶段多播器真正就绪了,再逐条补发。如果你在自研框架时遇到过“事件发了但监听器没收到”的怪问题,大概率就是没有处理这个 buffer。这两行代码虽然不长,但它体现了事件机制里“先收藏、后发布”的完整设计思路。
3. 建地基:两种 BeanFactory 创建路径与默认装备
3.1 GenericApplicationContext 的“轻刷新” vs AbstractRefreshableApplicationContext 的“重刷新”
接着看第二步,obtainFreshBeanFactory():
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { refreshBeanFactory(); return getBeanFactory(); }核心都在那个抽象方法refreshBeanFactory()里。这里有两种完全不同的实现路径,我建议用一张表记住:
| 容器类型 | 代表 | refreshBeanFactory 行为 | BeanDefinition 来源 |
|---|---|---|---|
| GenericApplicationContext | AnnotationConfigApplicationContext | 不重建工厂,只更新状态 | register() / scan() 提前注册 |
| AbstractRefreshableApplicationContext | ClassPathXmlApplicationContext | 销毁旧工厂,重建新工厂并加载定义 | XML 文件 / Groovy 脚本 |
对于注解驱动的AnnotationConfigApplicationContext来说,GenericApplicationContext.refreshBeanFactory()的实现相当“轻”:
protected final void refreshBeanFactory() throws IllegalStateException { if (!this.refreshed.compareAndSet(false, true)) { throw new IllegalStateException("GenericApplicationContext does not support multiple refresh attempts"); } this.beanFactory.setSerializationId(getId()); }它只是把refreshed从 false 改成 true,并给 BeanFactory 设置一个序列化 ID。因为在这种路径里,DefaultListableBeanFactory早在构造阶段就创建完毕,BeanDefinition 也通过 reader 和 scanner 注册进去了,这一步只是“确认身份”。
但如果你用的是 XML 驱动的ClassPathXmlApplicationContext,父类AbstractRefreshableApplicationContext.refreshBeanFactory()就会动真格:先销毁并关闭旧工厂,然后新建DefaultListableBeanFactory,通过loadBeanDefinitions把 XML 里的<bean>定义加载进去。整个过程是真正意义上的“重建容器”。所以同一个refresh(),在不同 ApplicationContext 实现里“刷新”的力度完全不同。这也是为什么基于 XML 的容器可以重复 refresh,而GenericApplicationContext不支持多次刷新。
我实际排查过一个这样的问题:同事在一个注解驱动的容器里写了个定时任务调用context.refresh(),想实现配置“热加载”,结果直接抛IllegalStateException: GenericApplicationContext does not support multiple refresh attempts。如果你提前知道这条路径差异,这类问题基本一眼就能定位。
3.2 prepareBeanFactory:容器默认装备清单
BeanFactory 就绪之后,prepareBeanFactory()会给它装配一整套默认能力。这一步不创建业务 Bean,但它决定了后续每个 Bean 能感知多少容器能力。我挑几个关键装备解释。
第一个是ApplicationContextAwareProcessor:
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));这是一个内置的 BeanPostProcessor,专门处理实现了EnvironmentAware、EmbeddedValueResolverAware、ResourceLoaderAware、ApplicationEventPublisherAware、MessageSourceAware、ApplicationContextAware这些接口的 Bean,把容器自身塞给它们。紧接着的那一串ignoreDependencyInterface(...)就是为了保证这些 Aware 接口不会被@Autowired按普通依赖去解析——它们只能通过 setter 方式由 Aware 回调注入。这也是为什么你写@Autowired ApplicationContext会注入成功,但想@Autowired一个EnvironmentAware类型的字段就会失败:Aware 接口不是“值”,是“回调协议”。
真正让@Autowired ApplicationContext生效的,是接下来的registerResolvableDependency(...):
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); beanFactory.registerResolvableDependency(ResourceLoader.class, this); beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); beanFactory.registerResolvableDependency(ApplicationContext.class, this);它们在依赖解析器里登记了“可解析依赖”,告诉容器:如果谁声明注入ApplicationContext、ResourceLoader、ApplicationEventPublisher,就把容器本身作为值给它。这几个类是 Bean 创建链路里和容器交互最频繁的“特殊值”,提前登记好,实例化时就不用到处找来源。
最后还有ApplicationListenerDetector。它本身也是一个 BeanPostProcessor,负责在单例 Bean 创建完成时检查它是不是ApplicationListener,如果是就补充注册到事件多播器。这么做是为了覆盖那些“以普通 Bean 形式定义在配置里、没有用 addApplicationListener 注册”的监听器。它被放到 BPP 列表的最后,顺序很关键:只有所有 Bean 的后置处理都走完,这个“补漏”才有意义。
4. 扩展点编排:两类 Processor 的执行顺序为什么重要
refresh() 里第 4 到第 6 步,是整个启动流程中最“绕”的部分,也是 Spring 扩展性的大本营。
4.1 invokeBeanFactoryPostProcessors 的三轮处理
先明确一个前提:BeanFactoryPostProcessor是“针对 BeanDefinition 和 BeanFactory 本身的处理器”。它拿到的是还没实例化的 BeanDefinition,可以修改属性、替换实现类、注册新的定义、调整作用域等。因为它的工作对象是“定义”而不是“对象”,所以必须在任何普通 Bean 实例化之前执行。如果放到后面,修改定义就来不及了。
invokeBeanFactoryPostProcessors()的真正实现在PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()里,它反复调用getBeanNamesForType来捞取处理器,按优先级分批次执行。我把它简化成下面这个调用清单:
- 先把所有实现了
BeanDefinitionRegistryPostProcessor的处理器名字捞出来; - 第一次循环:只处理实现
PriorityOrdered的那些,调用它们的postProcessBeanDefinitionRegistry(registry); - 第二次循环:只处理实现
Ordered的; - 第三次循环:处理剩余没有顺序的;
- 每批处理完后,立即重新扫描
BeanDefinitionRegistryPostProcessor,防止前面的处理器注册了新的同类处理器; - 所有
BeanDefinitionRegistryPostProcessor处理完毕后,再捞所有BeanFactoryPostProcessor,仍然按 PriorityOrdered、Ordered、无顺序三批,调用postProcessBeanFactory(beanFactory)。
为什么要这么绕?因为处理器之间有依赖和顺序要求。比如框架需要先注册占位符处理器PropertySourcesPlaceholderConfigurer,再注册自定义后置处理器,如果没有优先级机制,顺序就只能靠 BeanDefinition 的注册顺序保证,非常脆弱。Spring 的做法是把“顺序”内建为排序规则,让实现方通过Ordered接口或@Order注解显式声明。实际开发里,一旦你发现“我的 BFPP 怎么没生效”,第一反应就该查它有没有被另一个 Ordered 更靠前的处理器抢先动了 BeanDefinition。
这里还有一个隐蔽点:invokeBeanFactoryPostProcessors()在获取这些处理器实例时已经调用了getBean(...)。也就是说,BFPP 自身也是 Bean,也会被实例化。但由于此刻registerBeanPostProcessors()还没执行,连一个用户自定义的 BeanPostProcessor 都没有注册,所以 BFPP 实例化时不会经过任何 BPP 的后置处理。如果你想给某个 BFPP 配一个 BPP 来增强它,是行不通的,因为 BPP 注册得太晚,需要这种组合时得把增强逻辑直接写在 BFPP 内部,或者换一种框架层面的设计。
4.2 registerBeanPostProcessors 注册的先后
BeanPostProcessor 和 BeanFactoryPostProcessor 虽然名字只差一个词,但职责完全不同:BFPP 改“定义”,BPP 拦“创建”。BPP 在 Bean 实例化后、初始化前后执行,比如postProcessBeforeInitialization、postProcessAfterInitialization。
registerBeanPostProcessors()同样由PostProcessorRegistrationDelegate实现,排序规则几乎一致:先 PriorityOrdered,再 Ordered,最后无序。比较特殊的是队列末尾会追加MergedBeanDefinitionPostProcessor以及ApplicationListenerDetector。前者是专门在 BeanDefinition 合并后处理元信息的内置处理器,AutowiredAnnotationBeanPostProcessor就属于这一类;后者就是上一章提到的监听器补漏器。
这里有个必须理解的细节:注册 BPP 不等于执行 BPP。registerBeanPostProcessors()只是把它们实例化出来放进后置处理器列表,真正对每个 Bean 调用它们,要等到finishBeanFactoryInitialization()批量实例化时才发生。所以你在自定义 BPP 里打日志,会发现“注册日志”比“执行日志”早一大截,这是正常现象。执行顺序同样值得注意:ApplicationContextAwareProcessor是prepareBeanFactory()阶段最早注册进去的,所以它的postProcessBeforeInitialization会最先执行。如果你注册了多个自定义 BPP,想控制相互之间的执行顺序,就给它们实现Ordered,数值小的先执行。这是 Spring 容器里非常基础又极容易踩错的一个约定。
4.3 postProcessBeanFactory:被低估的子类钩子
很多文章讲到第 4 步就一句话带过。但这个钩子恰恰是理解 Spring Boot 的关键。postProcessBeanFactory()在AbstractApplicationContext里默认空实现,留给子类往容器里加入特定领域的组件。比如在 Web 专用容器中,很多和 Servlet 容器相关的处理器就是在这里注册进去的。
你看各种框架源码时,如果看到有人重写这个方法向 BeanFactory 添加 BPP、Scope、PropertyEditor,那都是在“启动最早期”塞东西。把这些扩展放在这里而不是别处,是因为它早于所有 BFPP 的执行,可以保证后续任何处理器和 Bean 能立即看到这些容器级增强。所以postProcessBeanFactory虽然不张扬,但它提供了一个“先于一切”的扩展窗口,对那些需要在容器正式生产 Bean 之前就介入的场景极其关键。
5. 容器内部的“后勤”就位:消息源、事件多播器与监听器
BFPP 和 BPP 都就位后,容器开始处理“配套基础设施”。这阶段没有业务 Bean,但少了它,很多功能都会悄悄失效。
5.1 为什么 messageSource 必须叫这个名字?
initMessageSource()的逻辑看着简单,但对 i18n 的配置方式有决定性影响:
protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); } else { DelegatingMessageSource dms = new DelegatingMessageSource(); dms.setParentMessageSource(getInternalParentMessageSource()); this.messageSource = dms; beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource); } }注意MESSAGE_SOURCE_BEAN_NAME就是字符串"messageSource"。Spring 在容器里查有没有这个名字的 Bean,有就用,没有就注册一个DelegatingMessageSource兜底。你写 i18n 配置时,如果把 MessageSource Bean 命名成myMessageSource,这个步骤找不到,直接用默认空实现,getMessage()永远拿不到正确资源文件里的内容。这不是玄学,就是按名字查的。DelegatingMessageSource本身不会真正解析消息,它会向上委托给父级 MessageSource,在没有父级的情况下基本等于空实现,所以你没有自定义 messageSource 时,调用messageSource.getMessage()只会得到异常或 null。
5.2 事件多播器的缺省策略与同步执行
initApplicationEventMulticaster()的逻辑和消息源几乎一样:有叫applicationEventMulticaster的 Bean 就用,没有就 new 一个SimpleApplicationEventMulticaster(beanFactory)并注册成单例。这个默认多播器很关键:它在当前调用线程里直接遍历监听器并同步执行。也就是说,默认情况下 Spring 的事件模型是同步的——publishEvent()返回时,所有监听器都已经执行完了。
这个同步特性决定了两个实践规律:第一,监听器里千万不要写耗时操作,否则会阻塞事件发布线程,严重时拖垮整个请求链路;第二,如果你确实需要异步监听,可以自定义一个applicationEventMulticasterBean,配置一个带线程池的SimpleApplicationEventMulticaster,并设置TaskExecutor。Spring 没有默认配线程池,是因为容器宁可保持语义简单,也不愿引入意外的并发行为。
5.3 registerListeners 为什么要分三步
registerListeners()是事件机制最精妙的一步,它分三段:
protected void registerListeners() { // 1. 先把静态注册的监听器加进多播器 for (ApplicationListener<?> listener : getApplicationListeners()) { getApplicationEventMulticaster().addApplicationListener(listener); } // 2. 再找出所有实现了 ApplicationListener 的 BeanDefinition String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false); for (String listenerBeanName : listenerBeanNames) { getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName); } // 3. 最后发布提前暂存的事件 Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents; this.earlyApplicationEvents = null; if (!CollectionUtils.isEmpty(earlyEventsToProcess)) { for (ApplicationEvent earlyEvent : earlyEventsToProcess) { getApplicationEventMulticaster().multicastEvent(earlyEvent); } } }第一步处理的是通过 API 手动加入的监听器,也就是prepareRefresh()里备份过的applicationListeners,它们已经实例化好了,直接绑定到多播器即可。第二步是通过getBeanNamesForType(ApplicationListener.class, true, false)找出所有“以 Bean 形式定义”的监听器,但这里只是把 BeanName 注册进去,并没有调用getBean实例化。为什么不在这里实例化?因为finishBeanFactoryInitialization()马上就要到了,等到那一步再实例化,可以和其他单例一起走完整的 Bean 生命周期,包括 BPP 的增强。如果你在这里强行实例化,监听器反而会被剥夺一部分 Bean 能力。这是 Spring 对“时机”的精细控制。第三步把earlyApplicationEvents里暂存的事件一次性补发出去,到这里整个事件机制才算真正完整。
6. 重头戏:单例 Bean 的批量实例化全链路
前 10 步,容器已经万事俱备,现在进入真正的“生产环节”。
6.1 preInstantiateSingletons 的遍历与判断
finishBeanFactoryInitialization()在一组收尾检查后调用了preInstantiateSingletons(),这是DefaultListableBeanFactory的核心方法,它决定了哪些 Bean 会在启动时被创建。代码逻辑大致如下:
public void preInstantiateSingletons() throws BeansException { List<String> beanNames = new ArrayList<>(this.beanDefinitionNames); for (String beanName : beanNames) { RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { if (isFactoryBean(beanName)) { // FactoryBean 需要额外处理 } else { getBean(beanName); } } } for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { singletonInstance.afterSingletonsInstantiated(); } } }几个容易被忽略的设计:第一行先把beanDefinitionNames拷成新列表,因为实例化过程中,某些 Bean(特别是 FactoryBean)会触发新的 BeanDefinition 注册,如果不快照,直接在原集合上 forEach 就会抛ConcurrentModificationException。getMergedLocalBeanDefinition用于合并父子 BeanDefinition,@Configuration类、@Import、@Bean方法生成的 BeanDefinition 往往存在继承关系,这里拿到合并后的完整定义,判断abstract、singleton、lazyInit才有意义。
条件!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()意味着:抽象 Bean 不实例化、原型 Bean 不在这阶段创建、懒加载单例也会被跳过。所以“懒加载”并不是说迟一点创建,而是“等你第一次getBean时才创建”。如果你配置了@Lazy,却期待容器启动时它就已经在那里,那就搞错了语义。创建完所有单例后还有一个二次循环:凡是实现了SmartInitializingSingleton的 Bean,会收到afterSingletonsInstantiated()回调。这是“所有普通单例都已就绪”的信号,比监听ContextRefreshedEvent更可靠,因为它不会因为热刷新而重复触发。如果你想在容器启动完成后做一次性初始化,优先考虑这个接口,而不是事件监听。
6.2 从 getBean 到 doCreateBean:一次实例化的完整路线
getBean(beanName)是几乎所有创建入口的公共前缀。经过doGetBean里的一堆判断(单例缓存、父子容器、作用域等)后,真正创建对象发生在createBean和doCreateBean。这条链路过一遍大概就是:
- 判断单例缓存里有没有已经存在的实例,有就直接返回;
- 没有则创建 BeanWrapper,里面包含
Class.forName得到的对象实例; applyMergedBeanDefinitionPostProcessors:让AutowiredAnnotationBeanPostProcessor这类处理器扫描构造器、字段、方法上的注入点,缓存元数据;- 如果 Bean 在允许提前暴露的名单里(单例且允许循环依赖),就把一个
ObjectFactory放进三级缓存singletonFactories; populateBean(beanName, mbd, bw):属性填充,包括@Autowired、@Resource、@Value等依赖的解析和注入;initializeBean:依次处理invokeAwareMethods(BeanNameAware、BeanClassLoaderAware、BeanFactoryAware)、postProcessBeforeInitialization、afterPropertiesSet、自定义 init-method、postProcessAfterInitialization;- 注册单例到
singletonObjects,清理二级、三级缓存。
如果 Bean 还实现了DisposableBean或定义了 destroy-method,容器会把它们登记到disposableBeans,等close()时统一销毁。这一条链路也对应了你日常用到的绝大多数回调:BeanNameAware、@PostConstruct、InitializingBean、@PreDestroy,它们各自的调用位置就在上面 2 到 6 步之间。
6.3 三级缓存与循环依赖:为什么启动阶段不会“爆栈”
每次讲到实例化,循环依赖都是一个绕不开的大话题。Spring 处理单例之间循环依赖的三级缓存,本质上是为了解决一个顺序问题:A 依赖 B,B 又依赖 A,如果按普通依赖顺序创建,谁先创建都会在填充属性时差一个“还没出生的对象”,形成死循环。三级缓存的三个 Map 我通常用“成品区、半成品区、工厂区”来记:
singletonObjects:成品,存放已经完整初始化的单例;earlySingletonObjects:半成品,存放已经 new 出来但属性还没填充完成的实例引用;singletonFactories:工厂,存放ObjectFactory,它能在需要时“提前暴露”一个早期引用。
具体到 A、B 循环依赖的场景:创建 A 时,先把 A 的ObjectFactory放进三级缓存;A 填充属性发现需要 B,于是去创建 B;创建 B 时,它的属性填充需要 A,于是从三级缓存拿到 A 的工厂并调用getObject(),得到一个早期引用放进二级缓存,这个引用被注入给 B;B 创建完成,A 继续完成剩余的属性填充和初始化,最终替换成完整成品。整个过程就像两个人在黑暗的房间里互相递钥匙,先递出去的人虽然还没装修完,但至少证明“我在这里”。
源码阅读时要注意,@Autowired解决循环依赖靠的正是这个三级缓存,而@Async、@Transactional这类依赖 AOP 代理增强的 Bean,往往因为代理对象的创建时机在属性填充之后,会出现“循环依赖失败”的经典错误。这类问题我在实际项目里踩过不只一回:一个自引用调用的@AsyncBean,启动时偶尔报BeanCurrentlyInCreationException,追到最后就是代理与三级缓存的交互顺序问题。
7. 收尾与熔断:finishRefresh() 以及启动失败的“打扫战场”
7.1 LifecycleProcessor 与 ContextRefreshedEvent
finishRefresh()首先清理资源缓存、初始化 LifecycleProcessor,然后发布ContextRefreshedEvent。initLifecycleProcessor()和消息源、多播器的套路一样:有lifecycleProcessor这个 Bean 就用,没有则注册一个DefaultLifecycleProcessor。DefaultLifecycleProcessor.onRefresh()会扫描容器里所有Lifecycle接口的实现,按照SmartLifecycle的getPhase()排序后调用start()。这就是为什么有些组件在应用启动时会自动调用start()方法——它不一定是因为监听器,而是 Lifecycle 机制在finishRefresh()阶段统一驱动的。如果你在自定义组件里实现了SmartLifecycle,注意它的isAutoStartup()返回 false 时不会在启动阶段被自动 start,这也是一个经常被误解的点。
紧接着的publishEvent(new ContextRefreshedEvent(this))是容器启动完成的“官方通知”。很多人用它做“启动后执行任务”,但要小心:如果容器被重复刷新,这个事件会重复发布;如果只想在“启动完成后第一次初始化”时执行,用SmartInitializingSingleton更合适。
7.2 启动失败的 destroyBeans() 和 cancelRefresh()
refresh()的 catch 分支是我每次讲源码都会强调的部分。启动失败不是直接扔异常就完了,它还要把创建出来一半的对象全部销毁掉:
catch (BeansException ex) { destroyBeans(); cancelRefresh(ex); throw ex; }destroyBeans()调用beanFactory.destroySingletons(),把所有已经创建的单例 Bean 都按 DisposableBean 的规则销毁一遍,再把各种单例缓存清空。这个设计的意图很明显:容器启动失败了,就不能留下半初始化的资源,否则这种容器虽然不能正常工作,却可能持有着数据库连接、线程池等资源,造成泄漏或者诡异的调用问题。
cancelRefresh(ex)的默认实现就是把active置为 false。仔细想一下:如果在prepareRefresh()已经设置了 active 为 true,但后续失败时不移除这个状态,那isActive()就会误报容器存活,很多基于容器的关闭逻辑就会出错。所以失败时不仅抛异常,还同步维护了容器状态的正确性。
最后finally里还有一句resetCommonCaches(),它会清掉 Spring 内部缓存的反射元数据、注解解析结果等。这些缓存对单次启动没问题,但容器可能被反复创建,静态缓存如果不清理,内存增长会很可观。这个细节平时没人注意,但和“大应用反复刷新为什么内存上涨”这类问题直接相关。
8. 读完这条链路后,我想分享的几个实操经验
8.1 拿到启动异常,先对号入座
现在无论看到什么启动报错,我第一反应都是“它属于哪一步”。比如:报错是BeanDefinitionStoreException、BeanDefinitionOverrideException,多半发生在 BFPP 阶段(第 5 步);BeanCurrentlyInCreationException、UnsatisfiedDependencyException,多半在第 11 步实例化阶段;报错信息里出现ContextRefreshedEvent或LifecycleProcessor,则在收尾阶段。这样能帮我把排查范围瞬间缩小到几百行代码内,而不是漫无目的地翻日志。
8.2 自己也踩过的顺序坑
之前维护某个内部基础组件时,我在自定义 BPP 里依赖另一个 BFPP 处理后的 BeanDefinition,结果 BPP 一直拿不到预期数据。后来才发现,BFPP 的修改发生在第 5 步,而我的 BPP 注册顺序在第 6 步,数据链路本身没问题,问题出在 BPP 没有标注Ordered,注册得太晚。后来把自定义 BPP 改成实现PriorityOrdered,并在getOrder()中返回一个小值,问题立刻消失。顺序不是玄学,是源码里写死的回调优先级。
8.3 事件机制要比想象中更慎重
既然默认事件是同步的,自定义业务事件时就要小心监听器链。我见过有同事在ContextRefreshedEvent监听器里做了一个耗时的预热缓存操作,启动时整个应用卡了几十秒。后来改成监听器内部只提交异步任务才解决。如果你确实需要同步语义,至少要在监听器里捕获异常、限制耗时,避免一个监听器拖垮发布线程。
8.4 读源码的顺序建议
如果你刚准备啃 Spring 源码,我不建议一上来就盯着doCreateBean那种最深处的细节。先把refresh()的 12 步列表完整读两遍,理解每步的“输入—输出—状态变化”,再逐步深入。带着问题读,比按文件顺序读高效得多。比如“为什么我的懒加载没生效?”“为什么事件发出来没人收?”,这些问题都能在本文这条链路上找到对应节点。把这 12 步装进脑子里,Spring 在你眼里就不再是一个黑盒,而是一台每一步都能看清传动结构的机器。