Skip to content

Spring Boot 启动流程全过程原理

Spring Boot 启动不是“一个 main 方法就完了”。SpringApplication.run() 背后会推断应用类型、准备配置环境、创建容器、加载 BeanDefinition、执行自动配置、刷新 IOC 容器、启动内嵌 WebServer、发布事件、执行 Runner。任何一步出问题,都可能导致启动慢、Bean 没创建、端口没起来、自动配置没生效。

一句话先建立直觉:

Spring Boot 启动流程就是:先准备运行环境,再创建并刷新 Spring 容器,最后按应用类型启动 WebServer 并发布启动完成事件。

学习目标

学完这一页,你要能说清楚:

  1. SpringApplication.run() 大体做了哪些事。
  2. Boot 怎么推断当前是普通应用、Servlet Web 还是 Reactive Web。
  3. Environment 在启动流程中为什么这么早准备。
  4. ApplicationContext 为什么有不同实现。
  5. 自动配置发生在启动流程的哪一步。
  6. refresh() 为什么是 Spring 容器启动核心。
  7. 内嵌 Tomcat 什么时候创建,谁创建。
  8. ApplicationRunnerCommandLineRunner 和启动事件有什么区别。
  9. 启动慢、端口占用、Bean 创建失败、自动配置没生效怎么排查。
  10. 商业项目里哪些初始化逻辑不能阻塞启动。

最小启动代码

java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

表面上只有一行:

java
SpringApplication.run(DemoApplication.class, args);

实际它会完成:

  1. 创建 SpringApplication
  2. 准备监听器和初始化器。
  3. 准备配置环境。
  4. 创建 Spring 容器。
  5. 加载启动类和自动配置。
  6. 刷新容器。
  7. 启动 WebServer。
  8. 执行启动完成后的回调。

总体流程图

mermaid
flowchart TD
    A["main 方法"] --> B["SpringApplication.run"]
    B --> C["创建 SpringApplication"]
    C --> D["推断 WebApplicationType"]
    D --> E["加载初始化器和监听器"]
    E --> F["准备 Environment"]
    F --> G["打印 Banner"]
    G --> H["创建 ApplicationContext"]
    H --> I["加载启动类、业务组件、自动配置类"]
    I --> J["refresh 刷新容器"]
    J --> K["实例化非懒加载单例 Bean"]
    K --> L["启动内嵌 WebServer"]
    L --> M["发布 Started/Ready 事件"]
    M --> N["执行 Runner"]

这张图要和 Spring、Spring Boot、Web 三条线合起来看:

阶段属于哪条线说明
准备 EnvironmentBoot 工程化配置、Profile、命令行参数
加载自动配置Boot 自动配置候选配置类、条件注解
refreshSpring IOCBeanDefinition、Bean 生命周期
启动 WebServerBoot WebTomcat/Jetty/Undertow
DispatcherServletSpring MVC请求分发

创建 SpringApplication

SpringApplication.run(DemoApplication.class, args) 内部会先创建 SpringApplication 对象。

它会准备一些基础信息:

  1. 主启动类是谁。
  2. 应用类型是什么。
  3. 初始化器有哪些。
  4. 监听器有哪些。
  5. 是否打印 Banner。
  6. 是否允许懒加载。

可以手动创建:

java
import org.springframework.boot.SpringApplication;

public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(DemoApplication.class);
        application.setAdditionalProfiles("dev");
        application.run(args);
    }
}

这说明 run 不是魔法,它是一个可配置的启动器。

推断 WebApplicationType

Spring Boot 会根据 classpath 推断应用类型:

类型场景常见容器
NONE命令行、批处理、非 Web 服务AnnotationConfigApplicationContext
SERVLETSpring MVC、传统 WebAnnotationConfigServletWebServerApplicationContext
REACTIVEWebFlux 响应式应用AnnotationConfigReactiveWebServerApplicationContext

判断逻辑可以简化理解为:

mermaid
flowchart TD
    A["检查 classpath"] --> B{"有 WebFlux 且没有 Spring MVC 吗"}
    B -- "是" --> C["REACTIVE"]
    B -- "否" --> D{"有 Servlet 和 Spring MVC 相关类吗"}
    D -- "是" --> E["SERVLET"]
    D -- "否" --> F["NONE"]

为什么这一步重要?

因为应用类型决定后面创建哪种 ApplicationContext,也决定是否需要启动内嵌 WebServer。

初始化器和监听器

Spring Boot 启动过程中有两个容易混淆的扩展:

扩展作用时机
ApplicationContextInitializer在容器创建后、refresh 前修改容器容器早期
ApplicationListener监听启动事件、失败事件、刷新事件多个阶段

初始化器示例:

java
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;

public class StartupMarkerInitializer
        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext context) {
        context.getBeanFactory().registerSingleton("startupMarker", "context-created");
    }
}

事件监听示例:

java
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;

public class ReadyListener implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        System.out.println("应用已经可以接收请求");
    }
}

区别:Initializer 是“改容器”,Listener 是“听事件”。

准备 Environment

Environment 准备很早,因为后续很多动作都依赖配置:

  1. 哪个 Profile 生效。
  2. 端口是多少。
  3. 是否启用某个自动配置。
  4. 数据源地址是什么。
  5. 日志级别是什么。

配置加载链路:

mermaid
flowchart TD
    A["准备 Environment"] --> B["读取命令行参数"]
    A --> C["读取系统属性"]
    A --> D["读取环境变量"]
    A --> E["读取 application.yml"]
    A --> F["读取 Profile 配置"]
    A --> G["读取配置中心或自定义 PropertySource"]
    B --> H["合并 PropertySource"]
    C --> H
    D --> H
    E --> H
    F --> H
    G --> H

为什么配置解密要放在早期?

因为自动配置和配置绑定会在后面读取配置。如果你等业务 Bean 创建后才解密,很多条件判断已经结束了。

创建 ApplicationContext

不同应用类型创建不同容器:

应用类型ApplicationContext
普通非 WebAnnotationConfigApplicationContext
Servlet WebAnnotationConfigServletWebServerApplicationContext
Reactive WebAnnotationConfigReactiveWebServerApplicationContext

Servlet Web 容器不仅有普通 IOC 能力,还能创建 WebServer。

如果你引入 spring-boot-starter-web,通常就是 Servlet Web 应用,默认会创建支持内嵌 Tomcat 的上下文。

加载启动类和自动配置

启动类上的 @SpringBootApplication 是组合注解:

mermaid
flowchart TD
    A["@SpringBootApplication"] --> B["@SpringBootConfiguration"]
    A --> C["@ComponentScan"]
    A --> D["@EnableAutoConfiguration"]
    B --> E["启动类作为配置类"]
    C --> F["扫描业务组件"]
    D --> G["导入自动配置候选类"]

这一步会让 Spring 知道:

  1. 启动类是配置类。
  2. 要从启动类所在包开始扫描组件。
  3. 要导入自动配置候选类。

启动类放错包是新手常见坑:

text
com.company.Application
com.company.user.UserController
com.company.order.OrderService

可以扫到。

text
com.company.user.Application
com.company.order.OrderService

默认可能扫不到 com.company.order

自动配置发生在哪里

自动配置不是在某个神秘线程里发生。它发生在配置类解析阶段,最终仍然变成 BeanDefinition。

mermaid
flowchart TD
    A["@EnableAutoConfiguration"] --> B["AutoConfigurationImportSelector"]
    B --> C["读取 spring.factories 或 AutoConfiguration.imports"]
    C --> D["得到候选自动配置类"]
    D --> E["处理 exclude、去重、排序"]
    E --> F["条件注解判断"]
    F --> G["注册 BeanDefinition"]
    G --> H["refresh 阶段创建 Bean"]

所以自动配置和 Spring IOC 是一条链:

text
自动配置负责把默认 Bean 定义放进容器,IOC 负责真正创建和管理 Bean。

refresh 为什么是核心

ApplicationContext.refresh() 是 Spring 容器真正启动的核心。

可以拆成:

mermaid
flowchart TD
    A["refresh"] --> B["准备 BeanFactory"]
    B --> C["执行 BeanFactoryPostProcessor"]
    C --> D["注册 BeanPostProcessor"]
    D --> E["初始化消息源和事件广播器"]
    E --> F["Servlet Web 应用创建 WebServer"]
    F --> G["实例化非懒加载单例 Bean"]
    G --> H["完成容器刷新"]

关键点:

  1. BeanDefinition 在这里被进一步处理。
  2. BeanPostProcessor 在这里注册,为 AOP、事务、生命周期增强做准备。
  3. 非懒加载单例 Bean 通常在这里实例化。
  4. Servlet Web 应用的 WebServer 也在刷新过程中创建。

如果启动卡住,很多时候就是卡在 refresh() 的某个阶段。

Bean 创建阶段发生什么

非懒加载单例 Bean 会在容器启动阶段创建。

大致流程:

mermaid
flowchart TD
    A["BeanDefinition"] --> B["实例化对象"]
    B --> C["依赖注入"]
    C --> D["Aware 回调"]
    D --> E["BeanPostProcessor 初始化前"]
    E --> F["@PostConstruct / init-method"]
    F --> G["BeanPostProcessor 初始化后"]
    G --> H["单例 Bean 可用"]

这解释了启动慢的常见原因:

  1. 构造方法里访问外部系统。
  2. @PostConstruct 做大批量预热。
  3. 初始化方法里执行慢 SQL。
  4. BeanPostProcessor 做大量扫描。
  5. 创建代理时依赖复杂。

商业项目里,Bean 初始化阶段要尽量轻,不要把不可控长任务塞进去。

内嵌 Tomcat 什么时候启动

Servlet Web 应用里,WebServer 通常在 refresh() 过程中创建。

简化链路:

text
refresh()
  -> onRefresh()
  -> createWebServer()
  -> ServletWebServerFactory#getWebServer()
  -> Tomcat 启动并绑定端口

流程图:

mermaid
flowchart TD
    A["ServletWebServerApplicationContext"] --> B["refresh"]
    B --> C["onRefresh"]
    C --> D["createWebServer"]
    D --> E["查找 ServletWebServerFactory"]
    E --> F["TomcatServletWebServerFactory"]
    F --> G["创建 Tomcat"]
    G --> H["注册 Servlet、Filter、Listener"]
    H --> I["绑定端口并启动"]

关键 Bean:

Bean作用
TomcatServletWebServerFactory创建内嵌 Tomcat
DispatcherServletSpring MVC 前端控制器
DispatcherServletRegistrationBean把 DispatcherServlet 注册到 Servlet 容器
FilterRegistrationBean注册 Filter
ServletRegistrationBean注册自定义 Servlet

请求为什么能进入 Controller

启动成功后,请求链路大致是:

mermaid
flowchart TD
    A["客户端请求"] --> B["Tomcat Connector"]
    B --> C["Filter 链"]
    C --> D["DispatcherServlet"]
    D --> E["HandlerMapping 找到 Controller 方法"]
    E --> F["HandlerAdapter 调用方法"]
    F --> G["参数解析、校验、业务调用"]
    G --> H["HttpMessageConverter 写 JSON 响应"]

这就是为什么 Spring Boot 启动流程不能只学到端口监听,还要知道 DispatcherServlet 是怎么注册进 WebServer 的。

Runner 和启动事件

Spring Boot 启动后常见两个 Runner:

java
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmupRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        System.out.println("启动完成后预热缓存");
    }
}
java
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class StartupCheckRunner implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("启动完成后检查依赖");
    }
}

区别:

扩展参数适合场景
ApplicationRunnerApplicationArguments,参数更结构化读取启动参数、轻量初始化
CommandLineRunner原始 String... args简单启动后任务

注意:Runner 仍然会影响应用“完全启动完成”的时间。不要在 Runner 里做不可控长任务。

常见启动事件

事件大致时机适合做什么
ApplicationStartingEvent刚开始启动极少直接使用
ApplicationEnvironmentPreparedEventEnvironment 准备好观察配置、日志准备
ApplicationPreparedEventContext 准备好但未 refresh观察容器早期状态
ApplicationStartedEvent容器已刷新,Runner 未执行启动阶段统计
ApplicationReadyEventRunner 执行后,应用可服务注册实例、轻量预热
ApplicationFailedEvent启动失败告警、诊断日志

不要把所有初始化都放到一个事件里。要先判断你需要的资源是否已经准备好。

商业项目启动设计建议

不要在构造方法里访问外部系统

错误:

java
public HospitalClient() {
    remoteServer.ping();
}

后果:

  1. 外部系统慢会拖慢启动。
  2. 外部系统不可用会导致服务无法启动。
  3. 连接超时太长时排查困难。

更好的方式:

  1. 创建 Bean 时只设置配置。
  2. 健康检查负责判断依赖可用性。
  3. 必要预热放 Runner,并设置超时和降级。

字典预热不要无限阻塞

医疗采集平台常见启动动作:

  1. 加载医院字典。
  2. 恢复历史批次。
  3. 注册采集任务。
  4. 检查第三方接口。

这些都要有超时、日志、指标和失败策略。不能让一个医院接口慢,拖死整个服务启动。

启动慢怎么排查

mermaid
flowchart TD
    A["启动慢"] --> B["看日志卡在哪个阶段"]
    B --> C["Environment/配置中心"]
    B --> D["Bean 创建"]
    B --> E["WebServer 启动"]
    B --> F["Runner 执行"]
    C --> G["配置中心网络、超时、Profile"]
    D --> H["构造器、PostConstruct、慢SQL、外部调用"]
    E --> I["端口占用、Filter/Servlet初始化"]
    F --> J["缓存预热、字典加载、历史任务恢复"]

排查清单:

  1. 先看最后一条日志停在哪。
  2. 开启 debug=true 看自动配置报告。
  3. 看是否卡在配置中心拉取。
  4. 搜索项目里的 @PostConstructInitializingBeanApplicationRunner
  5. 查 Bean 构造方法里是否访问数据库、Redis、HTTP。
  6. 查连接池初始化超时。
  7. 查端口是否被占用。
  8. 接入 Actuator startup 端点或应用启动指标。
  9. 对启动后预热任务加耗时日志和超时。

常见启动失败

异常或现象常见原因排查方向
端口占用server.port 被占换端口或释放端口
Bean 找不到扫描路径不对、条件不满足启动类包位置、conditions
Bean 冲突多个同类型 Bean@Primary@Qualifier、条件装配
配置绑定失败类型错误、缺必填项/actuator/configprops、启动异常
数据源启动慢数据库不可达、超时长连接串、网络、连接池参数
自动配置没生效依赖缺失、exclude、配置开关debug=true、conditions

面试标准回答

Spring Boot 启动流程

Spring Boot 启动从 SpringApplication.run 开始,先创建 SpringApplication,推断应用类型,加载初始化器和监听器,然后准备 Environment、读取配置和 Profile,再根据应用类型创建 ApplicationContext。之后解析启动类、组件扫描和自动配置,把 BeanDefinition 注册到容器,执行 refresh() 创建非懒加载单例 Bean。Servlet Web 应用会在刷新过程中创建并启动内嵌 Tomcat,最后发布启动完成事件并执行 Runner。

refresh() 为什么重要

refresh() 是 Spring 容器真正启动的核心阶段,会准备 BeanFactory、执行 BeanFactoryPostProcessor、注册 BeanPostProcessor、初始化事件广播器、创建非懒加载单例 Bean。AOP、事务、生命周期回调、自动配置 Bean 的创建都离不开这个阶段。

内嵌 Tomcat 什么时候启动

Servlet Web 应用会创建 ServletWebServerApplicationContext,它在 refresh()onRefresh() 阶段调用 createWebServer(),通过 ServletWebServerFactory 创建 Tomcat,注册 Servlet、Filter、Listener,最后绑定端口并开始接收请求。

Runner 适合做什么

ApplicationRunnerCommandLineRunner 在容器启动完成后执行,适合做轻量缓存预热、启动检查、注册状态等。它们不适合放不可控长任务,否则会拖慢启动甚至导致启动失败。

启动慢怎么排查

先看日志卡在哪个阶段,再区分是配置中心、Bean 创建、WebServer 启动还是 Runner 执行。重点检查构造器、@PostConstruct、初始化方法、数据源连接、外部 HTTP 调用、配置中心、端口占用和启动后预热任务。可以配合 debug=true、Actuator startup、线程栈和耗时日志定位。

关联知识点