SPRING BOOT · SPRINGAPPLICATION.RUN · REFRESH

一行 run,背后走了四段路

从 main 方法到“Started … in 2.3 seconds”,Spring Boot 先认清自己是什么应用,再读配置、建容器,然后在 refresh 里把所有 Bean 和 Tomcat 都造出来,最后跑 Runner、宣布就绪。

SpringApplication.run(ShopApplication.class, args);
① 造 SpringApplication
  • 判断是不是 Web 应用
  • 读扩展名单(spring.factories)
② 准备环境和空容器
  • 读命令行、环境变量、application.yml
  • 打 banner,建一个空容器,放进主类
③ refresh:最重的一段
  • 扫描 + 自动配置,得到几百张 Bean 图纸
  • 建 Tomcat(先不开门)
  • 造出所有单例 Bean,最后打开端口
④ 收尾
  • 打印 Started
  • 跑 Runner
  • 宣布就绪
总指挥SpringApplication

run() 就是它的一张启动清单,按顺序一步步做。

Spring Boot 独有
环境Environment

所有配置排好优先级放在一起:命令行压过环境变量,环境变量压过 yml。

建容器之前就准备好
容器ApplicationContext

装所有 Bean 的工厂。refresh() 这一步把它从空壳变成能用。

Web 应用用 ServletWebServer 版本
内嵌 TomcatWebServer

refresh 中途就建好了,但要等 所有 Bean 造完才绑定端口。

不用再单独部署 war 包
配角 广播员 每到一个节点就发一个事件 医生 FailureAnalyzer,启动失败时写诊断报告 Runner ApplicationRunner、CommandLineRunner,启动完跑一次

run() 的骨架

Spring Boot 3 的源码去掉计时和细节后,就剩下面这些。注释里的编号,对应下面时间线上的步骤。

SpringApplication简化版
new SpringApplication(ShopApplication.class)          // 1 判断应用类型,读 spring.factories 里的扩展
        .run(args);

public ConfigurableApplicationContext run(String... args) {
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, mainApplicationClass);              // 2 ApplicationStartingEvent
    try {
        ConfigurableEnvironment env = prepareEnvironment(listeners, …);   // 3 读配置
        Banner banner = printBanner(env);                                   // 4
        context = createApplicationContext();                               // 5 一个空容器
        prepareContext(bootstrapContext, context, env, listeners, …);       // 6 跑 Initializer,放进主类
        refreshContext(context);                                            // 7 ~ 11 都在这一行里
        afterRefresh(context, arguments);                                   // 空方法,留给子类
        // 12 打印 "Started ShopApplication in … seconds"
        listeners.started(context, timeTakenToStartup);                     // 12 ApplicationStartedEvent
        callRunners(context, arguments);                                    // 13 ApplicationRunner / CommandLineRunner
    } catch (Throwable ex) {
        throw handleRunFailure(context, ex, listeners);   // 失败:发 ApplicationFailedEvent,FailureAnalyzer 写报告
    }
    listeners.ready(context, timeTakenToReady);                             // 14 ApplicationReadyEvent
    return context;
}

跟着一次启动走一遍

一个普通的 Web 项目。每一步看:这时已经有了什么,发了哪个事件,你能在这里插手做什么,日志里多了哪几行。也可以看看启动失败时,卡在了哪一步。

这时已经有了什么

这一步发出的事件
你能在这里插手
控制台(INFO 级别,省略了时间和类名)黄色是这一步新打的
1/14

表里 Bean 的个数是一个普通示例项目里的量级,不同项目差别很大。

refresh() 的十二步

这是 Spring 框架本身的方法,不是 Boot 加的。Boot 只是选了一个能内嵌 Tomcat 的容器,再在第 9 步里把 Tomcat 建出来。标了颜色的五步最值得记。

1prepareRefresh记下启动时间,检查必填配置
2obtainFreshBeanFactory拿到真正装 Bean 的 BeanFactory
3prepareBeanFactory放进类加载器、几个内置 Bean
4postProcessBeanFactoryWeb 容器在这里加点自己的东西
5invokeBeanFactoryPostProcessors扫描 @Component、解析 @Configuration、跑自动配置,得到所有图纸(第 03 篇)
6registerBeanPostProcessors先把改装师们造出来,后面每个 Bean 都要过他们的手
7initMessageSource国际化消息
8initApplicationEventMulticaster事件广播器
9onRefreshBoot 在这里建 Tomcat,装好 Filter 和 DispatcherServlet,但先不绑定端口
10registerListeners登记监听器
11finishBeanFactoryInitialization造出所有非懒加载的单例,每个都走一遍生命周期(第 04 篇)
12finishRefresh启动 SmartLifecycle,Tomcat 在这里绑定端口,发 ContextRefreshedEvent
为什么端口最后才开如果 Tomcat 一建好就接请求,请求可能打到还没造完的 Bean 上。所以 Boot 在 refresh 的第 9 步只建好 Tomcat,把连接器摘下来,等到第 12 步所有 Bean 都好了,再装回去开始监听。

启动过程中的事件,按顺序

前四个事件发出时,容器里的 Bean 还没造出来,写在 Bean 上的 @EventListener 根本收不到。

ApplicationStartingEventrun() 刚开始,什么都还没有@EventListener 收不到
ApplicationEnvironmentPreparedEvent配置读完了,容器还没建收不到
ApplicationContextInitializedEvent空容器建好,Initializer 跑完收不到
ApplicationPreparedEvent主类的图纸放进去了,refresh 之前收不到
WebServerInitializedEventrefresh 最后一步,端口刚绑定收得到
ContextRefreshedEventrefresh 完成收得到
ApplicationStartedEvent打印 Started 之后,Runner 之前;同时存活状态变成 CORRECT收得到
ApplicationReadyEventRunner 跑完;同时就绪状态变成 ACCEPTING_TRAFFIC收得到
ApplicationFailedEvent任何一步抛异常都会发这个,代替后面的事件要早早登记才收得到

想收前四个事件:把监听器写进 META-INF/spring.factories 的 org.springframework.context.ApplicationListener 下,或者在 main 方法里 application.addListeners(…)。

扩展点地图:想在启动时做点事,放在哪

越往上越早,能碰到的东西越少;越往下越晚,所有 Bean 都在,但应用也越接近对外服务了。

扩展点时间线上第几步典型用途怎么登记
EnvironmentPostProcessor3 读配置时从配置中心拉配置,解密密码spring.factories
ApplicationContextInitializer6 容器建好、Bean 还没登记改容器设置,加属性源spring.factories,或 addInitializers
BeanFactoryPostProcessor7 读图纸时改 Bean 定义,动态注册 Beanstatic @Bean 或 @Component
BeanPostProcessor8 造出来,10 每个 Bean 都过一遍套代理、统一注入@Component
@PostConstruct10 这个 Bean 刚造好初始化自己的字段方法上加注解
SmartInitializingSingleton10 结束时,所有单例都造完需要别的 Bean 都在的初始化实现接口
SmartLifecycle11 和 Tomcat 一起启动启停后台组件,比如 MQ 消费者实现接口
ApplicationRunner
CommandLineRunner
13 Started 之后、就绪之前预热缓存,检查数据@Component
@EventListener(ApplicationReadyEvent)14 一切就绪注册到注册中心,发通知方法上加注解

JDK 8 和 JDK 17:Boot 2 和 Boot 3

run() 的步骤、refresh 十二步、事件顺序,两边都一样。变的是登记方式、一些日志,以及 Boot 3 多出来的提前编译。

Spring Boot 2

JDK 8+
  • 自动配置登记在 spring.factories(2.7 起也认 AutoConfiguration.imports)
  • 2.4 起,application.yml 改由 ConfigData 机制读取,多 profile 的写法变成 spring.config.activate.on-profile
  • 启动日志:Started ShopApplication in 2.3 seconds (JVM running for 2.7)
  • 内嵌 Tomcat 9,javax.servlet

Spring Boot 3

JDK 17+
  • 自动配置只认 AutoConfiguration.imports;EnvironmentPostProcessor、ApplicationListener、FailureAnalyzer 这些还是写在 spring.factories
  • 启动日志改成:Started ShopApplication in 2.3 seconds (process running for 2.7)
  • 内嵌 Tomcat 10,jakarta.servlet
  • 可以提前编译(AOT):第 7 步读图纸的活放到构建时做,配合 GraalVM 打成原生程序,启动只要几十毫秒
  • 3.2 起配 spring.threads.virtual.enabled=true,Tomcat 用虚拟线程处理请求(要 JDK 21)
没变的:run() 的四段路refresh 十二步Tomcat 最后才开端口事件顺序FailureAnalyzer 报告

常踩的坑

先在时间线上找到出问题的那一步,再看那一刻什么已经有了、什么还没有。

@EventListener 收不到 ApplicationStartingEvent

那时容器还没建,你的 Bean 不存在。登记在 spring.factories 里,或者用 addListeners。

Web server failed to start. Port 8080 was already in use.

端口要到最后一步才绑定,所以所有 Bean 都造完了才失败。换 server.port,或者停掉占端口的进程。

… required a bean of type '…' that could not be found.

第 10 步造 Bean 时缺了依赖。看那个类有没有被扫描到,或者对应的自动配置是不是被条件挡掉了,用 debug=true 看条件报告。

Could not resolve placeholder 'pay.url' in value "${pay.url}"

第 3 步没读到这个配置,第 10 步注入时才报错。检查 profile 有没有激活、配置文件名和位置对不对。

The bean '…' could not be registered … overriding is disabled.

两个地方定义了同名的 Bean。Boot 2.1 起默认不许覆盖。改名,或者确认要覆盖再配 spring.main.allow-bean-definition-overriding=true。

@PostConstruct 里调自己的 HTTP 接口,连接被拒

@PostConstruct 在第 10 步,端口第 11 步才开。要等应用能对外服务,放到 ApplicationReadyEvent 里。

CommandLineRunner 里抛了异常,整个应用退出了

Runner 在 run() 的 try 里面,异常会让启动失败(Failed to execute CommandLineRunner)。可以失败的任务自己 catch 住。

Runner 里预热了五分钟,K8s 的就绪探针一直不过

Runner 跑完才发 ApplicationReadyEvent,就绪状态才变成 ACCEPTING_TRAFFIC。耗时的预热放到异步线程。

启动太慢,不知道慢在哪

用 BufferingApplicationStartup 记录每一步耗时,配合 Actuator 的 /actuator/startup 查看。spring.main.lazy-initialization=true 能变快,但错误会推迟到第一次调用才暴露。

Controller 写好了,访问却 404

扫描默认只扫主类所在的包和子包。主类放到最外层包,或者在 @SpringBootApplication 上指定 scanBasePackages。