SPRING BOOT · SPRINGAPPLICATION.RUN · REFRESH
一行 run,背后走了四段路
从 main 方法到“Started … in 2.3 seconds”,Spring Boot 先认清自己是什么应用,再读配置、建容器,然后在 refresh 里把所有 Bean 和 Tomcat 都造出来,最后跑 Runner、宣布就绪。
- 判断是不是 Web 应用
- 读扩展名单(spring.factories)
- 读命令行、环境变量、application.yml
- 打 banner,建一个空容器,放进主类
- 扫描 + 自动配置,得到几百张 Bean 图纸
- 建 Tomcat(先不开门)
- 造出所有单例 Bean,最后打开端口
- 打印 Started
- 跑 Runner
- 宣布就绪
SpringApplicationrun() 就是它的一张启动清单,按顺序一步步做。
Spring Boot 独有Environment所有配置排好优先级放在一起:命令行压过环境变量,环境变量压过 yml。
建容器之前就准备好ApplicationContext装所有 Bean 的工厂。refresh() 这一步把它从空壳变成能用。
Web 应用用 ServletWebServer 版本WebServerrefresh 中途就建好了,但要等 所有 Bean 造完才绑定端口。
不用再单独部署 war 包run() 的骨架
Spring Boot 3 的源码去掉计时和细节后,就剩下面这些。注释里的编号,对应下面时间线上的步骤。
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 项目。每一步看:这时已经有了什么,发了哪个事件,你能在这里插手做什么,日志里多了哪几行。也可以看看启动失败时,卡在了哪一步。
这时已经有了什么
表里 Bean 的个数是一个普通示例项目里的量级,不同项目差别很大。
refresh() 的十二步
这是 Spring 框架本身的方法,不是 Boot 加的。Boot 只是选了一个能内嵌 Tomcat 的容器,再在第 9 步里把 Tomcat 建出来。标了颜色的五步最值得记。
prepareRefresh记下启动时间,检查必填配置obtainFreshBeanFactory拿到真正装 Bean 的 BeanFactoryprepareBeanFactory放进类加载器、几个内置 BeanpostProcessBeanFactoryWeb 容器在这里加点自己的东西registerBeanPostProcessors先把改装师们造出来,后面每个 Bean 都要过他们的手initMessageSource国际化消息initApplicationEventMulticaster事件广播器onRefreshBoot 在这里建 Tomcat,装好 Filter 和 DispatcherServlet,但先不绑定端口registerListeners登记监听器finishRefresh启动 SmartLifecycle,Tomcat 在这里绑定端口,发 ContextRefreshedEvent启动过程中的事件,按顺序
前四个事件发出时,容器里的 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 都在,但应用也越接近对外服务了。
| 扩展点 | 时间线上第几步 | 典型用途 | 怎么登记 |
|---|---|---|---|
| EnvironmentPostProcessor | 3 读配置时 | 从配置中心拉配置,解密密码 | spring.factories |
| ApplicationContextInitializer | 6 容器建好、Bean 还没登记 | 改容器设置,加属性源 | spring.factories,或 addInitializers |
| BeanFactoryPostProcessor | 7 读图纸时 | 改 Bean 定义,动态注册 Bean | static @Bean 或 @Component |
| BeanPostProcessor | 8 造出来,10 每个 Bean 都过一遍 | 套代理、统一注入 | @Component |
| @PostConstruct | 10 这个 Bean 刚造好 | 初始化自己的字段 | 方法上加注解 |
| SmartInitializingSingleton | 10 结束时,所有单例都造完 | 需要别的 Bean 都在的初始化 | 实现接口 |
| SmartLifecycle | 11 和 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)
常踩的坑
先在时间线上找到出问题的那一步,再看那一刻什么已经有了、什么还没有。
那时容器还没建,你的 Bean 不存在。登记在 spring.factories 里,或者用 addListeners。
端口要到最后一步才绑定,所以所有 Bean 都造完了才失败。换 server.port,或者停掉占端口的进程。
第 10 步造 Bean 时缺了依赖。看那个类有没有被扫描到,或者对应的自动配置是不是被条件挡掉了,用 debug=true 看条件报告。
第 3 步没读到这个配置,第 10 步注入时才报错。检查 profile 有没有激活、配置文件名和位置对不对。
两个地方定义了同名的 Bean。Boot 2.1 起默认不许覆盖。改名,或者确认要覆盖再配 spring.main.allow-bean-definition-overriding=true。
@PostConstruct 在第 10 步,端口第 11 步才开。要等应用能对外服务,放到 ApplicationReadyEvent 里。
Runner 在 run() 的 try 里面,异常会让启动失败(Failed to execute CommandLineRunner)。可以失败的任务自己 catch 住。
Runner 跑完才发 ApplicationReadyEvent,就绪状态才变成 ACCEPTING_TRAFFIC。耗时的预热放到异步线程。
用 BufferingApplicationStartup 记录每一步耗时,配合 Actuator 的 /actuator/startup 查看。spring.main.lazy-initialization=true 能变快,但错误会推迟到第一次调用才暴露。
扫描默认只扫主类所在的包和子包。主类放到最外层包,或者在 @SpringBootApplication 上指定 scanBasePackages。