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

发布时间:2026/7/7 4:45:21
Day15 SpringIOC容器启动全流程:从ApplicationContext到Bean实例化 专栏《Java后端工程师进阶之路》Day 15 / 90 主题从零搭建流式AI聊天后端SSE协议原理 WebFlux响应式流 前端EventSource接收如果你能把这个流程讲清楚Spring面试基本就通关了。但更重要的是理解它之后你排查线上启动慢、Bean注入失败、循环依赖报错这类问题的时候思路会完全不同。场景还原一个团队生产问题他们的微服务在 K8s 上重启后前 3 次启动全部超时被杀readiness probe 失败第 4 次才勉强活过来。运维以为是资源不够加内存加 CPU 都没用。我一问才知道他们在启动阶段偷偷干了几件事一个PostConstruct里同步拉取远程配置中心三个Autowired的 Bean 互相嵌套依赖还有个FactoryBean在getObject()里调了数据库查元数据。这不是内存不够是 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 方法 → 处理父类配置每一步都在往DefaultListableBeanFactory的beanDefinitionMap里塞东西。这个beanDefinitionMap是一个ConcurrentHashMapString, BeanDefinitionkey 是 beanNamevalue 是 BeanDefinition。来看一段代码验证这个过程。我们写一个最简单的 Spring Boot 启动入口自己跟踪 BeanDefinition 的数量变化// DefaultListableBeanFactory.preInstantiateSingletons() 简化版 public void preInstantiateSingletons() throws BeansException { ListString 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 { ListString 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-method3.3 三级缓存解决循环依赖的核心武器这是面试必问题。咱们不背答案直接看代码。// DefaultSingletonBeanRegistry 中的三级缓存定义 public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry { /** 一级缓存完全实例化 属性注入 初始化完成的 Bean */ private final MapString, Object singletonObjects new ConcurrentHashMap(256); /** 二级缓存已经实例化但属性还没注入完的「早期引用」*/ private final MapString, Object earlySingletonObjects new HashMap(16); /** 三级缓存可以生产早期引用的 ObjectFactory本质是 Lambda*/ private final MapString, ObjectFactory? singletonFactories new HashMap(16); }假设 A 依赖 BB 依赖 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 别做重活PostConstruct在initializeBean()阶段执行属于第 11 步单线程串行的一部分。如果在这里拉远程配置、预热缓存、建数据库连接池每个 Bean 慢 2 秒10 个 Bean 就是 20 秒。把这些活扔到ApplicationRunner或EventListener(ContextRefreshedEvent.class)里异步做不阻塞容器启动。2. 循环依赖能避免就避免三级缓存能救你一次但救不了构造器注入的循环依赖也救不了AsyncTransactional代理叠加后的诡异问题。设计层面解耦引入中间层、用事件驱动比依赖 Spring 的魔法更稳。3. 善用 Bean 的懒加载spring.main.lazy-initializationtrue可以把所有 Bean 变成懒加载容器启动时间直接减半。但代价是第一个请求会慢——适合 K8s 快速启动的场景。还有一种更精细的做法只对启动链路上的 Bean 做非懒加载其他都懒加载。spring: main: lazy-initialization: true// 只对启动必需的 Bean 标注为非懒加载 Component Lazy(false) public class HealthCheckService { // 这个 Bean 在容器启动时就会被创建 }Spring IOC 容器的启动流程看起来复杂但本质上就是一个工厂在开工前的准备流程读图纸解析配置→ 备料注册 BeanDefinition→ 开动机器实例化 Bean→ 质检出厂初始化回调。把这个流程装进脑子里你排查启动慢注入失败循环依赖这三个高频问题的时候就不再是瞎猜了——你知道该去哪个步骤找答案。金句Spring 容器的启动不是魔法它只是把你能手写的代码用一套精密的流水线按时序执行了一遍。读懂它你就不是在用框架你是在跟它配合。下一篇 Day 16Spring AOP 原理深度——JDK 动态代理 vs CGLIB自调用失效到底是谁的锅。