Spring 的 ApplicationContext 启动,很多人背过八股文,也知道大概有十二步,但真到了排查问题的时候,看着异常栈里的AbstractApplicationContext.refresh()往往一脸懵。我一个做后端的老朋友前几天就被线上一个启动失败折腾到凌晨:日志里报BeanCurrentlyInCreationException,他第一反应是"不是有三级缓存吗,为什么循环依赖还会挂",结果查了半天才发现是自己多写了一个@Async注解。这种问题如果你不了解 refresh() 内部每个阶段到底在干什么、后置处理器是在哪一步生效的,基本只能靠猜。
这篇文章我就把ApplicationContext接口体系、refresh()方法的完整执行链路、三级缓存机制、Bean 生命周期以及后置处理器的依赖关系全部串起来讲清楚。我尽量用我实际排障和读源码的经验来讲,不会只贴源码,而是告诉你每个阶段背后的设计意图、常见坑、以及你可以怎么利用这些扩展点做自己的功能。适合这几类人看:准备 Spring 面试的 Java 开发者、系统学习 Spring 源码的初学者、以及遇到容器启动异常不知道怎么定位的工程人员。
1. 先把背景讲清楚:ApplicationContext 与 refresh() 的关系
1.1 ApplicationContext 不是"一个类",而是一整套容器能力
很多人把 ApplicationContext 理解成"Spring 的大工厂",这个说法没错,但太笼统了。我从接口定义的角度重新梳理一下。
ApplicationContext 是一个接口,它同时继承了ListableBeanFactory、HierarchicalBeanFactory、MessageSource、ApplicationEventPublisher、ResourcePatternResolver等多个接口。也就是说,一个 ApplicationContext 实现类同时具备以下能力:
- 作为 BeanFactory:管理 bean 的定义、创建、依赖注入和生命周期;
- 作为 MessageSource:提供国际化消息解析能力;
- 作为 ApplicationEventPublisher:发布事件,配合
ApplicationListener实现观察者模式; - 作为 ResourcePatternResolver:支持从类路径、文件系统等位置加载资源。
常见的实现类有ClassPathXmlApplicationContext、AnnotationConfigApplicationContext、GenericWebApplicationContext等。它们虽然配置来源不同(XML、注解、Web 环境),但启动核心逻辑都在同一个父类里,也就是AbstractApplicationContext。这一点很重要:不管你是用 Spring Boot 还是手写 XML 配置,最终都会走到AbstractApplicationContext.refresh()这个方法。
所以当你面试被问到"Spring 容器启动流程"时,不要上来就背 Spring Boot 的启动过程,真正的底层核心就是 refresh()。Spring Boot 只是在 refresh() 之前做了一堆自动装配的准备工作,并在onRefresh()阶段额外启动了内嵌 Web 容器。
1.2 refresh() 为什么是容器启动的"总开关"
看名字就知道,refresh()是"刷新"的意思。官方的设计意图是:这个方法可以多次调用,每次会把当前的容器状态清掉,基于已有的配置重新加载。但因为大多数应用在启动时只调用一次,所以实际工作中它就是"容器启动的入口"。
我画一条最简单的调用链:
AnnotationConfigApplicationContext 构造函数 -> refresh() -> prepareRefresh() // 容器状态准备 -> obtainFreshBeanFactory() // 加载或刷新 BeanDefinition -> prepareBeanFactory() // 配置容器标准特性 -> postProcessBeanFactory() // 子类扩展点 -> invokeBeanFactoryPostProcessors() // 执行 BeanFactoryPostProcessor -> registerBeanPostProcessors() // 注册 BeanPostProcessor -> initMessageSource() // 初始化国际化资源 -> initApplicationEventMulticaster() // 初始化事件多播器 -> onRefresh() // 模板方法,子类扩展,如 Spring Boot 启动 Web 容器 -> registerListeners() // 注册监听器 -> finishBeanFactoryInitialization() // 预实例化所有非懒加载单例 bean -> finishRefresh() // 发布 ContextRefreshedEvent我用开餐厅来类比这套流程:prepareRefresh 是开门前检查水电;obtainFreshBeanFactory 是准备菜单和食材清单;invokeBeanFactoryPostProcessors 是修改菜单,比如根据新店员的习惯调整菜品;registerBeanPostProcessors 是培训服务员,让他们知道上菜前后要做什么;finishBeanFactoryInitialization 是真正把每道菜做出来备着;finishRefresh 是挂出"营业中"的牌子,通知客人可以来了。
这样类比你再看整条流程就清楚多了。下面我逐个阶段深入拆解。
2. 逐层拆解 refresh() 的十二步启动流程
2.1 准备与装载:prepareRefresh() 和 obtainFreshBeanFactory()
prepareRefresh()干的事比较基础但很关键:
- 设置容器的启动时间
startupDate、活跃状态active、关闭状态closed; - 初始化
PropertySources,也就是把环境变量、系统属性等配置源准备好; - 校验必填的
requiredProperties,比如当前环境下是否缺少关键配置项; - 初始化 earlyApplicationEvents,用来暂存那些在监听器注册之前就发布的事件。
我实际排查过一个案例:某服务启动时报MissingRequiredPropertiesException,就是因为有人把配置中心的某个 key 写成了必填项,但没在配置文件中提供。这个错误就是在prepareRefresh()阶段抛出来的,而不是在创建 bean 阶段。看异常栈的时候如果发现是在 refresh 早期挂的,优先检查Environment相关的配置。
obtainFreshBeanFactory()是把配置文件/配置类解析成 BeanDefinition 的核心方法。这里要区分一下:ClassPathXmlApplicationContext在这个阶段会调用loadBeanDefinitions()解析 XML 文件;AnnotationConfigApplicationContext则是在构造函数中通过AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner完成扫描和注册,然后在refresh()时刷新容器。这里我不展开所有实现类,但有一点你必须记住:refresh()之后,容器内部的BeanFactory(实际是DefaultListableBeanFactory)就已经持有了全部 BeanDefinition,但此时 bean 还没有实例化。
2.2 环境装配:prepareBeanFactory() 与 postProcessBeanFactory()
prepareBeanFactory()给底层的 BeanFactory 添加一些"标准配置",这些配置不依赖具体子类:
- 设置
ClassLoader和表达式解析器; - 注册默认的环境 bean:
environment、systemProperties、systemEnvironment; - 注册
ApplicationContextAwareProcessor,让实现 Aware 接口的 bean 能拿到容器自身; - 忽略
EnvironmentAware、ApplicationEventPublisherAware、ResourceLoaderAware等接口的自动装配,因为这些已经由 AwareProcessor 处理了; - 注册
ApplicationListenerDetector,用于在 bean 创建后自动检测并注册监听器; - 注册
LoadTimeWeaver(如果存在)和@Autowired注解的解析配置。
你有没有遇到过在一个普通@Component里直接注入ApplicationContext还注入成功的情况?就是因为ApplicationContextAwareProcessor在这里发挥了作用,它会在 bean 初始化阶段把容器回调给实现了ApplicationContextAware接口的对象。
紧接着的postProcessBeanFactory()是个空实现方法,留给子类去添加额外内容。比如GenericWebApplicationContext就利用这个方法注册了ServletContext相关的作用域和组件。Spring Boot 的ServletWebServerApplicationContext也通过重写这个方法注册了WebApplicationContextServletContextAwareProcessor。这一步骤体现了模板方法模式的典型用法:固定流程由父类控制,变化点交给子类扩展。
2.3 核心扩展点:invokeBeanFactoryPostProcessors() 和 registerBeanPostProcessors()
这两个方法放到一起讲,因为它们配合使用才能支撑起 Spring 的插件化扩展体系。
invokeBeanFactoryPostProcessors()负责找到容器里所有的BeanFactoryPostProcessor并执行它们的postProcessBeanFactory()方法。这个步骤的执行顺序有讲究,源码里有一段非常复杂的排序逻辑:
- 先执行通过编码方式手动注册的
BeanDefinitionRegistryPostProcessor,也就是实现了优先级接口PriorityOrdered的; - 再执行实现了
Ordered接口的; - 然后执行剩余的;
- 最后执行普通的
BeanFactoryPostProcessor。
为什么这个顺序重要?因为BeanDefinitionRegistryPostProcessor在运行时可以注册额外的 BeanDefinition,而后续 bean 的创建完全依赖 BeanDefinition。如果让普通后置处理器先执行,它面对的可能是"不完整"的 bean 定义集合。实际上 Spring 就是通过分层排序来保证扩展点的确定性。
我举个例子:很多框架的 starter 里会写一个BeanDefinitionRegistryPostProcessor来注册自己的核心组件,比如 MyBatis 的MapperScannerConfigurer。它在这一步执行时直接把所有 Mapper 接口扫描成 BeanDefinition,这样后面创建 bean 的阶段才能正常生产 Mapper 代理对象。
registerBeanPostProcessors()是另一个经常被误解的点:它只是"注册"后置处理器,并不立即执行。真正的执行时机是 bean 实例化过程中,AbstractAutowireCapableBeanFactory在创建每个 bean 时回调这些后置处理器。所以在容器启动早期,你往容器里放一个BeanPostProcessor,它只会被登记到beanPostProcessors列表里,要等finishBeanFactoryInitialization阶段创建 bean 时才会真正生效。
关于后置处理器在 Spring 容器启动流程中的依赖关系,可以这样理解:BeanFactoryPostProcessor 作用于 BeanDefinition,BeanPostProcessor 作用于 Bean 实例。前者属性改的是"图纸",后者改的是"成品"。两者都通过排序机制(PriorityOrdered / Ordered / 无序)来决定执行优先级。
2.4 事件与国际化:initMessageSource()、initApplicationEventMulticaster()、onRefresh()、registerListeners()
这几个步骤是为容器运行提供辅助能力。
initMessageSource()初始化消息源。如果容器里已经有用户自定义的MessageSourcebean,就直接使用;否则会注册一个DelegatingMessageSource作为默认实现。很多项目国际化不生效,原因就是没有配置任何 MessageSource bean,Spring 用了一个空实现兜底。
initApplicationEventMulticaster()初始化事件广播器。Spring 事件机制的核心是ApplicationEventMulticaster接口,默认实现是SimpleApplicationEventMulticaster。它主要负责维护监听器列表,并在事件发布时逐个回调。如果你的容器里没定义这个 bean,Spring 会创建默认实现并注册。
onRefresh()是一个模板方法,默认空实现,Spring Boot 的ServletWebServerApplicationContext在这里创建并启动内嵌 Tomcat、Jetty 或 Undertow。所以 Spring Boot 应用真正把 Web 服务器跑起来的时机不在 main 方法里,而是在 refresh() 的这一个阶段。
registerListeners()把容器中所有实现了ApplicationListener接口的 bean 注册到事件多播器中。同时,它还会把prepareRefresh()阶段暂存的 earlyApplicationEvents 在这里补发掉。
有个细节值得注意:如果某个事件是在registerListeners()之前发布的,监听器还没注册,事件不会丢失,而是被暂存在earlyApplicationEvents集合里,等监听器就绪后统一补发。但如果是registerListeners()之后发布的事件,走的就是常规的事件广播流程,不再有暂存机制。这解释了为什么很多人早期自定义事件时觉得"有时候收到有时候收不到"——很可能是在监听器注册完成之前发出去的。
2.5 重头戏:finishBeanFactoryInitialization() 和 finishRefresh()
finishBeanFactoryInitialization()是整条流程里工作量最大、也最容易出问题的一步。它会遍历所有已经注册的 BeanDefinition,实例化所有非懒加载的单例 bean。为了方便你理解,我再说细一点:
- 判断 beanDefinition 是否抽象、是否非单例、是否懒加载,符合条件的跳过;
- 创建 BeanFactoryPostProcessor 特殊标记的 bean(比如
BeanFactory自身); - 真正创建 bean 时,会经历:实例化前回调(
InstantiationAwareBeanPostProcessor)→ 构造器决定 → 实例化 → 属性填充(依赖注入)→ Aware 接口回调 →BeanPostProcessor的postProcessBeforeInitialization→@PostConstruct/InitializingBean→BeanPostProcessor的postProcessAfterInitialization; - 创建完成后放入容器一级缓存并做销毁回调注册。
这里也是循环依赖问题最多发的地方。因为很多 bean 都在这个阶段集中实例化,你中有我、我中有你就非常容易触发。
最后的finishRefresh()会做几件收尾工作:
- 清理资源缓存;
- 初始化生命周期处理器
LifecycleProcessor; - 调用
onRefresh()后启动已实现的 SmartLifecycle(如果容器支持); - 发布
ContextRefreshedEvent事件,通知所有监听器"容器已经准备好"; - 注册
LiveBeansView的 MBean。
如果你在项目里监听ContextRefreshedEvent,比如在ApplicationRunner或者ApplicationListener<ContextRefreshedEvent>里做初始化数据的逻辑,你体会到的时间点就是整个容器已经就绪之后。这也是为什么很多人用这个事件做"启动后执行一次"的操作。
3. 从源码到实战:这些步骤背后的关键机制
3.1 三级缓存机制与循环依赖的真相
Spring 解决构造器循环依赖之外的问题靠的是三级缓存,也就是容器内部维护了三个 map:
| 缓存 | 名称 | 作用 |
|---|---|---|
| 一级缓存 | singletonObjects | 存储完全创建好的单例 bean |
| 二级缓存 | earlySingletonObjects | 存储已实例化但还未完成属性填充的早期 bean |
| 三级缓存 | singletonFactories | 存储 ObjectFactory,用于生成早期的 bean 引用 |
这里的关键点在于三级缓存存的是ObjectFactory,而不是 bean 本身。为什么不是二级缓存直接存 bean?因为 Spring 需要支持 AOP 代理的延迟生成。如果一个 bean 最终需要被 AOP 代理,三级缓存中保存的工厂可以在其他 bean 引用这个早期实例时判断是否需要提前生成代理对象。这是 Spring 宁可多搞一层缓存也不提前生成代理的原因,否则可能导致所有 bean 都提前被代理,破坏了正常的生命周期。
我再说清楚一点:假设 A 和 B 互相依赖,A 先被创建。A 实例化后放入三级缓存,然后进行属性填充时发现需要 B,于是去创建 B。B 实例化后放入三级缓存,进行属性填充时发现需要 A,于是从三级缓存中拿到 A 的 ObjectFactory,调用getObject()得到 A 的早期引用,放入二级缓存。B 拿到 A 的引用后完成创建并进入一级缓存。接着 A 继续创建,从一级缓存拿到完全创建好的 B,完成自己的属性填充。最终 A 创建完毕也进入一级缓存。
但注意,如果你用@Async注解,Spring 会通过AbstractAutoProxyCreator提前对 bean 做代理,而 B 在创建时拿到的是 A 的早期引用,这个引用在 A 被真正执行postProcessAfterInitialization之前就已经被缓存了,B 持有的仍是原始 A 对象而不是最终代理对象,于是 B 调用 A 的方法时异步增强不生效,还会在后续触发校验异常。
很多人在网上搜"三级缓存原理"背得滚瓜烂熟,但遇到@Async循环依赖还是不知道怎么排查。我想用我的经验提醒你:碰到BeanCurrentlyInCreationException,先想清楚依赖链是不是构造器注入,是不是后置处理器创建了代理对象导致提前暴露的引用不完整,然后再考虑"加一个 @Lazy"这种缓解方案。
3.2 Bean 生命周期中的后置处理器依赖关系
Bean 生命周期和 BeanPostProcessor 的关系是面试和实战的高频交叉点。一个标准 bean 的创建过程是这样的:
- 实例化前:
InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()有机会直接返回一个代理对象,替代正常创建流程; - 实例化:通过构造器或工厂方法创建对象;
- 实例化后:
MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition()解析注解元数据,为后续依赖注入做准备;InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation()决定是否继续走自动属性填充; - 属性填充:
InstantiationAwareBeanPostProcessor.postProcessProperties()执行@Autowired、@Resource、@Value等注入; - 初始化前:
BeanPostProcessor.postProcessBeforeInitialization(),这里会触发 Aware 回调,然后执行用户标注的@PostConstruct方法; - 初始化:调用
InitializingBean.afterPropertiesSet()或自定义 initMethod; - 初始化后:
BeanPostProcessor.postProcessAfterInitialization(),AOP 代理通常在这里生成; - 使用阶段;
- 销毁:触发
@PreDestroy、DisposableBean.destroy()、destroyMethod。
后置处理器之间的依赖关系可以归纳为"应用链"模式:它们按照排序依次包裹在 bean 创建流程外,前一个处理器的输出是后一个处理器的输入。比如AutowiredAnnotationBeanPostProcessor负责@Autowired注入,AnnotationAwareAspectJAutoProxyCreator负责 AOP 代理,AsyncAnnotationBeanPostProcessor负责@Async处理。如果多个处理器都尝试修改同一个 bean 的行为,执行顺序就变得至关重要,这就是为什么 Spring 提供了PriorityOrdered和Ordered接口来排序。
3.3 手写一个 mini refresh(),理解容器启动的最小骨架
为了让你彻底理解这套流程,我简化出了一个最小可用版本。这里不追求完整源码,而是让你看到骨架和事件顺序。
public class MiniApplicationContext { private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); public void refresh() { // 1. 准备阶段 prepareRefresh(); // 2. 加载 bean 定义(省略 XML/注解解析逻辑) obtainFreshBeanFactory(); // 3. 注册后置处理器 registerBeanPostProcessors(); // 4. 实例化非懒加载单例 bean finishBeanFactoryInitialization(); } private void finishBeanFactoryInitialization() { for (String beanName : beanFactory.getBeanDefinitionNames()) { Object bean = createBean(beanName); singletonObjects.put(beanName, bean); } } private Object createBean(String beanName) { // 实例化 Object bean = new Object(); // 属性填充 // 初始化回调(省略) return bean; } private void registerBeanPostProcessors() { // 从 beanFactory 中查找 BeanPostProcessor 类型的 bean // 排序后放入列表,后续每个 bean 创建时依次回调 // beanFactory.addBeanPostProcessor(...) } private void obtainFreshBeanFactory() { // 把配置解析成 BeanDefinition 并注册到 beanFactory // beanFactory.registerBeanDefinition(...) } private void prepareRefresh() { // 设置状态、初始化配置源 } }实际项目中你可以直接拿DefaultListableBeanFactory做最小容器的研究,配合ClassPathBeanDefinitionScanner扫描配置类,再用AnnotationConfigApplicationContext做对比,能看到真容器多做了哪些事情。
4. 实战复盘:通过启动日志和断点定位容器初始化问题
4.1 如何高效观察 Spring 启动过程
想要真正理解 refresh(),光看源码不够,要把启动日志和断点结合起来。
Spring 启动时有一条经典的日志顺序(以 Spring Boot 为例):
Starting Application on ... Running with Spring Boot v... ... Root WebApplicationContext: initialization started in ... ... BeanFactoryPostProcessor 日志(自动配置类注册阶段) ... Tomcat initialized with port(s): 8080 ... Root WebApplicationContext initialized in ... ms ... Started Application in ... seconds其中Root WebApplicationContext: initialization started这条日志出现在prepareRefresh()阶段,Root WebApplicationContext initialized出现在finishRefresh()阶段。通过对比这两个时间点之间的耗时,你能快速判断容器初始化是否成了瓶颈。
如果你需要更细致的观察,可以在AbstractApplicationContext.refresh()的各个方法调用处打断点,或者在 IDEA 中使用 Evaluate Expression 查看beanFactory.getBeanDefinitionNames()的实时内容。我个人习惯在invokeBeanFactoryPostProcessors之前打一个条件断点,过滤特定 beanName,这样能快速确认某个自定义 BeanDefinition 是否已经注册。
4.2 用 BeanFactoryPostProcessor 和 BeanPostProcessor 做扩展的实战案例
我举一个实战案例:一个多租户项目需要根据配置动态注册 DataSource 对应的SqlSessionFactoryBean。
如果用BeanDefinitionRegistryPostProcessor,核心逻辑是:
@Component public class DynamicDataSourceRegister implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 动态注册多个数据源对应的 SqlSessionFactory BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(SqlSessionFactoryBean.class); builder.addPropertyValue("dataSource", dataSource()); builder.addPropertyValue("mapperLocations", new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); registry.registerBeanDefinition("sqlSessionFactory1", builder.getBeanDefinition()); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 通常不需要额外处理 } }这段代码能生效的根本原因就是 refresh() 里invokeBeanFactoryPostProcessors()在加载完 BeanDefinition 之后、实例化 bean 之前执行了它。
再举个例子,如果你想在某个 bean 初始化完成后自动做监控指标上报,可以写一个BeanPostProcessor:
@Component public class MonitoringBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof HealthCheckable) { MetricsRegistry.register(beanName, (HealthCheckable) bean); } return bean; } }这里的关键认知是:MonitoringBeanPostProcessor自身在容器启动早期被注册进beanPostProcessors列表,但它对所有其他 bean 的监控逻辑是在其他 bean 各自的创建过程中生效的。如果你在两阶段之间理解不透,就很容易出现"后置处理器写了但一直没执行"的困惑。
4.3 Spring Boot 的 refresh() 扩展:自动配置如何被加载
Spring Boot 和 Spring 的差异主要体现在 refresh() 前后。Spring Boot 通过SpringApplication.run()最终也会调用ConfigurableApplicationContext.refresh(),但在调用之前做了这些事:
- 通过
@SpringBootApplication的组合注解启用自动配置; SpringFactoriesLoader加载META-INF/spring.factories中的AutoConfiguration类;- 通过
ConditionalOnClass、ConditionalOnMissingBean等条件注解过滤自动配置类; - 自动配置类本质上也是配置类,会被解析成 BeanDefinition,进入 refresh() 的标准流程。
进入 refresh() 之后,Spring Boot 最关键的动作是在onRefresh()阶段启动内嵌 Web 容器。你可以这样理解:Spring Framework 的 refresh() 是"做菜流程",Spring Boot 往流程里塞了"自动点单"和"自动摆桌"。
如果你深入研究过 Spring Boot 源码,你会发现ServletWebServerApplicationContext.onRefresh()调用了createWebServer()。这个方法创建 Servlet 容器并注册 dispatcherServlet 过程。这也就解释了为什么 Spring Boot 项目里DispatcherServlet的初始化时机和ApplicationContext的事件发布时机密切相关。
5. 常见问题与排查技巧实录
5.1 循环依赖报错:明明三级缓存能解决,为什么还报错
三级缓存只能解决"单例 bean + 属性注入"的循环依赖,以下情况依然会报错:
- 构造器注入循环依赖:因为 bean 实例化时还没放入三级缓存,依赖根本拿不到早期引用;
@Async等代理增强引起的循环依赖:前面分析过,提前暴露的对象不是最终代理对象;- 非单例作用域的 bean(prototype)循环依赖:三级缓存不负责 prototype;
@DependsOn强制依赖顺序导致先创建的 bean 还没缓存。
排查思路:先看异常栈里提到了哪个 beanName,然后在 IDE 里全局搜索该 bean 的引用关系。用@Lazy在字段上注入可以延迟代理的创建,但这不是根治办法,长期看应该重构依赖方向。
5.2 Bean 定义被覆盖:allowBeanDefinitionOverriding 的坑
Spring Boot 2.1 之后默认不允许 bean 定义覆盖,除非设置spring.main.allow-bean-definition-overriding=true。很多人扫描包时不小心同时加载了两个相同 beanName 的配置,启动会直接报BeanDefinitionOverrideException。
但有时候允许覆盖会掩盖问题:两个 starter 都定义了同一个 bean,但实现逻辑完全不一样,启动后到底用的是哪个,取决于加载顺序。这是一个很隐蔽的坑。我的建议是保持默认关闭覆盖,遇到冲突时优先用@Primary、@ConditionalOnMissingBean或在配置里排除自动配置类。
5.3 重复 refresh() 导致容器状态异常
前面说过 refresh() 理论上可以多次调用。但在 Spring Boot 场景下,同一个 context 如果重复 refresh,内部组件比如内嵌服务器可能无法正常工作。我在测试代码里试过在同一个AnnotationConfigApplicationContext对象上连续调用两次 refresh(),第二次启动会报各种状态异常。
排查方法:在AbstractApplicationContext里设置了synchronized (startupShutdownMonitor)锁,如果出现启动卡死,先用 jstack 看线程栈,确认是否有其他线程也在操作容器。实际应用中极少有人主动重复 refresh,但如果遇到,多半是测试代码或者动态刷新配置时误触。
故障现象通常是:bean 创建了一部分,另一部分拿到的是上一轮的旧实例,容器状态不一致。规避方式很简单——每次需要新容器就 new 一个 context,不要复用。
5.4 监听器没生效、事件没触发的问题
@EventListener注解的方法能被调用,依赖的是EventListenerMethodProcessor。这个类本身实现了SmartInitializingSingleton接口,它会在单例 bean 预实例化阶段结束后,扫描所有 bean,把带@EventListener的方法包装成ApplicationListenerMethodAdapter注册到事件多播器。
所以你会发现一个现象:如果某个@EventListener所在的 bean 是懒加载的,或者因为某种原因没有被实例化,它的事件监听就不会生效。Spring Boot 应用里常见的原因是启动后主动调用了close(),或者监听器所在 bean 被条件装配排除了。排查时可以查看启动日志里的 listener 注册情况,或者在ApplicationEventMulticaster.getApplicationListeners()上打断点确认。
5.5 常见问题速查表
| 症状 | 可能阶段 | 常见原因 | 解决思路 |
|---|---|---|---|
| MissingRequiredPropertiesException | prepareRefresh | 环境配置缺必填项 | 检查 Environment 属性及配置中心 |
| BeanDefinitionOverrideException | obtainFreshBeanFactory / 后置处理器 | 同名 BeanDefinition 冲突 | 关闭覆盖开关或排查扫描路径 |
| BeanCurrentlyInCreationException | finishBeanFactoryInitialization | 构造器循环依赖 / @Async 场景 | 加 @Lazy、重构依赖或改为 setter 注入 |
| NoSuchBeanDefinitionException | 任何阶段 | BeanDefinition 未被加载 | 检查扫描范围、条件注解、BeanDefinitionRegistryPostProcessor |
| ContextRefreshedEvent 没触发 | finishRefresh | 监听器注册失败或容器提前关闭 | 检查 listener bean 是否被实例化、有没有多容器并存 |
| 自定义后置处理器不执行 | registerBeanPostProcessors | 处理器本身没被注册或排序导致被跳过 | 在处理器打断点,看它是否进入 beanPostProcessors 列表 |
我在实际项目中踩过的最深的一个坑是:一个公共组件模块里的BeanPostProcessor被定义成内部类,没有设置 public,结果 Spring 扫描时静默跳过。启动不报错,但那批增强逻辑一直没生效,排查花了大半天。后来我提醒团队,凡是全局后置处理器的类,必须能被 Spring 正常扫描和实例化,并且最好打日志确认注册数量。
另外还有一个排查利器:开启 Spring 的 debug 日志,给org.springframework.beans.factory和org.springframework.context包设置TRACE级别,能看到每一步 bean 的创建顺序和 postProcessor 的执行日志。这个日志量很大,你最好配合启动参数定向输出到单独文件。
最后分享一个我自己用来验证"是否真正理解了 refresh()"的小技巧:给一个普通的@Component同时实现BeanFactoryPostProcessor、BeanPostProcessor、ApplicationListener<ContextRefreshedEvent>三个接口,然后观察这三个回调的执行顺序和日志打印顺序。跑一遍之后你会发现,postProcessBeanFactory最先执行,postProcessAfterInitialization在 bean 创建时执行,onApplicationEvent最后执行。有了这种直观感受之后再读源码,很多概念就一次性串起来了。如果你还能从这个组件里意识到"一个 bean 承担多种角色"时的副作用,你对 Spring 容器启动流程就已经不再是背答案的水平了。