Skip to content

Spring Boot 启动机制 ​

Spring Boot 启动不是“扫描注解然后启动 Tomcat”这么简单。一次完整启动需要先建立配置环境,再选择容器类型、加载初始定义、刷新对象图、创建服务器,最后执行 Runner 并发布就绪事件。不同阶段使用的扩展机制不同,故障入口也不同。

Spring Boot 启动源码主链

配套视频:EP02:run() 的完整启动编排 适合跟随本页主链观看;服务器阶段对应 EP19:内嵌 Tomcat 的创建与监听。

SpringApplication 构造阶段 ​

SpringApplication.run(OrderApplication.class, args) 会创建 SpringApplication,并保存主配置类。它根据 classpath 判断应用类型:MVC 条件满足时选择 Servlet Context,只有响应式 Web 条件满足且 MVC 不存在时才选择 Reactive,否则使用普通非 Web Context。

构造阶段还会发现 ApplicationContextInitializer 和 ApplicationListener。此时普通 Bean 尚不存在,这些扩展用于修改即将创建的 Context 或观察早期启动事件,不能按业务 Bean 的使用方式理解。

Primary Source 不是“扫描结果” ​

传给 run() 的 OrderApplication.class 是 Primary Source。它告诉 Boot 从哪里开始加载配置,通常同时带有 @SpringBootApplication。构造 SpringApplication 时还没有扫描 Controller、Service 或 Repository;这些类要等主配置被登记并在 refresh() 中由配置类处理器展开。

启动类位置会影响默认组件扫描根包。把启动类放在业务包的下层,可能导致同级模块没有被发现;放在默认包又可能扫描所有依赖。根包应覆盖应用组件,但不应把无关库全部纳入扫描。

应用类型是运行模型选择 ​

WebApplicationType 决定后续使用普通、Servlet 还是 Reactive ApplicationContext。它不仅影响有没有端口,还影响 WebServerFactory、作用域、服务器生命周期和请求入口。

当 MVC 与 WebFlux 同时在 classpath 中时,不能根据某个 Reactor 类型判断应用一定走响应式链。应检查 SpringApplication 的最终应用类型、Context 实际类型以及入口是 DispatcherServlet 还是 DispatcherHandler。

早期扩展点为什么不能都做成 Bean ​

Boot 启动早期需要在 Context 存在前完成日志、配置位置、Bootstrap Registry 和 Environment 修改。普通 Bean 依赖 Context 刷新后才能发现,因此无法反过来参与这些阶段。

扩展类型介入阶段适合做什么不应做什么
Bootstrap Registry 扩展Environment 前后为配置解析准备早期资源使用普通业务 Bean
EnvironmentPostProcessorEnvironment 准备增加或转换 PropertySource创建完整业务客户端
ApplicationContextInitializerContext 刷新前调整 Context、注册少量定义依赖全部单例已创建
ApplicationListener对应事件阶段观察生命周期和记录证据隐藏核心业务初始化
BeanFactoryPostProcessorrefresh 中定义阶段修改 BeanDefinition操作尚未创建的业务对象
RunnerContext 刷新后、就绪前执行短且可控的启动任务无超时远程调用

越早的扩展点拥有的运行环境越少,也越容易影响整个启动。平台代码若必须使用早期扩展,应保持功能窄、无业务副作用,并为失败提供清晰诊断。

Environment 准备阶段 ​

run() 创建 Environment,把默认属性、Config Data、环境变量、系统属性和命令行参数组织成有顺序的 PropertySource。日志配置、Profile、部分 spring.main.* 设置会在 Context 刷新前被读取,所以 Environment 必须提前完成。

Config Data 会解析包内与外部 application 文件、Profile 文档、spring.config.import 和可选位置。属性覆盖不是简单的“prod 文件高于默认文件”,还受来源类型、文件位置和文档顺序影响。排错时应确认最终 PropertySource,而不是只打开某个 YAML 文件。

环境准备还会触发早期监听器与日志初始化。此时发生的异常通常没有完整业务 Context,不能依赖普通 @Component 处理。需要参与这一阶段的扩展应使用 Boot 规定的早期发现机制,并避免引用尚未创建的业务 Bean。

Config Data 的详细发现、覆盖和绑定见 Config Data、配置覆盖与类型安全绑定。

Context 创建与初始加载 ​

ApplicationContextFactory 根据应用类型创建容器。此刻 Context 主要是一个尚未刷新的外壳。load() 随后把 primary source 登记为初始 BeanDefinition;组件扫描、@Bean 和自动配置仍要等待配置类处理器在 refresh() 中展开。

区分“Context 已创建”和“Context 已刷新”很重要。前者只能证明容器对象存在,后者才意味着普通单例、代理、生命周期组件和 Web 运行环境已经基本形成。

prepareContext() 会把 Environment 关联到 Context,应用 Initializer,并加载 Primary Source。SpringApplication.load() 使用 BeanDefinitionLoader 把主配置登记到 BeanDefinitionRegistry。这个阶段注册的是描述信息,不是已经执行构造器的启动类对象。

可以在 load() 后统计 BeanDefinition 数量,再在配置类处理完成后比较。后者会多出组件扫描、Import 和自动配置贡献的定义,这能直接证明“初始输入”和“最终对象图”之间还有配置解析过程。

refresh() 推进容器状态 ​

refresh() 先准备 BeanFactory,再执行 BeanFactoryPostProcessor。配置类处理器在这里扫描组件、处理 Import 并写入更多 BeanDefinition。随后容器注册 BeanPostProcessor,创建非懒加载单例;依赖注入、配置绑定、生命周期回调和自动代理都在对象创建过程中发生。

任何关键单例创建失败都会中断刷新。Spring 会销毁已创建的单例并关闭相关资源,避免留下一个只有部分对象可用的 Context。Bean 创建细节见 IoC 容器。

refresh() 内部可以按四个责任段理解:

  1. 准备 Context 与 BeanFactory,注册标准依赖和可解析对象。
  2. 执行 BeanFactoryPostProcessor,把初始配置展开为完整 BeanDefinition 集合。
  3. 注册 BeanPostProcessor,为注入、配置绑定、生命周期和自动代理安装创建规则。
  4. 创建非懒加载单例,完成生命周期组件初始化并发布刷新事件。

这四段不是简单的回调列表。工厂后处理器修改“怎样创建对象”,Bean 后处理器参与“正在创建的对象”,普通业务 Bean 则是处理结果。扩展点放错阶段,会出现依赖尚不可用、对象过早创建或无法获得代理等问题。

自动配置在什么时候进入 ​

自动配置候选由配置类解析链导入。条件评估决定哪些配置类和 Bean 方法形成定义,BeanFactory 随后才创建对象。条件报告属于定义阶段证据,不代表目标 Bean 一定成功初始化。

例如 DataSource 自动配置条件全部匹配,但连接池构造时仍可能因 URL 格式或驱动初始化失败。排查时要区分“定义有没有注册”和“实例有没有创建成功”。

WebServer 创建与端口监听 ​

Servlet Web Context 在刷新过程中寻找 ServletWebServerFactory。自动配置通常提供 Tomcat 或 Jetty Factory,服务器自定义器把 server.* 配置应用到 Factory,随后创建服务器并注册 DispatcherServlet、Filter 和 Listener。

服务器对象创建、Connector 绑定端口和应用发布就绪事件是不同时间点。排查时可以同时观察启动事件、Context 状态和 lsof -i :8080;生产探针则应以业务是否可以安全接流量为准。

Servlet Web Context 在 onRefresh() 附近创建 WebServer。服务器工厂、server.* 属性和 WebServerFactoryCustomizer 共同决定端口、TLS、压缩和线程等设置。随后 ServletContextInitializer 把 DispatcherServlet、Filter 和 Listener 注册到服务器环境。

端口冲突通常发生在服务器启动阶段,和 Controller 是否存在无关;Filter 初始化异常可能在服务器已经创建后阻止 Context 完成刷新。异常栈中的阶段比最外层“Application run failed”更有定位价值。

Runner、事件与就绪 ​

Context 刷新成功后,Boot 执行 ApplicationRunner 和 CommandLineRunner。它们仍位于启动关键路径,耗时初始化或远程调用会推迟应用就绪。Runner 完成后才发布 ApplicationReadyEvent。

必须在接流量前完成的短初始化可以放在启动链中;耗时预热应评估是否异步执行,并通过就绪状态控制流量。把不可控远程调用放入 Bean 构造器或初始化回调,会让整个 Context 被外部依赖超时拖住。

启动事件不是同一个时间点 ​

Boot 的事件覆盖 Environment 准备、Context 初始化、启动完成与应用就绪等阶段。监听早期事件的对象可能还不是 Bean;监听刷新后事件的普通 Bean 则可以使用完整对象图。

事件适合观察生命周期或触发与阶段一致的动作,不适合把核心业务依赖隐藏在监听器里。订单服务若必须在接流量前加载路由表,应让失败明确阻止就绪;若只是异步预热缓存,则应在预热期间控制 readiness,并允许失败后重试。

常见事件可以按“环境可用、Context 已建立、启动编排完成、应用已就绪、启动失败”理解。监听器应只依赖该时点已经成立的事实。例如准备 Environment 的事件不能读取 OrderService,ApplicationReadyEvent 可以访问完整 Bean,却不应承诺未来数据库永远可用。

源码调用坐标 ​

阶段观察入口要证明的事实
构造SpringApplication 构造器Primary Source、应用类型、早期扩展集合
环境prepareEnvironment()PropertySource 与激活 Profile 的最终结果
ContextcreateApplicationContext()实际 Context 类型是否符合预期
初始定义load()主配置类只是初始 BeanDefinition
刷新AbstractApplicationContext.refresh()配置解析、后处理器和单例创建顺序
服务器Web Context 的 onRefresh()WebServerFactory 与端口何时生效
RunnercallRunners()哪个 Runner 延长了启动关键路径
就绪ApplicationReadyEventBoot 启动编排已完成,不等于所有下游永远健康

观察源码时同时记录时间戳、线程名、BeanDefinition 数量和端口状态。只有方法断点而没有运行结果,容易把“经过某方法”误判为“阶段已经成功”。

四个失败实验 ​

配置格式错误 ​

在 YAML 中制造缩进错误。应用会在 Environment/Config Data 阶段停止,普通 Controller 与 Service 尚未创建。修复后应验证目标 PropertySource 与最终绑定值,不只确认进程能起来。

删除 Service 注解 ​

OrderService 不再形成 BeanDefinition,Controller 构造器依赖在单例创建阶段无法解析。此时 Environment 已经完成,但 Context 无法刷新,端口通常不会进入可用状态。

占用服务器端口 ​

先启动一个进程占用 8080,再运行应用。业务 Bean 可能已经创建,失败却发生在 WebServer 绑定 Connector 时。FailureAnalyzer 会给出可读提示,底层证据仍是端口占用与服务器异常。

Runner 调用慢服务 ​

在 Runner 中调用一个 30 秒超时的远端接口。Context 与端口可能已经存在,但 ApplicationReadyEvent 被推迟。如果平台过早送流量,会出现“进程活着但初始化未完成”的窗口。修正方案是缩短关键初始化、设置明确超时,并用 readiness 表达业务就绪。

启动耗时怎样拆解 ​

总启动时间只能说明慢,不能说明慢在哪里。可以结合启动日志、ApplicationStartup/StartupStep、JFR、Bean 创建耗时与外部调用日志,把时间分到 Config Data、配置解析、Bean 创建、服务器和 Runner。

常见慢点包括:扫描范围过大、初始化时访问网络、数据库迁移、连接池预热、代理或类路径过多、Runner 串行任务。优化前先取得阶段证据;盲目开启懒加载可能只是把问题推迟到第一笔请求。

启动优化还要考虑失败发现。关键配置和依赖若被过度懒加载,应用可能先进入就绪,随后在真实流量下才暴露缺失。

测试启动阶段而不是只测 Controller ​

完整 @SpringBootTest 可以证明主要配置能形成 Context,但还应针对不同阶段使用更窄测试:配置属性用 ApplicationContextRunner,自动配置用条件矩阵,Web 服务器用随机端口测试,Runner 与事件用显式监听记录顺序。

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApplicationStartupTest {

    @LocalServerPort
    int port;

    @Test
    void applicationStartsAndServesHealth() {
        var client = RestClient.create("http://localhost:" + port);
        var status = client.get()
                .uri("/actuator/health")
                .retrieve()
                .toEntity(String.class)
                .getStatusCode();
        assertThat(status.is2xxSuccessful()).isTrue();
    }
}

随机端口避免与本地进程冲突,也能证明服务器真正监听并处理请求。但 health 成功不能替代订单业务测试,应根据测试目标分别验证启动、协议和业务。

启动失败后的资源清理 ​

Context 刷新中途失败时,已经创建的单例与资源需要销毁。自定义线程池、SDK Client 或临时文件若没有接入 Bean 生命周期,可能在启动失败后继续残留线程或句柄,影响重试和测试进程退出。

基础设施对象应通过 Bean 销毁回调、AutoCloseable 适配或明确的 destroyMethod 释放。不要在静态初始化块中启动无法关闭的线程,也不要在构造器中注册全局钩子后忽略失败路径。

按阶段阅读异常 ​

配置导入异常通常发生在 Environment 阶段;Context 类型错误与 classpath 或显式 WebApplicationType 有关;BeanDefinition 解析失败多指向配置类和条件;BeanCreationException 要继续追踪最底层构造、注入和初始化异常;WebServerException 则检查端口、证书、Factory 和 Servlet 注册。

Boot 的 FailureAnalyzer 可以把常见底层异常改写成易读描述,但真正的定位仍要结合阶段和第一处有效 Caused by。

版本差异与稳定边界 ​

Boot 4.1 的主线基于 Framework 7.0,Boot 3.5 基于 Framework 6.2。两条线的总体启动阶段一致,但模块拆分、包名、自动配置类和托管依赖可能不同。源码断点应以项目实际版本为准,不把内部方法签名当成公共 API。

稳定理解应落在责任上:先准备 Environment,再创建并刷新合适的 Context,Web 应用在刷新中建立服务器,Runner 完成后发布就绪事件。即使内部实现调整,这条阶段边界仍能用于定位大多数启动故障。

继续阅读 ​

别急,先让缓存热一下。