Day15 SpringIOC容器启动全流程:从ApplicationContext到Bean实例化

Day15 SpringIOC容器启动全流程:从ApplicationContext到Bean实例化

专栏:《Java后端工程师进阶之路》(Day 15 / 90) 主题:从零搭建流式AI聊天后端:SSE协议原理 + WebFlux响应式流 + 前端EventSource接收

如果你能把这个流程讲清楚,Spring面试基本就通关了。但更重要的是,理解它之后,你排查线上启动慢、Bean注入失败、循环依赖报错这类问题的时候,思路会完全不同。

场景还原:一个团队生产问题,他们的微服务在 K8s 上重启后,前 3 次启动全部超时被杀(readiness probe 失败),第 4 次才勉强活过来。运维以为是资源不够,加内存加 CPU 都没用。

我一问才知道,他们在启动阶段偷偷干了几件事:一个@PostConstruct里同步拉取远程配置中心,三个@Autowired的 Bean 互相嵌套依赖,还有个FactoryBeangetObject()里调了数据库查元数据。

这不是内存不够,是 IOC 容器启动被他们拖死了。

我让他们把启动流程打印出来,一看日志:refresh() 的 12 步,卡在第 6 步和第 11 步,加起来占了 38 秒。

今天这篇文章,就是把 Spring IOC 容器启动的完整流程拆给你看。不只是"面试背八股"那种程度—一起走到源码层面,理解每一环到底干了什么,为什么卡,以及怎么优化。


一、IOC 容器启动的 12 步流水线

Spring Boot 启动时,不管你用的是SpringApplication.run()还是AnnotationConfigApplicationContext,最终都会走到AbstractApplicationContext.refresh()方法。这个方法就是容器的启动总开关,里面 12 个核心步骤一个都跳不过。

// org.springframework.context.support.AbstractApplicationContext public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // 1. 准备刷新:记录启动时间、初始化属性源 prepareRefresh(); // 2. 获取 BeanFactory:如果是 AnnotationConfig 会在这里创建 DefaultListableBeanFactory ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 3. 准备 BeanFactory:注册内置 Bean(环境、系统属性等)、设置类加载器 prepareBeanFactory(beanFactory); try { // 4. 后置处理:留给子类的扩展点(Spring Boot 正是在这里插入 WebServer 的创建逻辑) postProcessBeanFactory(beanFactory); // 5. 调用 BeanFactoryPostProcessor:配置类解析就在这里发生! invokeBeanFactoryPostProcessors(beanFactory); // 6. 注册 BeanPostProcessor registerBeanPostProcessors(beanFactory); // 7. 初始化消息源(国际化) initMessageSource(); // 8. 初始化事件广播器 initApplicationEventMulticaster(); // 9. 留给子类的扩展点(Spring Boot 在 WebServer 时用的就是它) onRefresh(); // 10. 注册事件监听器 registerListeners(); // 11. 实例化所有非懒加载的单例 Bean —— 这就是最耗时的一步! finishBeanFactoryInitialization(beanFactory); // 12. 完成刷新:发布 ContextRefreshedEvent,启动 WebServer finishRefresh(); } catch (BeansException ex) { // 销毁已创建的 Bean destroyBeans(); cancelRefresh(ex); throw ex; } } }

我把这 12 步画成一条流水线,你一看就懂哪些是"轻量操作",哪些是"重灾区":

prepareRefresh → 时钟开始,属性源准备 [毫秒级] obtainFreshBeanFactory → 新建/刷新 BeanFactory [毫秒级] prepareBeanFactory → 注入环境变量、类加载器 [毫秒级] postProcessBeanFactory → WebServer 创建(Spring Boot) [几十毫秒] 😂 invokeBeanFactoryPostProcessors → ConfigurationClassParser 解析所有 @Configuration [秒级] registerBeanPostProcessors → 注册 BPP(不动手,只登记) [毫秒级] initMessageSource → 国际化的东西 [毫秒级] initApplicationEventMulticaster → 事件广播器初始化 [毫秒级] onRefresh → start WebServer (Spring Boot) [百毫秒] registerListeners → 把 Listener 挂上广播器 [毫秒级] 🤮 finishBeanFactoryInitialization → 实例化所有单例 Bean [秒到几十秒] finishRefresh → 发布事件,正式运行 [毫秒级]

你看到了——真正的两个重灾区是第 5 步第 11 步。一个负责"看懂你的配置",一个负责"把所有东西造出来"。下面我们分别拆开看。


二、第 5 步深入:BeanDefinition 的加载链路

第 5 步invokeBeanFactoryPostProcessors是整个容器启动中最"烧脑"的一步。它会找到所有BeanFactoryPostProcessor(包括BeanDefinitionRegistryPostProcessor),然后按优先级分三轮调用。

但对我们来说,最关键的是一个叫ConfigurationClassPostProcessor的家伙——它是 Spring 内置的、优先级最高的处理器,核心任务就是解析所有@Configuration类,把里面@Bean方法的定义注册成 BeanDefinition

整个链路的调用栈是这样的:

refresh() → invokeBeanFactoryPostProcessors() → ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry() → ConfigurationClassParser.parse() → processConfigurationClass() → doProcessConfigurationClass() → 扫描 @ComponentScan → 处理 @Import → 处理 @Bean 方法 → 处理父类配置

每一步都在往DefaultListableBeanFactorybeanDefinitionMap里塞东西。这个beanDefinitionMap是一个ConcurrentHashMap<String, BeanDefinition>,key 是 beanName,value 是 BeanDefinition。

来看一段代码,验证这个过程。我们写一个最简单的 Spring Boot 启动入口,自己跟踪 BeanDefinition 的数量变化:

// DefaultListableBeanFactory.preInstantiateSingletons() 简化版 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:先创建工厂本身,前缀加 & Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); } else { // 普通 Bean:走完整的 getBean 流程 getBean(beanName); } } } // 所有单例 Bean 实例化完成后,触发 SmartInitializingSingleton 回调 for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { ((SmartInitializingSingleton) singletonInstance).afterSingletonsInstantiated(); } } }

在 Spring Boot 3.2 最小项目(只依赖 spring-boot-starter-web)中,这段代码输出大约是480~520 个 BeanDefinition。你没看错——你写了一个main方法,Spring 帮你注册了 500 个 Bean。其中 90% 来自自动配置。

这就是为什么 Spring Boot 启动不慢——它懒。注册 500 个 BeanDefinition 只是登记了一个"设计图",真正的实例化还没开始。第 11 步才会动真格的。


三、第 11 步深入:finishBeanFactoryInitialization 到底干了什么

这是全流程最耗时的一步,也是"循环依赖三级缓存"发挥作用的地方。

3.1 入口:preInstantiateSingletons

第 11 步的入口在DefaultListableBeanFactory.preInstantiateSingletons(),逻辑很简单:

// DefaultListableBeanFactory.preInstantiateSingletons() 简化版 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:先创建工厂本身,前缀加 & Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); } else { // 普通 Bean:走完整的 getBean 流程 getBean(beanName); } } } // 所有单例 Bean 实例化完成后,触发 SmartInitializingSingleton 回调 for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName); if (singletonInstance instanceof SmartInitializingSingleton) { ((SmartInitializingSingleton) singletonInstance).afterSingletonsInstantiated(); } } }

每个getBean(beanName)都会走AbstractBeanFactory.doGetBean()DefaultSingletonBeanRegistry.getSingleton()

3.2 getBean 的核心流程

getBean(beanName) → doGetBean() → getSingleton(beanName) → 从三级缓存尝试拿 → 一级:singletonObjects(完全体) → 二级:earlySingletonObjects(半成品) → 三级:singletonFactories(工厂) → 如果都没拿到: → createBean(beanName) → doCreateBean() → createBeanInstance() —— 反射构造 → populateBean() —— 属性填充(触发依赖注入) → initializeBean() —— Aware 回调 → PostProcessors → init-method

3.3 三级缓存:解决循环依赖的核心武器

这是面试必问题。咱们不背答案,直接看代码。

// DefaultSingletonBeanRegistry 中的三级缓存定义 public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry { /** 一级缓存:完全实例化 + 属性注入 + 初始化完成的 Bean */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** 二级缓存:已经实例化但属性还没注入完的「早期引用」*/ private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); /** 三级缓存:可以生产早期引用的 ObjectFactory(本质是 Lambda)*/ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); }

假设 A 依赖 B,B 依赖 A——经典循环依赖。流程是这样的:

// 伪代码演示循环依赖的解决过程 // === 创建 A === // 1. A.createBeanInstance() → 反射调用构造器,A 实例被创建(但属性全是 null) // 2. A 实例被包装成 ObjectFactory,放入三级缓存 singletonFactories // 三级缓存存入: lambda(() -> getEarlyBeanReference("a", aInstance)) // 3. A.populateBean() → 发现依赖 B → 触发 getBean("b") // === 创建 B === // 4. B.createBeanInstance() → B 实例创建 // 5. B 包装成 ObjectFactory,放入三级缓存 // 6. B.populateBean() → 发现依赖 A → 触发 getBean("a") // 7. getSingleton("a"): // → 一级缓存: 没找到(A 还没完成) // → 二级缓存: 没找到 // → 三级缓存: 找到了!调用 ObjectFactory.getObject() // → 拿到 A 的早期引用(可能是代理对象) // → 把早期引用升级到二级缓存,从三级删除 // → 返回 A 的早期引用 // 8. B 拿到 A 的引用后继续填充,B 初始化完成 // 9. B 放入一级缓存 singletonObjects // === 回到 A 的创建 === // 10. A 拿到 B 的完整实例,填充完成 // 11. A 初始化完成,放入一级缓存 // (此时一级缓存中 A 的早期引用被完整版替换)

关键点总结:

  • 一级缓存存的是"成品",能正常使用
  • 二级缓存存的是"半成品",实例化了但属性还没填完,用来暴露给其他 Bean 解决循环依赖
  • 三级缓存存的是"工厂",只有在真正需要循环依赖时才触发(Lambda 懒执行)
  • 构造器注入的循环依赖无法解决——因为 Bean 连实例都没创建出来,根本放不进缓存

四、实战:定位启动慢的原因

我在开篇说的那个线上案例,最终定位问题用的是 Spring Boot 自带的ApplicationStartup机制。

Spring Boot 2.4+ 提供了BufferingApplicationStartup,可以记录每一步的耗时:

// Spring Boot 3.2.x, JDK 21 import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; @SpringBootApplication public class StartupDiagnosticsApplication { public static void main(String[] args) { SpringApplication app = new SpringApplication(StartupDiagnosticsApplication.class); // 关键配置:记录所有启动步骤耗时 app.setApplicationStartup(new BufferingApplicationStartup(2048)); // 启动后,通过 Actuator 端点查看 // GET /actuator/startup → 返回每一步的 start/end/duration app.run(args); } }

然后在application.yml中暴露端点:

yaml

# application.yml management: endpoints: web: exposure: include: startup

启动后访问 http://localhost:8080/actuator/startup,你会看到类似这样的 JSON:

{ "spring.boot.application.starting": { "duration": "0.001s" }, "spring.beans.instantiate": [ { "beanName": "dataSource", "duration": "1.234s" }, { "beanName": "entityManagerFactory", "duration": "3.567s" }, { "beanName": "myHeavyService", "duration": "8.912s" } ] }

哪个 Bean 慢,一目了然。


五、三条建议

1. @PostConstruct 别做重活

@PostConstructinitializeBean()阶段执行,属于第 11 步单线程串行的一部分。如果在这里拉远程配置、预热缓存、建数据库连接池,每个 Bean 慢 2 秒,10 个 Bean 就是 20 秒。把这些活扔到ApplicationRunner@EventListener(ContextRefreshedEvent.class)里异步做,不阻塞容器启动。

2. 循环依赖能避免就避免

三级缓存能救你一次,但救不了构造器注入的循环依赖,也救不了@Async+@Transactional代理叠加后的诡异问题。设计层面解耦(引入中间层、用事件驱动),比依赖 Spring 的"魔法"更稳。

3. 善用 Bean 的懒加载

spring.main.lazy-initialization=true可以把所有 Bean 变成懒加载,容器启动时间直接减半。但代价是第一个请求会慢——适合 K8s 快速启动的场景。还有一种更精细的做法:只对启动链路上的 Bean 做非懒加载,其他都懒加载。

spring: main: lazy-initialization: true
// 只对启动必需的 Bean 标注为非懒加载 @Component @Lazy(false) public class HealthCheckService { // 这个 Bean 在容器启动时就会被创建 }

Spring IOC 容器的启动流程看起来复杂,但本质上就是一个工厂在开工前的准备流程:读图纸(解析配置)→ 备料(注册 BeanDefinition)→ 开动机器(实例化 Bean)→ 质检出厂(初始化回调)。

把这个流程装进脑子里,你排查"启动慢""注入失败""循环依赖"这三个高频问题的时候,就不再是瞎猜了——你知道该去哪个步骤找答案。

金句:Spring 容器的启动不是魔法,它只是把你能手写的代码,用一套精密的流水线按时序执行了一遍。读懂它,你就不是在"用"框架,你是在跟它配合。

下一篇 Day 16:Spring AOP 原理深度——JDK 动态代理 vs CGLIB,自调用失效到底是谁的锅。