Skip to content

Spring Boot 启动原理 ​

很多人第一次学 Spring Boot 时会觉得神奇:为什么一个 main 方法就能把整个项目跑起来?

这一章我们就专门把启动过程拆开。

如果要深入理解 SpringApplication.run、应用类型推断、Environment、ApplicationContext、自动配置导入、refresh()、内嵌 Tomcat、Runner、启动事件和启动慢排查,建议继续看:Spring Boot启动流程全过程原理。

从入口开始看 ​

标准启动代码通常是这样:

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

真正的重点在 SpringApplication.run(...)。

启动流程总览 ​

mermaid
flowchart TD
    A["调用 SpringApplication.run"] --> B["推断应用类型"]
    B --> C["加载 ApplicationContext"]
    C --> D["准备 Environment"]
    D --> E["读取配置文件"]
    E --> F["打印 Banner"]
    F --> G["创建并刷新容器"]
    G --> H["触发自动配置"]
    H --> I["创建 Web 容器"]
    I --> J["注册 DispatcherServlet"]
    J --> K["应用启动完成"]

你可以把它理解成“先准备环境,再创建容器,再把 Web 能力挂上去”。

第一步:推断应用类型 ​

Spring Boot 会根据类路径里的依赖判断当前应用属于哪一种:

  1. NONE:不是 Web 应用。
  2. SERVLET:传统 Servlet Web 应用,最常见。
  3. REACTIVE:响应式 Web 应用。

如果项目里有 spring-webmvc,通常会判断为 SERVLET。

第二步:准备运行环境 ​

Environment 可以理解成“配置的总入口”,里面会收集很多配置来源:

  1. 命令行参数。
  2. 系统环境变量。
  3. JVM 参数。
  4. application.yml
  5. application-{profile}.yml

这一步完成后,Spring Boot 才知道端口、上下文路径、数据库地址等信息。

第三步:创建容器 ​

不同应用类型,会选择不同的容器实现。

例如 Servlet 应用通常会使用:

text
AnnotationConfigServletWebServerApplicationContext

这个容器除了普通 Spring 容器能力,还额外支持 Web 服务器相关功能。

第四步:刷新容器 ​

刷新容器是 Spring 启动中的核心阶段。

这一步会完成很多关键动作:

  1. 扫描组件。
  2. 注册 BeanDefinition。
  3. 实例化单例 Bean。
  4. 处理依赖注入。
  5. 处理各种后置处理器。
  6. 启动内嵌服务器。

自动配置是怎么生效的 ​

本节先放在启动流程里帮助你定位自动配置发生的位置。完整自动配置链路、Boot 2/Boot 3 声明文件区别、条件注解、排查方法和自定义 Demo,看 Spring Boot 自动配置原理。

@EnableAutoConfiguration ​

它会触发自动配置机制。

自动配置并不是凭空猜,而是通过读取一批预定义配置类,再根据条件判断是否加载。

条件注解的作用 ​

自动配置类里经常能看到这些注解:

  1. @ConditionalOnClass
  2. @ConditionalOnMissingBean
  3. @ConditionalOnProperty
  4. @ConditionalOnWebApplication

它们的意思分别类似于:

  1. 类路径里有某个类才生效。
  2. 容器里没有某个 Bean 才生效。
  3. 配置项满足条件才生效。
  4. 必须是 Web 应用才生效。

也就是说,Spring Boot 不是硬塞配置,而是“条件满足再装配”。

自动配置完整链路 ​

自动配置不是“启动时到处扫描 jar 包”。它有一条非常清晰的链路:

text
@SpringBootApplication
  -> @EnableAutoConfiguration
  -> @Import(AutoConfigurationImportSelector.class)
  -> 读取候选自动配置类
  -> 条件过滤
  -> 注册 BeanDefinition
  -> refresh 阶段创建 Bean
mermaid
flowchart TD
    A["启动类有 @SpringBootApplication"] --> B["组合注解里包含 @EnableAutoConfiguration"]
    B --> C["@Import 导入 AutoConfigurationImportSelector"]
    C --> D["读取自动配置候选名单"]
    D --> E["Boot 3: AutoConfiguration.imports"]
    D --> F["Boot 2: spring.factories"]
    E --> G["去重、排除 exclude、条件预过滤"]
    F --> G
    G --> H["把自动配置类交给 Spring 解析"]
    H --> I{"条件注解是否满足"}
    I -- "否" --> J["跳过"]
    I -- "是" --> K["注册 BeanDefinition"]
    K --> L["容器刷新时创建 Bean"]

几个点一定要分清:

  1. @EnableAutoConfiguration 自己不直接创建 Bean。
  2. AutoConfigurationImportSelector 负责找候选自动配置类。
  3. 候选类不是全部生效,还要过条件注解。
  4. 最终仍然回到 Spring IOC:注册 BeanDefinition,再创建 Bean。

@SpringBootApplication 三件事 ​

注解作用如果没有会怎样
@SpringBootConfiguration表示启动类也是配置类主启动类不能作为配置源
@ComponentScan扫描业务组件Controller、Service 可能进不了容器
@EnableAutoConfiguration导入自动配置候选类Web、JSON、数据源等默认能力不会自动装配

这也解释了为什么启动类不要随便放在很深的子包里。默认组件扫描从启动类所在包开始向下扫描,如果启动类放错位置,业务 Bean 可能根本扫描不到。

mermaid
flowchart TD
    A["com.company.Application"] --> B["扫描 com.company.*"]
    B --> C["controller/service/repository 能扫到"]
    D["com.company.module.Application"] --> E["只扫描 com.company.module.*"]
    E --> F["com.company.common 下的 Bean 可能扫不到"]

SpringApplication.run() 到底做了什么 ​

可以按“准备环境 -> 创建容器 -> 刷新容器 -> 启动完成”四段理解。

阶段关键动作为什么重要
创建 SpringApplication推断应用类型、加载初始化器和监听器决定用普通容器还是 Web 容器
准备 Environment读取命令行、环境变量、配置文件、Profile后续自动配置要读取配置
创建 ApplicationContext根据应用类型选择上下文实现Servlet 应用会用 WebServer 容器
refresh()注册 BeanDefinition、实例化单例 Bean、执行后置处理器Spring 核心启动流程
启动 WebServer创建 Tomcat/Jetty/Undertow,绑定端口Web 服务开始接请求
发布事件ApplicationStartedEvent、ApplicationReadyEvent 等监控、预热、初始化扩展

更完整的流程:

mermaid
flowchart TD
    A["main 方法调用 run"] --> B["创建 SpringApplication"]
    B --> C["推断 WebApplicationType"]
    C --> D["准备监听器和初始化器"]
    D --> E["准备 Environment 和 Profile"]
    E --> F["创建 ApplicationContext"]
    F --> G["加载启动类和自动配置类"]
    G --> H["refresh 容器"]
    H --> I["执行 BeanFactoryPostProcessor"]
    I --> J["注册 BeanPostProcessor"]
    J --> K["实例化非懒加载单例 Bean"]
    K --> L["启动内嵌 WebServer"]
    L --> M["执行 Runner"]
    M --> N["发布启动完成事件"]

refresh() 为什么是核心 ​

refresh() 是 Spring 容器真正“活起来”的阶段。很多自动配置看起来是 Boot 做的,但最终仍然是在 refresh() 中注册、解析、创建 Bean。

你可以重点记这几类动作:

  1. 准备 BeanFactory。
  2. 执行 BeanFactoryPostProcessor,允许修改 BeanDefinition。
  3. 注册 BeanPostProcessor,为生命周期和 AOP 做准备。
  4. 初始化消息源、事件广播器。
  5. 对 Web 应用创建并启动 WebServer。
  6. 实例化所有非懒加载单例 Bean。
  7. 发布容器刷新完成事件。

如果某个项目启动慢,通常要从这几个方向排查:

现象可能原因
卡在创建 Bean构造器、@PostConstruct、初始化方法里有慢逻辑
卡在数据源数据库连接不可用或连接超时太长
卡在配置中心配置拉取慢或网络不通
卡在类扫描包扫描范围过大、反射扫描太多
卡在 WebServer端口占用、Servlet/Filter 初始化慢

为什么能少写 XML ​

传统项目里,很多内容要手工在 XML 中声明。

而 Spring Boot 做了两件事:

  1. 用注解替代大量 XML。
  2. 用自动配置补齐通用场景。

所以开发者只需要写差异化配置,而不需要重复写基础配置。

内嵌 Tomcat 是什么时候启动的 ​

它通常发生在容器刷新阶段。

流程可以简单理解为:

  1. Boot 发现当前是 Servlet Web 应用。
  2. 容器中存在 WebServerFactory。
  3. 刷新时创建 Tomcat。
  4. 绑定端口。
  5. 注册 Servlet、Filter、Listener。
  6. 开始接收请求。
mermaid
flowchart TD
    A["启动类调用 run"] --> B["Boot 创建并刷新 ApplicationContext"]
    B --> C["容器解析 BeanDefinition"]
    C --> D["创建非懒加载单例 Bean"]
    D --> E["创建内嵌 Tomcat WebServer"]
    E --> F["注册 Servlet、Filter 和 Listener"]
    F --> G["Tomcat 绑定端口"]
    G --> H["Boot 发布启动完成事件"]

Tomcat 是谁创建的 ​

Servlet Web 应用中,Spring Boot 通常会创建 ServletWebServerApplicationContext。它在 refresh() 过程中会调用自己的 WebServer 创建逻辑,通过容器里的 ServletWebServerFactory 创建 Tomcat。

简化链路是:

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

对应到 Bean:

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

请求进来后的链路是:

mermaid
flowchart TD
    A["浏览器或调用方"] --> B["Tomcat Connector"]
    B --> C["Filter 链"]
    C --> D["DispatcherServlet"]
    D --> E["HandlerMapping 找 Controller"]
    E --> F["HandlerAdapter 调用方法"]
    F --> G["HttpMessageConverter 写响应"]
    G --> H["返回客户端"]

所以“内嵌 Tomcat”不是把外部 Tomcat 复制进代码里,而是应用启动时在同一个 JVM 里创建并启动一个 Servlet 容器。

引入 starter-web 就一定是 Tomcat 吗 ​

默认情况下,spring-boot-starter-web 会引入 Tomcat。但可以排除 Tomcat 并替换为 Jetty 或 Undertow。

Maven 示例:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

如果是 WebFlux,默认栈通常是 Reactor Netty,不是 Spring MVC + Tomcat 这条 Servlet 链路。

application.yml 为什么能自动生效 ​

因为 Spring Boot 在启动早期就会加载配置文件,并把内容放进 Environment。

随后,无论是:

  1. 自动配置类读取属性。
  2. @Value 注入属性。
  3. @ConfigurationProperties 绑定属性。

本质上都在读 Environment 中的数据。

常见启动扩展点 ​

CommandLineRunner ​

项目启动完成后执行,适合做初始化任务。

ApplicationRunner ​

和 CommandLineRunner 类似,但参数处理更友好。

监听启动事件 ​

可以监听:

  1. 容器准备完成。
  2. 容器刷新完成。
  3. 应用启动完成。
  4. 应用启动失败。

这样我们就能在不同阶段做日志、监控、预热等工作。

生产排查:启动慢怎么定位 ​

不要只看“应用启动用了 90 秒”这种总耗时,要拆阶段。

打开自动配置报告 ​

启动时加:

bash
java -jar app.jar --debug

可以看到哪些自动配置生效、哪些没生效,以及条件判断原因。排查“为什么某个 Bean 没有自动配置出来”时很有用。

使用 Actuator 查看启动阶段 ​

如果接入 Actuator,可以关注 startup、health、metrics、threaddump 等端点。生产环境不要无权限暴露敏感端点。

常见修复方向 ​

问题修复
初始化方法访问慢外部系统改成启动后异步预热,或设置短超时
Bean 构造器做大量计算延迟到业务需要时,或缓存结果
包扫描过大调整启动类位置和扫描范围
配置中心不可用导致等待配置超时、降级默认值、启动失败策略
端口被占用修改 server.port 或释放端口

医疗采集平台里,医院接口探活、字典预热、历史批次恢复这些逻辑不要全部阻塞在 Bean 初始化里。核心服务可以先启动,再由 Runner 或后台任务按可观测、可超时、可重试的方式执行。

小白容易踩的坑 ​

1. 启动类放错包 ​

默认组件扫描是从启动类所在包开始往下扫。

如果启动类放得太深或太偏,可能扫不到 Controller、Service。

2. 以为所有 Bean 都是自动生成 ​

很多核心组件确实由 Boot 帮你创建,但业务 Bean 仍然需要你自己写。

3. 不理解“覆盖默认配置” ​

Spring Boot 有默认值,但不代表不能改。

只要你显式声明自己的配置或自己的 Bean,很多默认行为都可以被覆盖。

本章小结 ​

Spring Boot 启动原理可以压缩成一句话:

先准备配置环境,再创建并刷新 Spring 容器,最后在条件满足时自动装配 Web 基础设施并启动内嵌服务器。

如果你能把这句话真正理解掉,后面学习自动配置、Starter、自定义扩展时就不会只停留在“会用”的层面。