Spring Boot 启动主链:main 方法如何成为可接流量的应用
同一个 main 方法,本地三秒启动,容器里却卡住一分钟;日志已经出现“Started”,网关仍收到连接拒绝;某个 Runner 失败后进程退出,但端口曾短暂监听。把这些现象都归结为“Spring 创建 Bean 很慢”会漏掉真正的阶段边界。Spring Boot 在容器刷新前后还要准备环境、选择应用类型、创建上下文、启动 WebServer、执行 Runner、发布可用性状态,并把失败翻译成诊断与退出码。
当前 Spring Boot 4.1 正式线最低要求 Java 17,并建立在 Spring Framework 7 上。仍运行 Boot 3.5 的系统可以沿同一主链理解启动,但升级需要同时核对 Jakarta、模块拆分、依赖管理与三方扩展。具体运行基线可从官方 System Requirements 验证,启动行为入口则是 SpringApplication 参考。
run 先组织启动策略,再创建业务对象
SpringApplication.run 不是 new AnnotationConfigApplicationContext() 的别名。它先根据 classpath 推断 WebApplicationType,发现并排序启动监听器和初始化器,创建 BootstrapContext,配置 headless、主应用类与默认属性,再开始环境准备。环境完成后才选择具体 ApplicationContext 类型并加载 primary sources。
BootstrapContext 让只在启动早期需要的对象跨越 environment 阶段,并在主 ApplicationContext 就绪后关闭或转交。把普通业务连接塞进早期上下文,会绕过标准生命周期并扩大资源泄漏面。EnvironmentPostProcessor 能在 context 创建前修改 Environment,适合配置来源基础设施;它若依赖普通 Bean,则时序本身就不成立。
SpringApplicationRunListener 与应用事件覆盖多个阶段,但不是所有事件都由普通 ApplicationContext multicaster 发布。context 尚不存在时,监听器必须通过启动注册机制发现。将一个普通 @EventListener 用来捕获最早 environment 事件,往往永远不会触发;诊断首先要确认事件发生时 Context 是否已经存在。
StartupPhaseDemo.java 把成功路径的不可逆阶段压缩为可运行状态模型:
javac --release 17 -Xlint:all -Werror examples/backend-development/spring-boot/boot-startup/StartupPhaseDemo.java examples/backend-development/spring-boot/boot-startup/StartupFailureDemo.java
java -cp examples/backend-development/spring-boot/boot-startup StartupPhaseDemophases=[bootstrap, environment-prepared, context-prepared, context-refreshed, runners-complete, ready] acceptsTraffic=true模型省略了监听器发现和容器内部细节,却固定了两个工程不变量:Environment 早于 Context refresh;ready 必须晚于 Runner 完成。若平台在端口监听后立即放流量,Runner 仍可能迁移数据或预热缓存,第一批请求便进入半就绪状态。
ApplicationContext 类型由应用模型决定
Servlet classpath、反应式 classpath 与显式 WebApplicationType 共同影响上下文类型。Servlet 应用通常进入 ServletWebServerApplicationContext,反应式应用进入对应 reactive context,非 Web 任务则使用普通 context。classpath 同时出现 MVC 与 WebFlux 时不能凭“依赖了 Reactor”推断运行模型,应查看实际选择与显式配置。
应用类型会改变 WebServer factory、scope、基础设施 Bean 和启动失败点。一个批处理工具误带 web starter,可能无意启动端口、增加安全面和内存;一个期望 Web 应用的模块因依赖排除变成 NONE,context 可以刷新却没有服务器。启动日志和指标应记录应用类型、context class、父 context、主 source 以及选中的 WebServer factory。
primary sources 通常包含 @SpringBootApplication 类。这个组合注解带入配置、组件扫描和自动配置,但扫描根包由主类位置决定。把主类放在过高公共包会扫描无关模块,放在过低包又漏掉组件。多 source 可以组合配置,却不应成为跨模块任意抓取配置类的手段;模块依赖仍要保持单向。
ApplicationContextInitializer 在 refresh 前拿到 ConfigurableApplicationContext,可注册 property source、激活基础设施或做结构校验。它不能假设普通 Bean 已存在,也不应在这里发远程请求。初始化器排序改变结果时要用显式 order 和集成测试固定,不能依赖 classpath 枚举顺序。
refresh 成功不等于整个应用已经 ready
Context refresh 进入 Spring Framework 的 BeanFactory 后处理、BeanPostProcessor 注册、非 lazy singleton 创建和生命周期启动。对 Web 应用,WebServer 的创建与 context refresh 相互协调,端口可能在 Runner 之前已经绑定。Context 成功后 Boot 发布 started 事件,然后调用所有 ApplicationRunner 和 CommandLineRunner,最后才发布 ready 事件并更新可用性。
Runner 适合有界、可失败、必须在接流量前完成的任务,例如验证关键映射或加载小型本地索引。无界消费循环、无限重试和大规模远程预热放进 Runner 会让 readiness 永远不出现。每个 Runner 应有名称、顺序、deadline、幂等与失败策略,并输出耗时;相同 order 的隐式先后不能承担业务正确性。
若启动动作可以在流量进入后渐进完成,应把能力状态单独暴露,而不是阻塞全局 ready。若没有它应用就无法正确服务,则失败应终止启动,不能记录 warning 后伪装 ready。区分“关键启动条件”和“可降级能力”比选择 Runner 还是监听器更重要。
ApplicationStartedEvent 与 ApplicationReadyEvent 的差异可以用端口和 Runner 验证:started 时 context 已刷新,但 Runner 尚未全部完成;ready 时调用链已返回前的启动任务完成。平台探针、自动化测试和启动耗时指标应以 ready 作为应用完成信号,而不是搜索日志文本 “Started”。
失败路径只发布一次失败结论,并清理已取得资源
环境解析、context 创建、Bean 初始化、端口绑定和 Runner 都可能抛异常。SpringApplication 捕获失败后通知 run listeners,发布失败事件(如果条件允许),调用失败分析器生成面向人的摘要,关闭 context,并由异常决定进程结果。某些失败发生得太早,普通事件监听器与日志系统尚未初始化,因此 stderr 与最原始 cause 仍是关键证据。
反向实验没有把 ready 混入失败路径:
java -cp examples/backend-development/spring-boot/boot-startup StartupFailureDemoevents=[starting, environment-prepared, failed:port-in-use] readyPublished=false exitCode=1真实端口占用通常发生在 WebServer 创建阶段,比模型中的 environment 更晚;模型只表达“任何终止失败都不能继续发布 ready”。故障报告要记录最后完成阶段、失败组件、最深 cause、端口/配置来源和清理结果。只保留顶层 Application run failed 会把 BindException、配置转换失败和循环依赖压成同一种现象。
FailureAnalyzer 能把特定异常转换成 description 与 action,但不能吞掉 cause 或自动修复。团队自定义分析器应匹配窄异常、输出安全信息,禁止打印秘密配置值。分析器本身失败时不能覆盖原异常,必须回退到完整堆栈。
退出码可以由异常或 Bean 提供,适合批处理调度器区分可重试与永久失败。Web 服务通常由编排平台根据进程码和探针决策;在失败监听器里调用 System.exit(0) 会把启动失败伪装成正常结束。只有最外层进程入口拥有退出权,库和 Starter 不应终止 JVM。
启动可观测性要把总时间分解到阶段与对象
总启动时间只是结果。ApplicationStartup 可以记录 startup steps,Buffering 或 JFR 实现用于观察 Bean 创建、配置处理等阶段;Actuator startup endpoint 只有配置了相应 recorder 才有数据。生产不应无限缓存每个细粒度 tag,记录器容量与暴露权限必须受控。
阶段指标至少包括:环境与 ConfigData 时间、context prepare、BeanFactory 后处理、单例创建、WebServer bind、Runner 总时长、ready 时间和失败阶段。Bean 创建长尾要按 bean name、type、依赖路径聚合,不能只列最慢十个就结束;一个慢 Bean 可能只是等待更深依赖。
类加载、DNS、熵、证书、远程配置和容器 CPU 限额都会影响启动。复现基线应固定镜像、JDK 发行线、CPU/内存限额、依赖缓存和外部服务延迟。连续多轮 warm start 与 cold start 分开统计,避免把构建缓存或文件系统页缓存收益归因给代码优化。
启动预算也影响发布容量。若每个实例需要两分钟 ready,而滚动发布每次只增加一个实例,突发扩容无法快速承接流量。优化优先级应由扩容 SLO 与故障恢复时间决定;盲目开启全局 lazy initialization 虽能缩短 ready,却把失败与延迟推到第一笔请求。
把启动完成定义成可验证契约
一条可审查的启动契约至少包含:应用类型和 context class 符合预期;关键配置已完成类型绑定与校验;自动配置命中集合与基线一致;端口已绑定;关键 Runner 完成;readiness 进入 accepting;失败时进程非零退出且资源回到基线。每项都应有自动化证据。
集成测试可以使用最小 context 验证非 Web 启动,用随机端口验证 WebServer,用故意冲突端口验证失败分析,用监听器收集事件顺序,用 ApplicationStartup 比较阶段。不要把生产秘密或真实外部依赖放进启动测试;外部边界应由可控替身和单独集成环境覆盖。
Spring Boot 的价值不是隐藏启动,而是把应用装配组织成一条有状态、有事件、有失败收敛的流水线。能指出当前停在哪一阶段、哪些资源已经建立、为什么还不能接流量,以及失败后谁负责关闭,才真正掌握了 SpringApplication.run。
