Skip to content

Spring Boot 启动流程(从 main 到 Tomcat 启动)

问题

Spring Boot 应用从 main 方法开始到 Tomcat 启动完成,经历了哪些关键步骤?面试官问这个问题的意图不是让你背流程,而是考察你对 Spring 容器生命周期 + 嵌入式 Web 容器的整合时机 的理解。

三段式分解

Spring Boot 启动流程可以拆成 三大阶段:初始化阶段、准备阶段、刷新阶段。下面逐段拆开来看,每个阶段都附时序图和常见坑。

整体时序图(文字描述)

[main]→SpringApplication.run()

  ├─① 初始化阶段
  │   ├─ new SpringApplication() → 加载 spring.factories、推断 Web 类型、定位主类
  │   └─ run() 实例方法 → 开始计时

  ├─② 准备阶段
  │   ├─ prepareEnvironment() → 合并配置源(命令行 > 环境变量 > yml)
  │   ├─ printBanner()
  │   ├─ createApplicationContext() → 按 Web 类型创建 Context
  │   ├─ prepareContext() → initializer 执行、Environment 注入
  │   └─ 包扫描 → 注册 @Component 等 bean 定义

  └─③ 刷新阶段(refresh() 13 步)
      ├─ 1-8: 准备 BeanFactory、BeanPostProcessor、MessageSource、事件广播器
      ├─ 9: onRefresh() → **Tomcat 在此启动,端口开放**
      ├─ 10: registerListeners()
      ├─ 11: finishBeanFactoryInitialization() → **实例化所有单例 bean(最耗时)**
      └─ 12: finishRefresh() → 发布 ContextRefreshedEvent,容器就绪

注意:第 9 步和第 11 步之间存在一个危险的时间窗口——Tomcat 端口已经打开,但大多数 bean 还没实例化。下文会详细讲这个坑。

第一阶段:初始化阶段

入口是 SpringApplication.run(Class<?> primarySource, String... args)。这个静态方法内部做了两件事:

1. 创建 SpringApplication 实例

java
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return new SpringApplication(primarySource).run(args);
}

new SpringApplication() 的构造方法干了三件事:

  • spring.factories 加载 SPI 实现:通过 SpringFactoriesLoader.loadFactoryNames() 读取所有 META-INF/spring.factories 文件,找到 ApplicationContextInitializerApplicationListener 的全限定类名,反射实例化后缓存到 initializerslisteners 字段。

    java
    // 实际加载逻辑(简化)
    List<String> initializerNames = SpringFactoriesLoader.loadFactoryNames(
        ApplicationContextInitializer.class, classLoader);
    for (String name : initializerNames) {
        this.initializers.add((ApplicationContextInitializer<?>) 
            ClassUtils.forName(name, classLoader).newInstance());
    }

    踩坑:自定义 starter 时如果 spring.factories 配置了 ApplicationContextInitializer 但类不存在,Spring Boot 不会直接报错,而是把异常吞掉(ClassNotFoundException 被 catch 后打 debug 日志),导致 initializer 静默失效。排查手法:加 --debug 参数看日志。

  • 推断 Web 应用类型:通过 ClassUtils 检查 Classpath 中是否存在 javax.servlet.Servlet(或 Jakarta Servlet)和 org.springframework.web.reactive.DispatcherHandler。结果有三种:

    • SERVLET:传统 Servlet 容器(Tomcat、Jetty、Undertow)
    • REACTIVE:WebFlux(Netty)
    • NONE:非 Web 应用(如纯命令行工具)
    java
    private WebApplicationType deduceWebApplicationType() {
        if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", null)
            && !ClassUtils.isPresent("javax.servlet.Servlet", null)) {
            return WebApplicationType.REACTIVE;
        }
        for (String className : SERVLET_INDICATOR_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return WebApplicationType.NONE;
            }
        }
        return WebApplicationType.SERVLET;
    }

    注意:如果 Classpath 同时存在 Servlet 和 WebFlux 依赖,Spring Boot 优先选择 REACTIVE。也就是说,如果你在 pom.xml 里带了 spring-boot-starter-webspring-boot-starter-webflux,默认会启动 WebFlux 而不是 Tomcat。很多人踩过这个坑。

  • 定位主启动类:抛出 new RuntimeException() 获取调用栈,找到栈里包含 main 方法的那个类。这是 Spring Boot 判断包扫描起点的依据。

2. 调用 run() 实例方法

java
public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();  // 启动计时器
    
    // 1. 配置无头模式(headless)
    configureHeadlessProperty();
    
    // 2. 获取并启动 SpringApplicationRunListeners
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    
    // 3. 进入准备阶段...
    // ...(后续步骤)
}

SpringApplicationRunListener 是一种 SPI 扩展点,允许你在启动流程的不同阶段插入自定义逻辑。默认只有 EventPublishingRunListener 一个实现,它把事件发布给 ApplicationListener 们。

第二阶段:准备阶段

1. prepareEnvironment():加载配置源

这一步的核心是构建 ConfigurableEnvironment,它内部维护了一个 MutablePropertySources 列表。配置来源按优先级从高到低排列:

优先级配置源示例
最高命令行参数--server.port=8081
JNDI 属性java:comp/env/
系统环境变量SERVER_PORT=8081
application-{profile}.ymlapplication-dev.yml
application.yml基础配置
最低默认属性spring.main.* 默认值
java
// 实际合并逻辑
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 此时 environment.getProperty("server.port") 返回 8081(如果命令行指定了)

踩坑:多个 Profile 的配置合并规则是 后加载的覆盖先加载的,而不是 Profile 优先级。application-dev.yml 如果在 application.yml 之后加载,就覆盖;如果 Spring Boot 2.x 的 spring.profiles.includespring.profiles.group 配置了顺序,要注意覆盖顺序。

2. 打印 Banner

默认输出 Spring Boot ASCII 字样。可以通过 banner.txt 自定义,或者通过 spring.banner.location 指定文件位置。少数公司会用它来输出启动版本号和环境信息,方便运维确认。

3. 创建 ApplicationContext

根据 Web 类型创建不同的 ApplicationContext 实现:

Web 类型ApplicationContext 实现
SERVLETAnnotationConfigServletWebServerApplicationContext
REACTIVEAnnotationConfigReactiveWebServerApplicationContext
NONEAnnotationConfigApplicationContext

4. 调用 ApplicationContextInitializer

之前从 spring.factories 加载的 initializer 依次执行。每个 initializer 接收 ConfigurableApplicationContext 入参,可以修改 BeanFactoryPostProcessor、添加 PropertySource、注册自定义组件等。

常用的几个默认 initializer:

  • ContextIdApplicationContextInitializer:设置 Context ID(spring.application.name
  • ConfigurationWarningsApplicationContextInitializer:检查常见的配置错误
  • ServerPortInfoApplicationContextInitializer:把实际端口写入 local.server.port 属性,用于测试

5. 注入 Environment

这一步不仅是把 Environment 设置到 ApplicationContext,还会触发 ConversionService 的初始化——把字符串配置值转换成目标类型(如 StringDurationStringDataSize)。

6. 包扫描

调用 ClassPathBeanDefinitionScanner 扫描主启动类所在包及其子包,把 @Component@Service@Repository@Controller@Configuration 等 bean 定义注册到 BeanDefinitionRegistry

java
// 包扫描的实际入口
scanner.scan(basePackage);
// 如果主类是 com.example.demo.Application,则扫描 com.example.demo 及其子包

踩坑:主类的包路径如果和配置文件不在同级目录,会导致 @ComponentScan 扫描不到。很多人把主类放在 com.example 顶层,但 @Configuration 放在 com.example.config 子包,这就没问题。但如果 @Service 放在 com.other 包,就扫描不到了。解决方案是显式加 @ComponentScan("com.other")

第三阶段:刷新阶段(核心)

refresh()AbstractApplicationContext 的模板方法,执行 13 个关键步骤。

java
@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // 1. 准备刷新上下文
        prepareRefresh();
        // 2. 获取新的 BeanFactory
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        // 3. 准备 BeanFactory
        prepareBeanFactory(beanFactory);
        // 4. BeanFactory 后置处理
        postProcessBeanFactory(beanFactory);
        // 5. 调用 BeanFactoryPostProcessor
        invokeBeanFactoryPostProcessors(beanFactory);
        // 6. 注册 BeanPostProcessor
        registerBeanPostProcessors(beanFactory);
        // 7. 初始化 MessageSource
        initMessageSource();
        // 8. 初始化事件广播器
        initApplicationEventMulticaster();
        // 9. **onRefresh —— 嵌入式 Web 容器创建**
        onRefresh();
        // 10. 注册监听器
        registerListeners();
        // 11. 实例化所有非懒加载单例 bean
        finishBeanFactoryInitialization(beanFactory);
        // 12. 完成刷新
        finishRefresh();
    }
}

关键步骤详解

步骤 1-4:准备阶段(非 Spring Boot 特有)

  • prepareRefresh():设置启动时间戳、初始化占位符解析、初始化 earlyApplicationEvents 集合
  • obtainFreshBeanFactory():刷新 BeanFactory,销毁旧的 singleton(如果有),创建一个新的 DefaultListableBeanFactory
  • prepareBeanFactory():设置类加载器、注册 BeanPostProcessor 检测器、注册 ApplicationContextAwareProcessor(负责回调 Aware 接口)、注册 EnvironmentSystem PropertiesSystem Environment 等单例 bean
  • postProcessBeanFactory():空方法,留给子类覆盖。ServletWebServerApplicationContext 在这里注册 ServletContextAwareProcessor

步骤 5-6:核心处理

  • invokeBeanFactoryPostProcessors()这是 @Configuration@Bean 被解析的地方ConfigurationClassPostProcessorBeanFactoryPostProcessor 的实现)在这里解析所有 @Configuration 类,处理 @Bean@Import@ComponentScan@PropertySource 等注解。这是 Spring Boot 自动配置 生效的关键步骤——@EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 在这里被处理,加载 spring.factories 中的 EnableAutoConfiguration 配置类。

  • registerBeanPostProcessors():注册所有 BeanPostProcessor 实现类,用于后续 bean 实例化时的拦截处理。AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 等都在这里注册。

步骤 7-8:基础设施

  • initMessageSource():创建 MessageSource bean,用于国际化
  • initApplicationEventMulticaster():创建 SimpleApplicationEventMulticaster,用于管理事件监听器

onRefresh() —— 嵌入式 Tomcat 启动(核心考点)

onRefresh()ServletWebServerApplicationContext 中被重写,调用 createWebServer()。具体流程:

java
// ServletWebServerApplicationContext.createWebServer() 简化逻辑
private void createWebServer() {
    ServletWebServerFactory factory = getBeanFactory().getBean(ServletWebServerFactory.class);
    // 默认是 TomcatServletWebServerFactory
    // 如果导入了 spring-boot-starter-web,自动装配会创建它
    
    this.webServer = factory.getWebServer(servletContext -> {
        // 注册 DispatcherServlet(核心)
        servletContext.addServlet("dispatcherServlet", 
            new DispatcherServlet(applicationContext));
        
        // 注册 Filter(如果有)
        // 注册 ServletContextListener(如果有)
    });
    
    // 此处 Tomcat 已经启动,端口监听
    this.webServer.start();
}

Tomcat 启动的详细信息

  1. 从容器中获取 ServletWebServerFactory 实例。默认是 TomcatServletWebServerFactory,由 ServletWebServerFactoryAutoConfiguration 自动装配。
  2. 调用 factory.getWebServer(getSelfInitializer())
    • 创建 Tomcat 实例(new Tomcat()
    • 创建 StandardServerStandardServiceConnector
    • 配置 Connector:端口(server.port,默认 8080)、协议(org.apache.coyote.http11.Http11NioProtocol)、连接器参数(server.tomcat.max-connectionsserver.tomcat.accept-countserver.tomcat.max-threads 等)
    • 创建 Context(StandardContext),设置上下文路径(server.servlet.context-path,默认空)
    • 注册 TomcatServletWebServerFactory 中配置的 TomcatContextCustomizer(如压缩、编码等)
    • 返回 TomcatWebServer 实例
  3. TomcatWebServer.start()
    • 调用 Tomcat.getConnector() 启动 Connector 线程
    • 调用 Connector.start() 启动 NIO 端点
    • 绑定端口,开始监听

关键时间窗口onRefresh() 执行后,finishBeanFactoryInitialization() 执行前,Tomcat 8080 端口已经在监听,但大部分业务 bean 还没实例化。此时如果请求进来,可能出现:

  • DispatcherServlet 虽然注册了,但内部的 WebApplicationContext 还没完全初始化,请求处理异常
  • 依赖的 @Service@Repository 等 bean 还没创建,处理请求时抛出 NoSuchBeanDefinitionException
  • 健康检查端点(/actuator/health)返回 503 或 404

生产环境配置:使用 Spring Boot Actuator 的 /actuator/health/readiness 端点 + K8s 的 readinessProbe,让负载均衡器只在 readiness 返回 200 后才转发流量。

yaml
# application.yml - 健康检查配置
management:
  endpoints:
    web:
      exposure.include: health,info,readiness,liveness
  endpoint:
    health:
      show-details: always
    readiness:
      enabled: true
    liveness:
      enabled: true

finishBeanFactoryInitialization() —— 实例化所有 bean

这一步实例化所有非懒加载的单例 bean。这是容器启动最耗时的阶段(通常占 60%-80% 的启动时间)。

java
// AbstractApplicationContext.finishBeanFactoryInitialization() 简化
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    // 初始化 ConversionService(如果有)
    if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME)) {
        beanFactory.setConversionService(
            beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
    }
    
    // 实例化所有非懒加载单例 bean
    beanFactory.preInstantiateSingletons();
}

DefaultListableBeanFactory.preInstantiateSingletons() 遍历所有 bean 定义,逐个创建。每个 bean 实例化要经过:

  1. 反射创建实例(InstantiationAwareBeanPostProcessor 可能干涉)
  2. 属性注入(populateBean)——处理循环依赖(三级缓存:singletonFactoriesearlySingletonObjectssingletonObjects
  3. Aware 回调(BeanNameAwareBeanFactoryAwareApplicationContextAware
  4. BeanPostProcessor#postProcessBeforeInitialization
  5. 初始化回调(@PostConstructInitializingBean#afterPropertiesSet@Bean(initMethod=...))
  6. BeanPostProcessor#postProcessAfterInitialization

耗时分析(真实生产数据):

阶段耗时占比典型耗时
包扫描 + bean 定义注册5-10%0.3-0.8s
BeanFactoryPostProcessor 执行10-15%0.5-1.5s
BeanPostProcessor 注册<1%<0.1s
onRefresh (Tomcat 启动)5-10%0.3-0.8s
finishBeanFactoryInitialization60-80%3-15s
finishRefresh<1%<0.1s

数据来源:某电商平台 Spring Boot 应用(300+ bean 定义,4C8G 实例)的实际启动耗时统计。如果项目有 500+ bean,启动时间可能超过 30s。

finishRefresh() —— 发布事件

java
protected void finishRefresh() {
    // 清理资源缓存
    clearResourceCaches();
    // 初始化生命周期处理器
    initLifecycleProcessor();
    // 启动所有 SmartLifecycle bean
    getLifecycleProcessor().onRefresh();
    // 发布 ContextRefreshedEvent
    publishEvent(new ContextRefreshedEvent(this));
    // 注册 LiveBeansView(用于 JMX 监控)
    LiveBeansView.registerApplicationContext(this);
}

ContextRefreshedEvent 的到来意味着容器完全就绪,所有 bean 都已可用。

启动过程中的常见坑

坑 1:Tomcat 启动后、bean 初始化完前的请求

如前面所述,onRefresh() 启动 Tomcat 后,到 finishBeanFactoryInitialization() 完成之间有时间窗口。应急方案:

java
@Component
public class ReadinessFilter implements Filter {
    private volatile boolean ready = false;
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        if (!ready) {
            response.setStatus(503);
            return;
        }
        chain.doFilter(request, response);
    }
    
    // 通过 ApplicationListener<ContextRefreshedEvent> 设置 ready
    @EventListener(ContextRefreshedEvent.class)
    public void onReady() {
        this.ready = true;
    }
}

坑 2:启动慢导致健康检查超时

K8s 的 initialDelaySeconds 不够大时,Spring Boot 启动慢会触发 livenessProbe 杀死 Pod。排查手法:

  1. -Dspring.profiles.active=dev 减少自动配置加载
  2. 使用 spring-boot-starter-actuator 的 liveness 端点:/actuator/health/liveness
  3. 配置合理的 startupProbe
yaml
startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 6

坑 3:@ConditionalOnMissingBean 导致的 bean 覆盖

自动配置类被加载后,如果用户自定义的同类型 bean 优先级问题,可能导致自动配置的 bean 被覆盖或者不生效。排查方式:--debug 启动看 CONDITIONS EVALUATION REPORT

坑 4:Spring Boot 2.x vs 3.x 启动流程差异

Spring Boot 3.x(基于 Spring Framework 6 + Jakarta EE 9)在启动流程上有几个关键变化:

对比项Spring Boot 2.xSpring Boot 3.x
Servlet APIjavax.servletjakarta.servlet
自动配置加载spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
默认 Web 服务器Tomcat 9Tomcat 10
refresh() 步骤13 步13 步(核心不变)
GraalVM 支持实验性正式支持(AOT 编译)

实际迁移踩坑:从 SB 2.x 迁到 3.x 时,最隐蔽的问题不是 javaxjakarta 包名替换(IDEA 重构能搞定),而是:

  • 第三方 starter 依赖的 spring.factories 自动配置在 3.x 下不再被加载——必须迁移到 AutoConfiguration.imports 文件格式
  • RestTemplate 默认不再使用 Apache HttpClient,改为 java.net.http.HttpClient,连接池行为变了
  • 某些 @ConfigurationProperties 绑定行为在 3.x 下更严格,宽松绑定(relaxed binding)规则收窄

启动优化实战

在实际生产环境,600+ bean 的应用启动时间达到 45s 很常见。以下是实测有效的优化手段:

1. 懒加载非关键 bean

yaml
spring:
  main:
    lazy-initialization: true  # 全局懒加载,启动时间从 45s 降到 8s

代价:首次请求响应变慢(约 200-500ms 延迟),因为用到时才初始化。适合对冷启动首请求不敏感的后端服务。

2. 按需排除自动配置

java
@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,       // 如果不用数据库
    QuartzAutoConfiguration.class,           // 如果不用定时任务
    JmxAutoConfiguration.class               // 如果不配 JMX
})

实测:排除 4 个不必要的自动配置,减少 0.8-1.2s 的 BeanFactoryPostProcessor 执行时间。

3. 使用 Spring Boot 3.x AOT 编译

xml
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <excludes>
            <exclude>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </exclude>
        </excludes>
    </configuration>
</plugin>

AOT 编译在构建期解析所有配置,生成运行时所需的类信息,避免运行时反射开销。Spring Boot 3.x 原生镜像启动时间可以压缩到 200ms 以内

总结

Spring Boot 启动流程可以概括为三个关键点:

  1. 初始化阶段:创建 SpringApplication,加载 initializer 和 listener,推断 Web 类型
  2. 准备阶段:加载配置、创建 ApplicationContext、注入 Environment、扫描包
  3. 刷新阶段:执行 refresh() 13 步,其中 onRefresh() 创建嵌入式 Tomcat,finishBeanFactoryInitialization() 实例化所有单例 bean

面试进阶技巧

  • 不要只背流程,要说清楚 Tomcat 启动和 bean 初始化之间的时间差——Tomcat 在 onRefresh() 阶段就启动了,此时 bean 还没全部可用,所以生产环境需要配合健康检查(如 Spring Boot Actuator 的 /actuator/health/readiness)来告诉负载均衡器什么时候可以转发流量
  • 要能说出 refresh() 中哪几步最耗时(finishBeanFactoryInitialization() 占 60-80%)
  • 要能描述自动配置(AutoConfigurationImportSelector)在 invokeBeanFactoryPostProcessors() 时的加载时机
  • 如果能说出 Spring Boot 2.x 到 3.x 的启动流程差异(spring.factoriesAutoConfiguration.imports、Jakarta 迁移),就是加分项
  • 如果能给出启动优化的实战数据(懒加载从 45s 降到 8s、AOT 原生镜像 200ms 以内),说明你真的在生产环境折腾过

参考资料

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。