每天都写的那行代码
main 方法里的 SpringApplication.run(App.class, args)——写了千百遍,背后发生过什么?启动控制台那几十行日志不是装饰,它就是流程的逐行播报。这篇把 run 拆成十个坐标点,以后排查启动卡住、启动慢,每一步都有出处。
前传:new SpringApplication 的侦察工作
run 之前先构造 SpringApplication,这步做三件侦察:推断应用类型——classpath 里有 spring-webmvc 就是 SERVLET,有 webflux 就是 REACTIVE,都没有就是普通应用,这决定了后面起不起内嵌服务器;加载扩展点清单——从 META-INF 里捞出 ApplicationContextInitializer 和 ApplicationListener 两批预备役;推断主配置类——就是标了 @SpringBootApplication 的那个类。
主线:run 方法的十步旅程
第一站,启动监听器,发布 ApplicationStartingEvent;第二站,准备 Environment——读 application.yml、环境变量、命令行参数,解析 profile,发布 ApplicationEnvironmentPreparedEvent,配置解密、日志系统初始化都挂在这一站;第三站,打印 Banner;第四站,按应用类型创建 ApplicationContext,容器骨架落地;第五站,prepareContext——把主类注册进容器(你的 @SpringBootApplication 从这里成为 Bean 定义的源头),应用 Initializer;第六站,refresh(),整个启动的重头戏,下一节单独拆;第七站,收尾钩子;第八站,发布 ApplicationStartedEvent,计时器打出 Started in x seconds;第九站,callRunners:ApplicationRunner 和 CommandLineRunner 依次执行;第十站,发布 ApplicationReadyEvent,应用正式对外服务。
refresh:Spring 与 Boot 的交点
refresh() 是 Spring 容器生命周期(第 2 篇讲过的九步)在 Boot 里的正式上演:扫描组件、创建 BeanFactory、注册 BeanPostProcessor、批量实例化单例 Bean……Boot 特有的戏份在内嵌服务器上:onRefresh 阶段拿到 ServletWebServerFactory,finishRefresh 阶段创建并启动 Tomcat——所以「内嵌 Tomcat 什么时候起的」答案在 refresh 内部,不是 run 的最后一步。你看到的 Tomcat started on port 8080 日志就出自这里,随后 Bean 全部就绪,端口开始监听。
扩展点对号入座
@Component
public class BootListener {
// 服务器就绪、可以收流量了(Boot 2.6+ 可拿耗时)
@EventListener(ApplicationReadyEvent.class)
public void ready(ApplicationReadyEvent event) {
System.out.println("应用就绪,耗时:" + event.getTimeTaken().toMillis() + " ms");
}
}
// 或者实现 CommandLineRunner:启动第九站执行,适合预热
@Component
public class WarmUpRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// 预热缓存、拉取字典表等
}
}两个易混点辨析:Started 与 Ready 的区别——Started 表示容器刷新完成,Ready 表示 Runner 也跑完了、完全就绪;Runner 与 @PostConstruct 的区别——后者在 Bean 初始化时执行,早于容器刷新完成,依赖容器全貌的逻辑放 Runner 更稳。排查启动慢三板斧:日志级别调到 debug 看 ConditionEvaluationReport、用 Ready 事件打点计时、分阶段埋点定位慢在哪一站。
下一篇拆启动流程里最精彩的部分:自动装配——classpath 里加了个依赖,Bean 怎么就自己出现了?
评论 (0)