概述
本文将不仅拆解启动流程,更将用我们在前文所学的 IoC、DI、扩展点、设计模式等核心知识,作为“X光机”来透视每一步的设计初衷与内部机制。 这将是一次“知其然,更知其所以然”的深度巡游,目标是让您真正打通 Spring 核心容器与 Spring Boot 之间的任督二脉,构建起完整的知识体系。
核心观点导读
任何一个 Spring Boot 应用都始于一行 SpringApplication.run()。这看似简单的一行代码,内部却是一场精心编排的交响乐,它由多个乐章组成:应用类型的智能推断、精密的多层环境构建、IoC 容器的创建与刷新、自动配置的触发、嵌入式 Web 服务器的启动。Spring Boot 并未重复造轮子,而是巧妙地运用了模板方法、观察者、责任链等设计模式,将 Spring 核心容器中的各个组件重新编织,形成了一个清晰、可控且高度可扩展的启动流程。本文将带您深入这场交响乐的幕后,一探究竟。
核心要点提炼
- 启动骨架:
SpringApplication构造 →run()八阶段生命周期广播 → 环境准备 → 上下文创建与准备 → 刷新上下文(核心容器觉醒)→ 启动 Web 服务器 → 回调执行。 - 智能推断:基于类路径(Classpath)中的关键类,自动推断应用类型为 SERVLET、REACTIVE 或 NONE。
- 观察者模式(Observer):
SpringApplicationRunListener是贯穿run()方法的全局事件广播器,实现了启动生命周期各阶段的完全解耦。 - 模板方法模式(Template Method):
AbstractApplicationContext.refresh()依然是核心容器的启动骨架,Spring Boot 只是在子类中通过重写onRefresh()等钩子方法植入 Web 服务器启动逻辑。 - 自动配置入口:
@EnableAutoConfiguration注解通过@Import机制引入AutoConfigurationImportSelector,在刷新阶段借助ConfigurationClassPostProcessor(BDRPP) 批量加载并筛选自动配置类。 - 用 Spring 知识解读 Boot:理解 Spring Boot 的关键,在于用前文的 IoC、BDRPP、BD、扩展点、SPI 等知识去解构其启动流程。Boot 的每一步都是在管理和驱动 Spring 核心组件,而非凭空创造。
文章组织架构图
graph TD
Start["开端 一键启动 SpringApplication点run"] --> M1
subgraph M1["1 启动流程总览"]
M1_1["宏观骨架与StopWatch"]
M1_2["观察者模式 SpringApplicationRunListeners"]
end
M1 --> M2
subgraph M2["2 启动前准备 SpringApplication构造"]
M2_1["推断应用类型"]
M2_2["SPI加载Initializer与Listener"]
M2_3["可定制性总结"]
end
M2 --> M3
subgraph M3["3 启动阶段一 环境准备"]
M3_1["Environment创建"]
M3_2["PropertySource优先级与整合"]
M3_3["EnvironmentPostProcessor扩展点"]
M3_4["激活Profiles"]
end
M3 --> M4
subgraph M4["4 启动阶段二 上下文创建与准备"]
M4_1["根据类型创建ApplicationContext"]
M4_2["调用ApplicationContextInitializer"]
M4_3["注册主配置类为BeanDefinition"]
end
M4 --> M5
subgraph M5["5 启动阶段三 refreshContext 核心容器觉醒"]
M5_1["模板方法 AbstractApplicationContext点refresh"]
M5_2["自动配置触发"]
M5_2_1["ConfigurationClassPostProcessor BDRPP执行"]
M5_2_2["Import解析与AutoConfigurationImportSelector"]
M5_2_3["条件注解Conditional筛选"]
end
M5 --> M6
subgraph M6["6 启动阶段四 嵌入式Web服务器启动"]
M6_1["模板方法钩子 onRefresh"]
M6_2["WebServer创建与启动"]
end
M6 --> M7
subgraph M7["7 启动阶段五 Runner回调"]
M7_1["ApplicationRunner与CommandLineRunner"]
M7_2["发布ApplicationReadyEvent"]
end
M7 --> M8["8 全链路协作与知识解读"]
M8 --> M9["9 可定制性总结"]
M9 --> M10["10 生产事故排查"]
M10 --> M11["11 面试高频题"]
架构图分层说明
-
总览说明:全文共 11 个核心模块。我们从宏观的
run()骨架和观察者模式出发(模块 1、2),然后遵循启动的生命周期,逐一拆解环境准备(模块 3)、上下文创建与准备(模块 4)、核心刷新与自动配置触发(模块 5)、Web 服务器启动(模块 6)和最后的 Runner 回调(模块 7)。最后,我们通过一张“全链路协作图”(模块 8)将启动流程与 Spring 核心容器知识进行缝合,并总结其可定制性(模块 9)、排查生产问题(模块 10)和应对面试挑战(模块 11)。 -
逐模块说明:
- 模块 1 & 2:奠定基础,展示
run()的骨架和SpringApplication的准备工作,这是后续所有步骤的基石。 - 模块 3 & 4:进入
run()方法的早期阶段,构建应用的环境和 IoC 容器,是“运行时”的准备过程。 - 模块 5:本文重点。深入剖析
refresh()模板方法和自动配置的触发内幕,揭示 Spring Boot 如何复用并增强 Spring 核心容器。 - 模块 6 & 7:完成应用启动的最后冲刺,启动 Web 服务并执行用户自定义的回调。
- 模块 8, 9, 10, 11:从实践角度进行拔高和闭环,通过知识整合、事故复盘和面试突击,将技术内化为能力。
- 模块 1 & 2:奠定基础,展示
-
关键结论:Spring Boot 的启动流程,本质上是在 Spring 核心容器的生命周期骨架之上,通过 SPI 机制、
@Import机制和各种扩展点接口,构建了一套精密的“自动配置”封装层。理解它,需要全面打通 IoC、Bean 生命周期、后处理器、设计模式和 SPI 等多个核心知识领域,这也正是本文的价值所在。
1. 启动流程总览:SpringApplication.run() 的宏观骨架与观察者模式
1.1 宏观骨架:run() 方法的六阶段交响乐
SpringApplication.run() 方法的内部实现,将所有复杂性都委托给了同一个类的实例方法。我们将该方法的核心流程抽象为六个关键阶段,通过泳道图来直观展示。
graph TD
subgraph Swimlane_App["应用主线程"]
A["开始: SpringApplication.run()"] --> B
B["阶段1: 启动监听器广播"] --> C
C["阶段2: 准备环境"] --> D
D["阶段3: 创建并准备上下文"] --> E
E["阶段4: 刷新上下文 核心: 模板方法refresh()"] --> F
F["阶段5: 启动嵌入式Web服务器"] --> G
G["阶段6: 执行Runner回调"] --> H["结束: 应用已就绪"]
end
subgraph Legend["图例: 设计模式与扩展点"]
I["观察者模式: SpringApplicationRunListener"]
J["模板方法模式: AbstractApplicationContext.refresh()"]
K["扩展点: ApplicationContextInitializer"]
L["扩展点: EnvironmentPostProcessor"]
M["扩展点: ApplicationRunner/CommandLineRunner"]
end
B -.-> I
C -.-> L
D -.-> K
E -.-> J
G -.-> I
-
图表主旨概括:本图展示了
SpringApplication.run()方法内部按时间顺序执行的六个核心阶段,并标注了每个阶段应用的关键设计模式或扩展点。 -
逐层/逐元素分解:
- 主流程:从应用调用
run()开始,流程严格地、顺序地通过六个阶段。每个阶段都有明确的职责边界。StopWatch对象在阶段 1 启动,用于精确记录整个启动过程的耗时,体现了 Spring 对性能和可观测性的重视。 - 阶段 1 (启动监听器广播):它贯穿整个生命周期,通过
SpringApplicationRunListeners向所有注册的实现SpringApplicationRunListener接口的监听器广播事件(如starting())。 - 阶段 2 (准备环境):创建和配置应用的运行环境
Environment,这是一个策略模式的应用,根据应用类型创建不同类型的Environment。 - 阶段 3 (创建并准备上下文):根据应用类型创建对应的
ApplicationContext,并调用ApplicationContextInitializer对其进行预配置。 - 阶段 4 (刷新上下文):整个启动流程的核心。它调用
AbstractApplicationContext.refresh()模板方法,完成 Bean 的创建、依赖注入、后处理器执行等一系列 IoC 容器的标准工序。这是 Spring 核心容器的领域。 - 阶段 5 (启动 Web 服务器):如果是 Web 应用,则在此阶段创建并启动内嵌的 Tomcat、Jetty 或 Undertow 服务器。这是在
onRefresh()钩子中完成的。 - 阶段 6 (执行 Runner 回调):执行所有实现了
ApplicationRunner或CommandLineRunner接口的 Bean,这是容器完全启动后,执行一次性初始化任务的入口。
- 主流程:从应用调用
-
设计原理映射:
- 观察者模式 (Observer):
SpringApplicationRunListener是典型的观察者模式。SpringApplicationRunListeners作为被观察者(Subject),维护了一个观察者列表。每当进入新的启动阶段,它就通知所有观察者。 - 模板方法模式 (Template Method):阶段 4 的
refresh()方法是 Spring 框架中模板方法模式的典范,Boot 完全遵守并复用了这个骨架。
- 观察者模式 (Observer):
-
工程联系与关键结论:理解这六个阶段,就掌握了
SpringApplication.run()的顶层设计。每个阶段的实现都体现了高内聚、低内聚的软件设计原则,并通过扩展点机制实现了高度的可定制性。这个骨架是调试任何 Spring Boot 应用启动问题的导航图。
1.2 深入 SpringApplicationRunListener:贯穿始终的观察者
SpringApplicationRunListener 接口是 Spring Boot 启动流程中最核心的扩展点之一,它完美地体现了观察者模式。它允许开发者在应用启动的各个关键里程碑处插入自定义逻辑,而无需修改 Spring Boot 的核心代码,符合开闭原则。
接口方法定义与生命周期事件对照表:
| 接口方法 | 触发时机 (生命周期阶段) | 用途说明 |
|---|---|---|
starting() | 在 run() 方法开始执行后立刻调用,此时还未进行任何处理。 | 记录启动开始、环境准备前的初始化。 |
environmentPrepared(ConfigurableEnvironment) | 当 Environment 对象创建并配置完毕,但在将其应用到 ApplicationContext 之前调用。 | 可以对 Environment 做最后的检查或修改。 |
contextPrepared(ConfigurableApplicationContext) | ApplicationContext 创建并完成初始化,但在加载任何 Bean 定义、调用 refresh() 之前。 | 对刚创建好的上下文进行预配置。 |
contextLoaded(ConfigurableApplicationContext) | ApplicationContext 已经加载了所有 Bean 定义,但尚未调用 refresh() 方法。 | 可以检查和修改已加载的 Bean 定义。 |
started(ConfigurableApplicationContext) | ApplicationContext 已完成 refresh() 方法,应用上下文已刷新,ApplicationRunner 和 CommandLineRunner 被调用之前。 | 上下文已完全就绪,可以执行一些依赖于所有 Bean 已初始化的逻辑。 |
running(ConfigurableApplicationContext) | ApplicationRunner 和 CommandLineRunner 都执行完毕,应用完全启动后。 | 执行应用完全启动后的最终逻辑,如发送通知。 |
failed(ConfigurableApplicationContext, Throwable) | 启动过程中任何阶段发生错误时调用。 | 处理启动失败,记录错误信息、执行清理或报警。 |
-
用 Spring 核心知识解读:
SpringApplicationRunListeners并不直接实现List接口,而是在内部聚合了一个List<SpringApplicationRunListener>,并通过组合的方式广播事件。这又是一个经典的组合模式和开闭原则的体现。当需要新增一种事件监听器时,我们无需修改SpringApplicationRunListeners的广播逻辑,只需在spring.factories文件中添加新的注册即可。 -
内联示例:自定义启动耗时监听器 下面我们创建一个自定义的
SpringApplicationRunListener来精确记录每个阶段的耗时。// MyStartupTimingListener.java package com.example.demo.listener; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplicationRunListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.util.StopWatch; // 构造函数是必须的,Spring Boot 会通过反射调用此含参构造器 public class MyStartupTimingListener implements SpringApplicationRunListener { private final SpringApplication application; private final String[] args; private StopWatch stopWatch; // 必须提供这个构造方法 public MyStartupTimingListener(SpringApplication application, String[] args) { this.application = application; this.args = args; } @Override public void starting() { stopWatch = new StopWatch("MySpringApp"); stopWatch.start("1. 环境准备"); System.out.println("【自定义监听器】应用开始启动,计时开始..."); } @Override public void environmentPrepared(ConfigurableEnvironment environment) { stopWatch.stop(); stopWatch.start("2. 上下文创建与准备"); System.out.printf("【自定义监听器】环境准备完成,耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis()); } @Override public void contextPrepared(ConfigurableApplicationContext context) { stopWatch.stop(); stopWatch.start("3. 上下文刷新(refresh)"); System.out.printf("【自定义监听器】上下文创建并准备完成,耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis()); } @Override public void contextLoaded(ConfigurableApplicationContext context) { // 不在此处停止计时,因为 contextLoaded 和 refresh 紧密相连 } @Override public void started(ConfigurableApplicationContext context) { stopWatch.stop(); stopWatch.start("4. Runner回调"); System.out.printf("【自定义监听器】上下文刷新完成,耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis()); } @Override public void running(ConfigurableApplicationContext context) { stopWatch.stop(); System.out.printf("【自定义监听器】Runner回调执行完成,总耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis()); // 打印漂亮的格式化报表 System.out.println(stopWatch.prettyPrint()); } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { stopWatch.stop(); System.err.println("【自定义监听器】应用启动失败!"); exception.printStackTrace(); } }注册此监听器:在
src/main/resources/META-INF/spring.factories文件中添加:org.springframework.boot.SpringApplicationRunListener=\ com.example.demo.listener.MyStartupTimingListener启动应用,即可在控制台看到详细的阶段耗时分析报告。这验证了
SpringApplicationRunListener的观察者模式机制,其回调时机与我们分析的启动阶段完全一致。
2. 启动前的准备:SpringApplication 的构造、推断与可定制性
2.1 SpringApplication 构造过程源码全解
SpringApplication.run() 是一个静态方法,它内部会先创建一个 SpringApplication 实例,然后调用其 run() 方法。因此,启动的第一个关键步骤就是 SpringApplication 的构造。
sequenceDiagram
participant User as 用户代码/SpringApplication
participant SA as SpringApplication(构造器)
participant WR as WebApplicationType
participant SFL as SpringFactoriesLoader
participant FS as spring.factories文件
User->>SA: new SpringApplication(primarySources)
SA->>WR: deduceFromClasspath() <br> <strong>第一步:推断应用类型</strong>
WR-->>SA: 返回 WebApplicationType (SERVLET/REACTIVE/NONE)
SA->>SA: setInitializers(null) <br> <strong>第二步:加载Initializer</strong>
SA->>SFL: loadFactoryNames(ApplicationContextInitializer.class, classLoader)
SFL->>FS: 读取 META-INF/spring.factories
FS-->>SFL: 返回 Initializer 全类名列表
SFL-->>SA: 实例化并设置
SA->>SA: setListeners(null) <br> <strong>第三步:加载Listener</strong>
SA->>SFL: loadFactoryNames(ApplicationListener.class, classLoader)
SFL->>FS: 读取 META-INF/spring.factories
FS-->>SFL: 返回 Listener 全类名列表
SFL-->>SA: 实例化并设置
SA->>SA: deduceMainApplicationClass() <br> <strong>第四步:推断主配置类</strong>
SA-->>User: 构造完成
下面是 SpringApplication 构造器的核心源码(为简化已去除辅助逻辑):
// org.springframework.boot.SpringApplication
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 步骤一:推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 步骤二:通过 SPI 机制加载并实例化 ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 步骤三:通过 SPI 机制加载并实例化 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 步骤四:推断主配置类
this.mainApplicationClass = deduceMainApplicationClass();
}
源码解读:
-
WebApplicationType.deduceFromClasspath():这是“智能推断”的核心。它通过检查特定类路径下是否存在关键类来决定应用类型。例如,如果DispatcherServlet类存在,则是SERVLET应用;如果DispatcherHandler存在但DispatcherServlet不存在,则是REACTIVE应用。 -
getSpringFactoriesInstances():这是一个非常重要的方法,它封装了 Spring Boot 的 SPI 机制。它从META-INF/spring.factories文件中读取指定接口的实现类全限定名,然后进行实例化。这里加载了ApplicationContextInitializer和ApplicationListener,它们是启动流程早期阶段的关键扩展点。 -
deduceMainApplicationClass():这个方法通过分析new Throwable().getStackTrace()的调用栈,来找到一个包含main方法且类名匹配的类作为“主配置类”。这是一种相当巧妙但可靠的推断方式。 -
用 Spring 核心知识解读:
- SPI 与扩展点体系(第 9、7 篇):构造过程的第二步和第三步是 Spring Boot 可插拔性设计的核心。它完全利用了我们前文讲解的
SpringFactoriesLoader,这是 Spring 框架原生 SPI 机制的增强版。ApplicationContextInitializer和ApplicationListener这两个接口是优秀的扩展点契约,Spring Boot 启动时自动发现并执行它们,实现了对容器启动过程的声明式定制。 - 设计模式:构造器本身使用了生成器(Builder)模式的思想,虽然它不是传统的
Builder类,但其构造过程是将多个独立、复杂的组件(类型、Initializer、Listener)进行组装,形成一个复杂且可用的SpringApplication对象。
- SPI 与扩展点体系(第 9、7 篇):构造过程的第二步和第三步是 Spring Boot 可插拔性设计的核心。它完全利用了我们前文讲解的
2.2 SpringApplication 的可定制性总结
在 run() 之前,SpringApplication 提供了丰富的编程式定制手段,与声明式 SPI 方式互为补充。
| 定制方式 | 具体手段 | 示例 | 描述 |
|---|---|---|---|
| 编程式 | SpringApplication.setXxx() | setBannerMode(Off)、setAdditionalProfiles("dev")、addInitializers()、addListeners() | 在代码中直接修改 SpringApplication 实例的属性,优先级较高。 |
| 编程式 | Builder API | new SpringApplicationBuilder().sources(Parent.class).child(Child.class).run(args) | 为构建分层的 ApplicationContext 提供了流式 API。 |
| 声明式 | META-INF/spring.factories | org.springframework.context.ApplicationListener=\com.my.MyListener | 利用 SPI 机制,将扩展点实现类自动注册到 SpringApplication 中。 |
| 外部化 | 启动参数/环境变量等 | --server.port=8081 | 在 run() 的环境准备阶段被整合到 Environment 中,影响后续配置。 |
3. 启动阶段一:环境准备——PropertySource 优先级与扩展点
环境准备是 run() 方法执行的第一个实质性阶段,它为应用的运行构建了上下文——Environment 对象。
sequenceDiagram
participant RUN as SpringApplication.run()
participant PREP_ENV as prepareEnvironment()
participant ENV as ApplicationServletEnvironment
participant MPS as MutablePropertySources
participant POST as EnvironmentPostProcessor
participant EP_LIST as EnvironmentPostProcessors Factory
RUN->>PREP_ENV: 调用
PREP_ENV->>ENV: 创建 Environment (根据应用类型)
Note over PREP_ENV, MPS: 整合 PropertySource (按优先级从高到低)
PREP_ENV->>MPS: addLast(commandLineArgs) <br> <strong>1. 命令行参数</strong>
PREP_ENV->>MPS: addLast(servletConfigInitParams) <br>2. Servlet上下文初始化参数
PREP_ENV->>MPS: addLast(servletContextInitParams) <br>3. Servlet上下文参数
PREP_ENV->>MPS: addLast(systemProperties) <br>4. 系统属性
PREP_ENV->>MPS: addLast(systemEnvironment) <br>5. 操作系统环境变量
PREP_ENV->>MPS: addLast(random) <br>6. RandomValuePropertySource
PREP_ENV->>MPS: addLast(applicationConfig) <br>7. application.properties/yml
PREP_ENV->>EP_LIST: 加载并调用 EnvironmentPostProcessors
EP_LIST->>POST: postProcessEnvironment(environment, application)
POST-->>EP_LIST: 修改后的 Environment
EP_LIST-->>PREP_ENV: 完成
PREP_ENV->>ENV: 解析 spring.profiles.active 并激活相关 Profile
PREP_ENV-->>RUN: 返回 ConfigurableEnvironment
-
图表主旨概括:此序列图展示了
prepareEnvironment阶段如何创建一个Environment对象,并按照严格的优先级顺序整合各类属性源,最后调用EnvironmentPostProcessor扩展点。 -
逐层/逐元素分解:
- 参与者:
SpringApplication.run()作为调用者,prepareEnvironment()是执行者。ApplicationServletEnvironment(Web 应用默认)是Environment的具体实现。MutablePropertySources是存放所有属性源的数据结构。EnvironmentPostProcessor是关键的扩展点。 - 属性源整合:这是图解的核心。Spring Boot 将来自不同地方的配置信息包装成一个个
PropertySource对象,然后按照从高到低的优先级,通过addFirst或addLast方法插入到MutablePropertySources的CopyOnWriteArrayList中。越晚被addFirst插入的优先级越高。因此,属性查找时,会优先从命令行参数开始,一层层找到配置文件。这种优先级设计深刻体现了“约定优于配置,但也尊重显式声明”的哲学。 - 扩展点调用:在所有默认属性源都加载完毕后,Spring Boot 通过 SPI 机制加载并执行
EnvironmentPostProcessor,为开发者提供了一个在ApplicationContext创建前最后一个修改Environment的机会。
- 参与者:
-
设计原理映射:
- 策略模式:根据
webApplicationType创建不同的Environment实现(ApplicationServletEnvironment或ApplicationReactiveWebEnvironment)。 - 责任链模式:
EnvironmentPostProcessor列表的执行过程可以看作一个变体的责任链,每个处理器都有机会修改Environment。
- 策略模式:根据
-
工程联系与关键结论:掌握
PropertySource的优先级是排查线上配置不生效问题的首要技能。 当发现一个配置值不是预期的时候,应该立即想到它被更高优先级的源覆盖了。MutablePropertySources的有序列表结构是理解这一切的关键。
3.1 核心源码解读:属性源加载与 EnvironmentPostProcessor
// org.springframework.boot.SpringApplication
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// 1. 创建 Environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 2. 配置 Environment (属性源,profiles)
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 3. 加载并调用 EnvironmentPostProcessor (扩展点,通过SpringFactoriesLoader)
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(environment);
// ...省略将environment绑定到SpringApplication的部分
return environment;
}
protected void configurePropertySources(MutablePropertySources propertySources, String[] args) {
// ... existing property sources ...
if (args.length > 0) {
// 简化的命令行参数解析,实际上会封装为SimpleCommandLinePropertySource
propertySources.addFirst(new SimpleCommandLinePropertySource(args));
}
// 整合系统属性与系统环境
propertySources.addLast(new PropertiesPropertySource(System.getProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(System.getenv()));
// ...
}
-
说明:
addFirst和addLast直接决定了优先级。命令行参数通过addFirst被放在列表头部,获得最高优先级。 -
内联示例:自定义
EnvironmentPostProcessor我们可以通过一个自定义的EnvironmentPostProcessor在环境准备完成后动态添加一个配置源。// MyEnvironmentPostProcessor.java package com.example.demo.env; import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.PropertiesPropertySource; import java.util.Properties; public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { Properties props = new Properties(); // 模拟从远程配置中心拉取配置 props.put("app.custom.message", "Hello from Remote Config Center!"); // 创建一个新的PropertySource PropertiesPropertySource pps = new PropertiesPropertySource("remoteConfig", props); // 添加到首位,使其拥有最高优先级 environment.getPropertySources().addFirst(pps); System.out.println("【自定义EnvironmentPostProcessor】已加载远程配置并设为最高优先级!"); } }注册:在
META-INF/spring.factories中添加:org.springframework.boot.env.EnvironmentPostProcessor=com.example.demo.env.MyEnvironmentPostProcessor之后在应用的任何地方注入
@Value("${app.custom.message}")这个属性值,都可以获取到“Hello from Remote Config Center!”。这验证了EnvironmentPostProcessor在环境准备阶段的强大扩展能力,其执行时机正是在Environment创建之后、ApplicationContext创建之前。 -
关联第 10 篇(类型转换与数据绑定):
@ConfigurationProperties注解背后的“黑魔法”正是依赖于Environment中的ConversionService。当我们将application.yml中的字符串“30s”绑定到一个Duration类型的 Java 字段时,ConversionService会自动完成类型转换。这背后的机制我们在前文已详细讨论。
4. 启动阶段二:ApplicationContext 的创建与准备
有了前期的类型推断和环境准备,接下来的任务就是创建并预初始化核心的 IoC 容器。
sequenceDiagram
participant RUN as SpringApplication.run()
participant CREATE as createApplicationContext()
participant CTX as ApplicationContext
participant PREP as prepareContext()
participant INIT as ApplicationContextInitializer
participant LIST as SpringApplicationRunListeners
RUN->>CREATE: 调用
CREATE->>CTX: 根据webApplicationType <br> 实例化具体类
alt SERVLET
CTX-->>CREATE: AnnotationConfigServletWebServerApplicationContext
else REACTIVE
CTX-->>CREATE: AnnotationConfigReactiveWebServerApplicationContext
else NONE
CTX-->>CREATE: AnnotationConfigApplicationContext
end
CREATE-->>RUN: 返回 context
RUN->>PREP: prepareContext(context, environment, ...)
PREP->>CTX: setEnvironment(environment)
PREP->>INIT: 遍历并调用 initializers <br> <strong>步骤1: 调用ApplicationContextInitializer</strong>
loop 每个 Initializer
INIT->>CTX: initialize(context)
end
PREP->>LIST: contextPrepared(context) <br> (广播事件)
PREP->>CTX: load(primarySources) <br> <strong>步骤2: 将主配置类注册为BeanDefinition</strong>
PREP->>CTX: registerSingleton(...) <br> <strong>步骤3: 注册默认单例Bean</strong>
PREP->>LIST: contextLoaded(context) <br> (广播事件)
PREP-->>RUN: 准备完毕
-
用 Spring 核心知识解读:
- 容器抽象(第 1 篇):
createApplicationContext方法根据类型推断结果,返回ApplicationContext接口的不同实现。AnnotationConfigServletWebServerApplicationContext这种冗长的类名,清晰地表明了其职责:支持@Configuration注解、面向 Servlet Web 环境、并具备启动嵌入式 Web 服务器的能力。这完美地体现了我们前文所学的“面向接口编程”和“容器抽象”的思想。上层代码(即run()方法)只与ConfigurableApplicationContext交互,而不关心其具体实现。 prepareContext的职责:此阶段是refresh()之前最后的“准备期”。- 调用
ApplicationContextInitializer:这些是在SpringApplication构造时加载的,现在它们对刚刚创建的ApplicationContext进行“预处理”,例如激活某些 Profile、设置资源加载器等。 - 注册主配置类:将
primarySources(即我们的主应用类,如@SpringBootApplication标注的类)解析并注册为BeanDefinition到BeanDefinitionRegistry中。这是一个关键动作。它意味着我们的主配置类变成了一个普通的 Bean 定义。为什么后续refresh()能处理它?因为我们的主配置类上肯定有@ComponentScan或@SpringBootApplication(它内部包含@ComponentScan),而refresh()阶段的ConfigurationClassPostProcessor会解析这些注解,从而完成组件扫描和自动配置。Spring Boot 只负责“注册定义”,核心容器负责“解析定义并驱动生命周期”,职责分离得淋漓尽致(关联第 7 篇扩展点体系)。
- 调用
- 容器抽象(第 1 篇):
-
内联示例:自定义
ApplicationContextInitializer// MyApplicationContextInitializer.java package com.example.demo.initializer; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; public class MyApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext context) { // 在Bean定义加载前,激活“test” profile context.getEnvironment().addActiveProfile("test"); System.out.println("【自定义Initializer】初始化上下文,已激活[test] profile。"); System.out.println("【自定义Initializer】上下文ID: " + context.getId()); } }注册:在
META-INF/spring.factories中添加:org.springframework.context.ApplicationContextInitializer=com.example.demo.initializer.MyApplicationContextInitializer启动应用,你会在日志中看到 profile 已被激活,并且该 Initializer 的日志在
contextPrepared事件之后、contextLoaded事件之前打印,验证了其精确的执行时机。
5. 启动阶段三:刷新上下文——核心容器的觉醒与自动配置触发(重点模块)
这是整个启动流程最核心、最复杂的阶段。Spring Boot 在此阶段并没有另起炉灶,而是通过“复用 + 增强” 的方式,将 Spring Core 的强大能力与自身的自动配置理念完美结合。
5.1 refresh() 模板方法的概要回顾
refreshContext 方法最终会调用到 AbstractApplicationContext.refresh(),这套我们熟悉的 12 步模板方法序列。Spring Boot 仅仅在 refresh() 前后增加了自己的逻辑,并在关键的钩子方法中植入了 Web 服务器的启动逻辑。
graph TD
Start["refreshContext(context)"] --> Pre["pre-refresh: 注册shutdown hook等"]
Pre --> CallRefresh["调用 context.refresh()"]
subgraph CoreRefresh ["AbstractApplicationContext.refresh() 模板方法"]
S1["1. prepareRefresh()"] --> S2["2. obtainFreshBeanFactory()"]
S2 --> S3["3. prepareBeanFactory()"]
S3 --> S4["4. postProcessBeanFactory() 模板方法钩子: 空实现 留给子类"]
S4 --> S5["5. invokeBeanFactoryPostProcessors() 核心: 自动配置触发点"]
S5 --> S6["6. registerBeanPostProcessors()"]
S6 --> S7["7. initMessageSource()"]
S7 --> S8["8. initApplicationEventMulticaster()"]
S8 --> S9["9. onRefresh() 模板方法钩子: 启动Web服务器"]
S9 --> S10["10. registerListeners()"]
S10 --> S11["11. finishBeanFactoryInitialization()"]
S11 --> S12["12. finishRefresh()"]
end
CallRefresh --> S1
S12 --> Post["post-refresh: 可选的afterRefresh钩子"]
Post --> End["返回上下文"]
用 Spring 核心知识解读:这个 12 步模板方法是整个 Spring 生态的基础设施(第 2、11 篇),它定义了 IoC 容器启动的固定算法骨架。Spring Boot 作为这个模板方法的“用户”或“子类”,通过在特定步骤(主要是第 5 步和第 9 步)使用的扩展机制,来注入自己的自动配置和 Web 服务器启动逻辑。
5.2 自动配置触发过程详解
这是模板方法中第 5 步 (invokeBeanFactoryPostProcessors) 的核心内容,也是 @EnableAutoConfiguration 最终发挥作用的地方。
sequenceDiagram
participant Core as AbstractApplicationContext
participant BDRPP as ConfigurationClassPostProcessor<br>(BDRPP)
participant Parser as ConfigurationClassParser
participant AIS as AutoConfigurationImportSelector
participant Cond as ConditionalEvaluator
Core->>BDRPP: postProcessBeanDefinitionRegistry(registry) <br> <strong>Step 1: BDRPP被执行</strong>
BDRPP-->>Core: 开始处理所有@Configuration类
BDRPP->>Parser: 遍历并解析每个配置类
Note over Parser: 处理主配置类,发现@SpringBootApplication
Parser->>Parser: 解析@SpringBootApplication上的元注解
Parser->>Parser: 发现@EnableAutoConfiguration
Parser->>Parser: 解析@EnableAutoConfiguration上的@Import
Note over Parser, AIS: @Import(AutoConfigurationImportSelector.class)
Parser->>AIS: 调用 selectImports() <br> <strong>Step 2: 触发自动配置选择器</strong>
AIS->>AIS: getCandidateConfigurations() <br> <strong>Step 3: 加载候选配置</strong>
Note over AIS: 从META-INF/spring/<br>org.springframework.boot<br>.autoconfigure.AutoConfiguration.imports<br>和 spring.factories 读取
AIS->>Cond: 对每个候选配置进行条件筛选 <br> <strong>Step 4: 按条件过滤</strong>
Cond->>Cond: 检查@ConditionalOnClass, <br>@ConditionalOnMissingBean等
Cond-->>AIS: 返回满足条件的配置类
AIS-->>Parser: 返回最终的自动配置类列表
Parser->>Core: 将配置类解析为BeanDefinition并注册
BDRPP-->>Core: 继续执行,进入Bean的实例化生命周期
-
图表主旨概括:此序列图聚焦于
invokeBeanFactoryPostProcessors阶段,详细展示了@EnableAutoConfiguration注解如何通过@Import、AutoConfigurationImportSelector和SpringFactoriesLoader等一系列联动,最终完成自动配置的加载和筛选。 -
逐层/逐元素分解:
ConfigurationClassPostProcessor(BDRPP) 启动:作为 Spring 最核心的后处理器之一,它在invokeBeanFactoryPostProcessors中被优先执行。它的任务就是扫描所有已经注册的@Configuration类。我们的主配置类(带有@SpringBootApplication)在阶段 4 已经被注册,所以此刻它被“逮个正着”。@Import链追踪:ConfigurationClassParser解析我们的MainApplication类,发现它被@SpringBootApplication标记,进而解析出@EnableAutoConfiguration,最终发现其上的@Import(AutoConfigurationImportSelector.class)。这完全印证了我们前文(第 8 篇)所学的@Import机制。AutoConfigurationImportSelector执行:selectImports()方法被调用,这是自动配置的黑箱核心。它内部调用getAutoConfigurationEntry()。- 候选配置加载与筛选:
- 加载:
getCandidateConfigurations()方法利用SpringFactoriesLoader,从所有 jar 包的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports和spring.factories文件中提取org.springframework.boot.autoconfigure.EnableAutoConfiguration键对应的所有全限定类名。这些就是“自动配置候选项”。 - 筛选:这是最关键的一步。它遍历所有候选项,使用
ConditionalEvaluator评估每个配置类上的@ConditionalOnXxx条件注解。例如,如果某个配置类标记了@ConditionalOnClass({ MongoClient.class }),但应用的类路径下没有 MongoDB 的依赖,这个配置类就会被移除。
- 加载:
- 注册 BeanDefinition:筛选通过后的配置类,会被
ConfigurationClassParser当作@Configuration类继续解析,它们内部定义的 Bean(如DataSource、JdbcTemplate等)最终都会被解析为BeanDefinition注册到BeanFactory中。
-
设计原理映射:
- 模板方法模式:整个
refresh()方法是骨架,invokeBeanFactoryPostProcessors是其中一步。 - 责任链模式:
BeanFactoryPostProcessor的执行可以看作一个责任链,ConfigurationClassPostProcessor是这个链上的核心一环。 - 策略模式:
@ConditionalOnXxx注解是策略模式的体现,@Conditional是策略接口,其不同的实现类(OnClassCondition,OnBeanCondition)是具体策略,用于动态决定配置是否生效。
- 模板方法模式:整个
-
工程联系与关键结论:自动配置不是什么“魔法”,而是一套基于“SPI发现 + @Import导入 + @Conditional条件筛选”的精密机制。 如果你掌握了这三个核心知识点(分别对应第 9、8、- 篇),你就彻底理解了自动配置的内核。
5.3 核心源码解读:自动配置选择器
// org.springframework.boot.autoconfigure.AutoConfigurationImportSelector
// 入口方法,由 ConfigurationClassParser 调用
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 获取自动配置条目
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// ... 检查是否禁用等 ...
// 1. 获取所有候选配置类名
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 2. 去重
configurations = removeDuplicates(configurations);
// 3. 获取应排除的配置
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 4. 应用条件筛选 (核心)
checkExcludedClasses(configurations, exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
// filter方法内部会使用ConditionalEvaluator进行筛选
// 5. 触发事件 (AutoConfigurationImportEvent)
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
// 通过 SpringFactoriesLoader 加载候选配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
// ... 从 AutoConfiguration.imports 等位置加载 ...
return configurations;
}
源码解读:这段代码清晰地展示了自动配置的加载和筛选链路。getCandidateConfigurations 负责“一网打尽”,filter 方法负责“优胜劣汰”。fireAutoConfigurationImportEvents 则体现了其可观测性,允许我们监听自动配置的导入过程。
6. 启动阶段四:嵌入式 Web 服务器的启动——模板方法钩子
sequenceDiagram
participant Core as AbstractApplicationContext
participant SW as ServletWebServerApplicationContext
participant Factory as TomcatServletWebServerFactory
participant WS as TomcatWebServer
participant SCI as ServletContextInitializer(DispatcherServlet等)
Note over Core: refresh()执行到第9步
Core->>SW: onRefresh() <br> <strong>模板方法钩子被调用</strong>
SW->>SW: createWebServer()
SW->>Factory: getWebServer()
Factory->>WS: new TomcatWebServer(...)
activate WS
WS->>WS: 初始化Tomcat容器
Factory->>SCI: 获取所有 ServletContextInitializer Beans
loop 每个 Initializer
Factory->>SCI: customize(context) <br> (注册到ServletContext)
end
WS->>WS: start() <br> <strong>Tomcat正式启动,监听端口</strong>
deactivate WS
WS-->>SW: 返回已启动的 WebServer
SW-->>Core: onRefresh() 结束
Note over Core: refresh()继续后续步骤(finishRefresh等)
-
图表主旨概括:本序列图展示了在
refresh()模板方法的第 9 步onRefresh()钩子中,ServletWebServerApplicationContext如何创建、配置并启动一个嵌入式 Tomcat 服务器。 -
逐层/逐元素分解:
- 模板方法钩子:
onRefresh()是一个受保护的空方法,由AbstractApplicationContext定义。ServletWebServerApplicationContext重写了它,并且通常在其中只做一件事:调用createWebServer()。 - 工厂模式应用:
createWebServer()会获取一个ServletWebServerFactoryBean。这通常是TomcatServletWebServerFactory、JettyServletWebServerFactory或UndertowServletWebServerFactory。这是标准的工厂方法模式(不是 GOF 的,而是 Spring 的实践模式)。 - 自适应服务器创建:
ServletWebServerFactory.getWebServer()是一个创建性操作。它会 new 一个具体的WebServer实例(如TomcatWebServer),并将所有实现了ServletContextInitializer接口的 Bean(包括最重要的DispatcherServletRegistrationBean,它会注册DispatcherServlet)传递给WebServer。 - 服务器启动:最后,
webServer.start()被调用,Tomcat/Jetty/Undertow 的内置实例正式开始监听端口,准备接收 HTTP 请求。
- 模板方法钩子:
-
设计原理映射:
- 模板方法模式:最核心的设计。核心容器的启动算法(
refresh())不变,子类通过重写onRefresh()来添加特定于 Web 环境的启动步骤,完全符合开闭原则。 - 工厂方法模式:
ServletWebServerFactory负责创建WebServer对象,将对象的创建逻辑和使用逻辑解耦。
- 模板方法模式:最核心的设计。核心容器的启动算法(
-
工程联系与关键结论:
onRefresh()是 Spring Boot 将 Web 服务器启动逻辑嵌入到 Spring 核心容器生命周期中的唯一接口。 如果你需要自定义 Web 服务器的配置(如端口、线程池等),你应该首先想到定制ServletWebServerFactoryBean。
7. 启动阶段五:Runner 回调与启动完成
这是启动流程的尾声,标志着应用已完全就绪,可以开始对外服务。
afterRefresh(Context):一个可选的钩子,在refresh()紧接完成后、任何 Runner 调用之前执行。默认是空实现。callRunners(Context, args):从ApplicationContext中获取所有实现了ApplicationRunner和CommandLineRunner接口的 Bean,按@Order排序后依次调用。CommandLineRunner接收原始的String... args。ApplicationRunner接收封装好的ApplicationArguments,它提供了更方便的选项参数(如--foo=bar)解析能力。
- 发布
ApplicationReadyEvent:在所有 Runner 执行完毕后,会发布这个事件,标志着应用真正“启动完成”。SpringApplicationRunListeners.running()也在此阶段被调用。
用 Spring 核心知识解读:Runner 回调机制本质上是利用 IoC 容器的基础能力,允许在容器完全初始化之后,立即执行一次性的、依赖于容器内 Bean 的初始化任务。相比于 @PostConstruct,Runner 的执行时机更晚,更能保证外部资源(如网络、数据库)已完全就绪。
8. 全链路协作图:用 Spring 核心容器知识解读整个启动流程
下表将 Spring Boot 的每个启动步骤与 Spring 核心容器的组件、设计模式及我们系列文章的知识点进行关联,帮助您形成系统性的认知。
| Spring Boot 启动步骤 | 对应的 Spring 核心容器组件/方法 | 使用的设计模式 | 关联的系列篇章 |
|---|---|---|---|
| 1. 构造与类型推断 | SpringFactoriesLoader, ClassLoader | 生成器模式, SPI机制 | 第 9 篇: SPI与插件化 |
| 2. 环境准备 | Environment, PropertySource, MutablePropertySources | 策略模式, 责任链模式 | 第 1 篇: 容器抽象, 第 10 篇: 类型转换 |
| 3. 上下文创建 | ApplicationContext 接口及其实现 | 工厂方法模式, 面向接口编程 | 第 1 篇: 容器抽象 |
| 4. 上下文准备 | ApplicationContextInitializer, BeanDefinitionRegistry | 观察者模式, 扩展点机制 | 第 7 篇: 扩展点体系 |
| 5. 刷新上下文 | AbstractApplicationContext.refresh() | 模板方法模式 | 第 2 篇: Bean生命周期, 第 11 篇: 设计模式 |
| 5.1 自动配置触发 | ConfigurationClassPostProcessor, @Import, @Conditional | 责任链模式, 策略模式 | 第 7 篇: BDRPP, 第 8 篇: @Import机制, 第 9 篇: SPI |
| 6. Web服务器启动 | onRefresh() 钩子, ServletWebServerFactory | 模板方法模式, 工厂方法模式 | 第 11 篇: 设计模式 |
| 7. Runner回调 | ApplicationRunner, CommandLineRunner, ApplicationEvent | 观察者模式 | 第 7 篇: 扩展点体系 |
| 贯穿全流程 | SpringApplicationRunListener | 观察者模式 | 第 11 篇: 设计模式 |
关键结论:从这张表中可以清晰地看到,Spring Boot 的启动流程本身就是一本 Spring 核心容器设计能力的最佳实践教科书。 它的每一步,无论是环境构建、容器刷新还是 Web 服务器启动,都严格建立在 IoC、DI、扩展点和设计模式等基础能力之上。Spring Boot 并未创造一个新世界,而是让 Spring 的世界变得更加智能和自动化。
9. SpringApplication 的可定制性总结
为了让你对如何干预启动流程了如指掌,我们将其可定制性归纳如下:
| 定制层面 | 具体方式 | 执行/生效阶段 | 示例 |
|---|---|---|---|
| 编程式 | SpringApplication.setXxx() | SpringApplication 构造后, run() 前 | setBannerMode(Off),setAdditionalProfiles("dev") |
| 编程式 | SpringApplication.addXxx() | SpringApplication 构造后, run() 前 | addInitializers(new MyInitializer()), addListeners(new MyListener()) |
| 编程式 | SpringApplicationBuilder | SpringApplication 构造期间 | new SpringApplicationBuilder().sources(App.class).profiles("dev").run(args) |
| 声明式(SPI) | spring.factories | SpringApplication 构造期间 | 注册 ApplicationContextInitializer, ApplicationListener, SpringApplicationRunListener, EnvironmentPostProcessor 等 |
| 外部化配置 | 命令行, 环境变量, application.yml | prepareEnvironment 阶段 | --server.port=8081, export SPRING_PROFILES_ACTIVE=dev |
| 注解驱动 | @EnableAutoConfiguration, @ComponentScan | refreshContext 阶段 | 主配置类上的这些注解,决定了自动配置和组件扫描的范围 |
10. 生产事故排查专题
理论最终要服务于实践。下面我们复盘三个典型的生产事故,并给出排查思路和工具。
案例一:启动卡住,无任何日志输出
- 现象:应用启动后,长时间没有任何输出,既不抛出异常,也不继续执行。线程看似“假死”。
- 排查工具/命令:
jcmd <pid> VM.command_line:查看启动参数。jstack -l <pid>:打印所有线程的堆栈信息,分析线程状态。
- 排查思路:
- 使用
jcmd找到 Java 进程 PID。 - 多次执行
jstack -l <pid>,观察线程堆栈。重点关注main线程和任何处于WAITING或BLOCKED状态的线程。 - 检查
main线程的堆栈,看它卡在哪个启动阶段。
- 使用
- 根因(结合启动流程):最常见的原因是某个
ApplicationContextInitializer或SpringApplicationRunListener中的自定义代码存在死锁、死循环或长时间的网络/IO等待。例如,一个在starting()方法中尝试连接不可达的外部服务且没有设置超时的 Listener。 - 解决与最佳实践:
- 立即修复阻塞的代码。对于外部依赖调用,必须设置合理的网络超时、异步化或使用断路器。
- 最佳实践:在关键自定义扩展点(如 Initializer, Listener)中增加明确的日志,并尽量保持逻辑轻量和幂等。
案例二:application.yml 配置不生效
- 现象:在
application.yml中配置了server.port=8888,但应用依然从 8080 端口启动,或者自定义属性取不到值。 - 排查工具/命令:
wget/curl http://localhost:8080/actuator/env或启用 Spring Boot Actuator 的env端点。- 在代码中注入
Environment,编写一个CommandLineRunner打印所有属性源。@Component public class EnvPrinter implements CommandLineRunner { @Autowired private ConfigurableEnvironment env; @Override public void run(String... args) { env.getPropertySources().forEach(ps -> { System.out.println("Source: " + ps.getName() + " -> " + ps.getSource()); }); } }
- 排查思路:
- 使用上述方法打印出
MutablePropertySources中的所有PropertySource。 - 检查
server.port属性存在于哪个源中。你会发现它可能在commandLineArgs或者系统环境变量中也存在。 - 确认
application.yml对应的PropertySource是否被加载,以及它在列表中的位置。
- 使用上述方法打印出
- 根因(结合启动流程):优先级覆盖。根据我们第 3 节的分析,
application.yml的优先级远低于命令行参数和系统环境变量。如果同时存在,高优先级的源会覆盖低优先级的源的配置。 - 解决与最佳实践:
- 检查是否在启动命令中传入了
--server.port=8080,或者设置了名为SERVER_PORT的环境变量。这是 12-Factor App 推荐的实践,但需要团队知晓其优先级。 - 最佳实践:通过 Actuator 的
env端点或代码打印,将运行时Environment的可视化作为启动后的标准检查项。
- 检查是否在启动命令中传入了
案例三:嵌入式容器端口冲突
- 现象:应用启动时,控制台抛出
java.net.BindException: Address already in use异常,启动失败。 - 排查工具/命令:
- Linux/Mac:
lsof -i :8080或netstat -anp | grep 8080 - Windows:
netstat -ano | findstr :8080
- Linux/Mac:
- 排查思路:
- 使用上述命令,找出正在占用目标端口(如 8080)的进程 ID (PID)。
- 通过
ps -ef | grep <PID>(Linux) 或任务管理器 (Windows) 确认该进程是不是之前没被杀死的旧应用进程,或者其他应用。
- 根因(结合启动流程):在
ServletWebServerApplicationContext.onRefresh()阶段,WebServer.start()尝试绑定到指定端口时,发现该端口已被其他进程或本应用的前一个未完全终止的实例占用。 - 解决与最佳实践:
- 立即杀掉占用端口的非预期进程。
- 更改本应用的端口:
--server.port=8090。 - 最佳实践:启用
server.port=0让系统随机分配可用端口(在开发测试中很方便但生产不推荐),或在生产环境的部署脚本中加入强制杀掉旧进程并等待的操作。更重要的是,确保应用有优雅停机机制,避免强行 KILL。
11. 面试高频专题
(本模块严格与正文分离,旨在从面试角度巩固知识)
-
问题:请完整描述
SpringApplication.run()方法从开始到结束的主要步骤。- 标准回答:分六步:1. 启动监听器广播;2. 环境准备;3. 上下文创建与准备;4. 刷新上下文;5. 启动嵌入式Web服务器;6. 执行Runner回调。
- 追问 1:
SpringApplication实例是在什么时候创建的?
回答:在调用静态run()方法时,内部会先 new 一个SpringApplication实例,再调用其实例run()方法。 - 追问 2:应用类型是在哪一步被推断出来的?
回答:在SpringApplication的构造过程中,通过WebApplicationType.deduceFromClasspath()静态方法推断。 - 追问 3:
SpringApplicationRunListener和ApplicationListener有什么区别?
回答:前者是专门为监听run()方法启动生命周期而设的扩展点,后者是 Spring 核心容器的通用事件监听器,监听如ContextRefreshedEvent等事件。前者启动全过程可用,后者在refresh()之后才注册其事件广播器。 - 加分回答:能结合源码,指出
SpringApplication构造时通过SpringFactoriesLoader加载ApplicationListener,但这些监听器会一直跟随到refresh()阶段,被注册进ApplicationContext成为通用事件监听器的一部分。
-
问题:Spring Boot 在刷新上下文时,是如何触发自动配置的?(关联型题目)
- 标准回答:在
invokeBeanFactoryPostProcessors阶段,ConfigurationClassPostProcessor解析@SpringBootApplication上的@EnableAutoConfiguration注解,该注解通过@Import引入了AutoConfigurationImportSelector。该 Selector 通过SpringFactoriesLoader加载所有候选的自动配置类,并用@Conditional注解进行筛选,最后符合条件的配置类会被注册进容器。 - 追问 1:
ConfigurationClassPostProcessor是BeanFactoryPostProcessor还是BeanDefinitionRegistryPostProcessor?这有什么不同?
回答:它是BeanDefinitionRegistryPostProcessor。后者是前者的子接口,它允许在标准BeanFactoryPostProcessor执行之前,修改BeanDefinitionRegistry,即可以注册新的 Bean 定义,这正是@Import和组件扫描需要的能力。 - 追问 2:
@Import机制在这里起到了什么作用?
回答:它是“桥梁”,将@EnableAutoConfiguration这个标记注解与实际的逻辑处理器AutoConfigurationImportSelector连接起来。当解析到@Import时,容器会实例化并调用该 Selector。 - 追问 3:如果一个自动配置类上有
@ConditionalOnMissingBean(DataSource.class),但项目里手动配置了一个,这个自动配置类还会生效吗?为什么?
回答:不会。AutoConfigurationImportSelector在filter阶段会使用ConditionalEvaluator评估所有@Conditional注解。OnBeanCondition会检查容器中是否已存在DataSourcebean,如果存在,则不满足OnMissingBean条件,该配置类会被移除。 - 加分回答:能进一步讲清楚
AutoConfigurationImportSelector内部使用AutoConfigurationImportEvent来广播导入事件,可以用于监控和调试自动配置。
- 标准回答:在
-
问题:
SpringApplication.run()内部有哪些地方用到了模板方法模式?举出至少两个。(关联型题目)- 标准回答:1.
AbstractApplicationContext.refresh()是整个启动的核心,它定义了 12 步的算法骨架,如postProcessBeanFactory(),onRefresh()等都是留给子类的钩子,SpringApplication.run()最终会调用它。2.SpringApplication自身的run()方法也可以看作一个模板方法,其中包含了afterRefresh()钩子。 - 追问 1:
onRefresh()在 Spring Boot 中具体用来做什么?
回答:在ServletWebServerApplicationContext中,onRefresh()钩子被重写以启动嵌入式 Web 服务器(Tomcat、Jetty 等)。 - 追问 2:为什么不直接在
run()方法里写死启动 Web 服务器的代码,而非要用模板方法呢?
回答:这完全是出于对开闭原则的遵守。未来如果有了新的容器类型(如 Netty),我们只需要创建新的ApplicationContext子类并重写onRefresh()即可,核心的refresh()框架完全不需要做任何修改。 - 追问 3:
postProcessBeanFactory()这个钩子在Boot里有什么应用?
回答:AnnotationConfigServletWebServerApplicationContext重写了它,用于注册一个WebApplicationContextServletContextAwareProcessor,它能自动将ServletContext和ServletConfig注入给实现了相关Aware接口的 Bean。 - 加分回答:能明确指出模板方法模式的原则是“Don‘t call us, we’ll call you”,并关联好莱坞原则。
- 标准回答:1.
-
问题:如果不用 Spring Boot,如何手动实现类似的自动配置机制?(系统设计/关联型题目)
- 标准回答:需要组合Spring核心组件:1. 创建一个
@EnableXxx注解,在其内部用@Import引入一个ImportSelector实现类。2. 该ImportSelector实现类负责读取配置文件(类似spring.factories),加载候选配置类。3. 基于@Conditional注解和ConditionEvaluator实现一个条件筛选机制。4. 在ImportSelector中将筛选后的配置类全名返回,Spring 容器会自动处理它们。 - 追问 1:在这个过程中,
BeanDefinitionRegistryPostProcessor是必要的吗?
回答:不必自己实现,但整个机制是建立在它之上的。@Import注解的处理是由 Spring 内置的ConfigurationClassPostProcessor(一个 BDRPP)完成的。 - 追问 2:你如何设计条件筛选?
回答:我会定义一个@MyConditional注解,并实现一个MyConditionMatcher来读取注解的元数据并检查当前上下文(类路径、已有Bean等)。ImportSelector在返回类名之前,会遍历候选类并调用这些Matcher进行筛选。 - 追问 3:如何让用户方便地注册他们的配置类?
回答:借鉴spring.factories的设计,约定一个配置文件(比如META-INF/my-autoconfig.properties),使用键值对方式,让用户将配置类全名写在该文件中。我的ImportSelector会扫描所有这类文件并加载。 - 加分回答:能讨论到实现一个 SPI 机制时的类加载器隔离问题,以及如何通过
@Order等注解处理配置类的加载顺序。
- 标准回答:需要组合Spring核心组件:1. 创建一个
-
问题:请描述
ApplicationContextInitializer、SpringApplicationRunListener和BeanFactoryPostProcessor的执行顺序和用途。- 标准回答:
SpringApplicationRunListener:执行顺序最早,贯穿run()全生命周期。ApplicationContextInitializer:在ApplicationContext创建后、refresh()之前被调用。BeanFactoryPostProcessor:在refresh()的invokeBeanFactoryPostProcessors阶段被调用。
- 追问 1:如果想在 Bean 实例化之前修改 Bean 定义,应该用哪个?
回答:BeanFactoryPostProcessor(特别是其子接口BeanDefinitionRegistryPostProcessor)。 - 追问 2:如果想在环境准备后、上下文创建前修改环境参数,应该用哪个?
回答:EnvironmentPostProcessor或SpringApplicationRunListener的environmentPrepared事件。 - 追问 3:
ApplicationContextInitializer能用来做数据库初始化吗?
回答:不推荐。它执行时,BeanFactory甚至还没有初始化,无法拿到数据库连接池等 Bean。它适用于修改上下文属性、激活 Profile 等非常早期的、与 Bean 无关的配置。 - 加分回答:能绘制出一个清晰的生命周期时间线,将这些扩展点精确地标注在
run()和refresh()的时序图上。
- 标准回答:
-
问题:
SpringApplicationBuilder的作用是什么?- 标准回答:用于构建具有父子上下文关系的 Spring Boot 应用,提供了流式 API。
- 追问 1:什么场景会用到父子上下文?
回答:比如 Spring Boot + Spring Cloud 的应用,Spring Cloud 的 Bootstrap 上下文就是应用的父上下文。 - 追问 2:
SpringApplicationBuilder和直接 newSpringApplication然后调用run()有什么区别?
回答:Builder 可以更优雅地管理多个SpringApplication实例和它们之间的关系,尤其适合构建分层上下文。直接使用SpringApplication通常用于单层上下文。
-
问题:Spring Boot 如何保证
CommandLineRunner和ApplicationRunner的执行顺序?- 标准回答:通过实现
org.springframework.core.Ordered接口或标注@Order注解来指定顺序,数字越小优先级越高。 - 追问 1:如果两个 Runner 都没指定顺序,执行顺序是怎样的?
回答:没有预定义的顺序,可能会受到 Bean 注册顺序的影响,但这是不确定的,不应依赖。 - 追问 2:某个 Runner 抛出异常会发生什么?
回答:这将导致run()方法退出,异常会被抛出,导致整个应用启动失败。SpringApplicationRunListener.failed()会被调用。 - 追问 3:
ApplicationRunner和CommandLineRunner参数有何不同?
回答:ApplicationRunner.run(ApplicationArguments args)参数提供了强大的选项参数(--foo=bar)和非选项参数(non-option-arg)的解析能力,而CommandLineRunner.run(String... args)只是传递了原始的字符串数组。
- 标准回答:通过实现
-
问题:如何在不使用 Spring Boot Actuator 的情况下,获取应用启动耗时?
- 标准回答:可以自定义一个
SpringApplicationRunListener,在starting()方法中启动StopWatch,在running()方法中停止计时并打印报表。 - 追问 1:还有什么其他方法可以监控启动时间?
回答:还可以利用 Spring Framework 的ApplicationStartedEvent和ApplicationReadyEvent来手动计时。 - 追问 2:
StopWatch的prettyPrint()方法输出的报表包含哪些信息?
回答:包含每个任务名称、耗时(毫秒)以及各任务耗时占总耗时的百分比。
- 标准回答:可以自定义一个
-
问题:
spring.factories文件中的键值对是什么意思?- 标准回答:键是接口的全限定名,值是该接口的实现类的全限定名列表(逗号分隔)。
- 追问 1:这个文件是如何被加载的?
回答:由SpringFactoriesLoader.loadFactories()或loadFactoryNames()方法加载。 - 追问 2:为什么 Spring Boot 2.7 开始引入
AutoConfiguration.imports文件?
回答:主要是为了性能优化和职责分离。spring.factories用途广泛,加载它时需要加载整个文件。而AutoConfiguration.imports专用于自动配置,加载更快,并且避免了不必要的类加载。这是向 Spring Boot 3.x 完全移除对spring.factories支持迈出的第一步。
-
问题:启动时常见的“Cannot determine embedded database driver class for database type NONE”错误怎么解决?
- 标准回答:这个错误是因为
DataSourceAutoConfiguration根据类路径推断需要配置嵌入式数据库,但找不到合适的驱动。解决方案:排除该自动配置类或在application.yml中明确配置数据源连接信息。 - 追问 1:如何排除某个特定的自动配置类?
回答:在@SpringBootApplication注解中使用exclude属性,如@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})。 - 追问 2:为什么会出现这个错误?
回答:因为项目中引入了 Spring Data JPA 或 MyBatis 等依赖,它们触发了DataSourceAutoConfiguration的条件,但你又没有配置任何数据库连接信息。 - 追问 3:如何排查是哪个自动配置类被触发了?
回答:在application.yml中设置logging.level.org.springframework.boot.autoconfigure=DEBUG,控制台会打印出详细的正负条件匹配报告。
- 标准回答:这个错误是因为
-
问题:
@Value注入和@ConfigurationProperties在底层是如何工作的?- 标准回答:两者都是依赖
Environment对象抽象。@Value是通过AutowiredAnnotationBeanPostProcessor在属性填充环节进行注入,其值解析依靠ConfigurableBeanFactory中的resolveEmbeddedValue方法链最终委托给PropertySourcesPropertyResolver。而@ConfigurationProperties是通过ConfigurationPropertiesBindingPostProcessor,它会在 Bean 初始化后,将Environment中的属性绑定到带有该注解的 Bean 上,利用BinderAPI 和ConversionService完成类型转换。 - 追问 1:哪个优先级更高,
@Value还是application.properties里的默认值?
回答:@Value是“消费”配置的一方,它没有优先级。它的值来源是Environment,而application.properties是其众多PropertySource之一。 - 追问 2:
@ConfigurationProperties如何做到类型安全的?
回答:它利用了 Spring 的ConversionService,该服务前面有各种Converter,可以将 String 转换成 Duration, DataSize 等多种复杂类型。 - 追问 3:如果属性名在文件中是
app-name,Java 字段应该怎么写,为什么?
回答:应该写为appName,这是宽松绑定(Relaxed Binding),@ConfigurationProperties支持将短横线隔开的命名映射到驼峰命名的字段。
- 标准回答:两者都是依赖
-
问题:Spring Boot 应用启动快结束时,控制台输出的彩色日志和启动信息是在哪个阶段实现的?
- 标准回答:日志的 Banner 输出发生在
SpringApplication.run()的最早期,由Banner接口及其实现类(如SpringBootBanner)完成。而“Started XxxApplication in X.XX seconds” 这句日志是在callRunners()之后,由logStarted()方法输出的,具体时机在SpringApplicationRunListeners.started()回调完成后。 - 追问 1:如何自定义 Banner?
回答:在resources目录下放一个banner.txt文件,或者编程式地通过SpringApplication.setBanner()设置。 - 追问 2:可以关闭这个日志输出吗?
回答:可以,通过SpringApplication.setBannerMode(Banner.Mode.OFF)或setLogStartupInfo(false)。
- 标准回答:日志的 Banner 输出发生在
-
问题:如果在一个
ApplicationContextInitializer中setAdditionalProfiles("dev"),和在application.yml中设定spring.profiles.active: dev,哪个优先级高?- 标准回答:
ApplicationContextInitializer中的设置优先级更高。 - 追问 1:为什么?
回答:因为ApplicationContextInitializer是在Environment整合application.yml之后执行的,它可以覆盖之前的任何设置。实际上,spring.profiles.active本身也是从application.yml或别的源中读取的。Initializer中的编程式addActiveProfile是最后写入的。 - 追问 2:这会造成什么问题?
回答:可能会造成“幽灵配置”,即团队里有人在代码或内部二方库的spring.factories里硬编码了 profile,导致排查配置问题时非常困难。
- 标准回答:
-
问题:如果在
onRefresh()阶段,也就是 Web 服务器启动时发生 OOM,会发生什么?failed()监听器会被调用吗?- 标准回答:
failed()监听器会被调用。 - 追问 1:它的流程是怎样的?
回答:AbstractApplicationContext.refresh()内部会catch住异常,然后关闭上下文并重新throw。这个异常会传播到SpringApplication.run()方法,其catch块会handleRunFailure(),在这个方法中会调用listeners.failed(context, exception)。 - 追问 2:在这种场景下,
context对象可用吗?
回答:可能处于部分初始化的状态,在failed()监听器中使用它时必须非常小心,做好判空和状态检查。
- 标准回答:
-
系统设计题:请设计一个 Spring Boot 应用启动分析工具,它能精确测量每个自动配置类的加载耗时,并识别出最耗时的几个 Bean 初始化过程。
- 标准回答:
- 测量自动配置耗时:利用
SpringApplicationRunListener和AutoConfigurationImportListener。在contextLoaded事件时,注册一个定制的BeanFactoryPostProcessor。这个BDRPP在postProcessBeanDefinitionRegistry开始时计时。通过监听AutoConfigurationImportEvent,可以知道哪些类被导入了。但这只能得到整体的invokeBeanFactoryPostProcessors耗时。更细粒度的话,需要自定义一个代理ConfigurationClassPostProcessor来做耗时记录,实现复杂但可行。 - 识别最耗时的 Bean 初始化:实现一个
BeanPostProcessor。在postProcessBeforeInitialization中记录开始时间,在postProcessAfterInitialization中记录耗时。将这些数据存入一个ConcurrentHashMap。 - 数据可视化:待到
ApplicationReadyEvent事件触发后,用一个ApplicationRunner将所有耗时数据读取出来,按耗时排序,输出到日志或通过一个 HTTP 端点暴露,完成分析。
- 测量自动配置耗时:利用
- 追问 1:你的 BeanPostProcessor 会对业务 Bean 有侵入性吗?
回答:没有侵入性,它只是一个容器级的后处理器,对业务 Bean 的代码是透明的。 - 追问 2:如何避免你的计时工具本身成为性能瓶颈?
回答:使用高并发容器如ConcurrentHashMap,并尽量保持计时逻辑极简,避免在BeanPostProcessor中进行复杂的日志打印或 IO 操作,只在最后统一处理和输出。 - 追问 3:如何设计使这个工具模块化,不使用时不影响启动速度?
回答:可以将它封装成一个独立的 starter,内部使用@ConditionalOnProperty或@ConditionalOnClass进行开关控制,只有当引入这个 starter 并设置了特定配置项(如my.startup.analysis.enabled=true)时,才会加载并启用分析组件。
- 标准回答:
延伸阅读
- 《Spring Boot 编程思想》 - 小马哥 (mercyblitz): 深入 Spring Boot 源码的必读经典,对自动配置和启动流程有极为详尽的剖析。
- Spring Boot 官方参考文档 (第十节: SpringApplication) : 最权威的第一手资料,详细解释了
SpringApplication的各项配置和特性。- Spring Framework 官方源码
AbstractApplicationContext.refresh(): 阅读其方法上的 Javadoc 和源码注释,是理解 Spring 核心容器生命周期的关键。- 《Spring 揭秘》 - 王福强: 虽然有些年头,但对 IoC、AOP 和容器设计的底层逻辑讲解依然非常透彻。
- 博客: "How Spring Boot’s Autoconfiguration Works" (类似 Baeldung 上的专题文章): 此类文章通常配有清晰的图和时序,有助于快速建立全局观。
展望 Spring Boot 3.x 启动流程变化 Spring Boot 3.x 基于 Spring Framework 6,其启动流程有了革命性的变化,主要体现在 AOT (Ahead-Of-Time) 编译 方面。传统的动态启动模型(JVM 启动 → 推断 → 加载配置 → 解析 → 实例化)正在被优化。借助 GraalVM
native-image技术,Spring Boot 3.x 应用可以在构建时完成大部分启动阶段的“计算”,将推断、配置解析乃至 Bean 定义的注册提前完成,生成可直接运行的本地映像。此外,spring.factories文件已被正式废弃,完全由AutoConfiguration.imports文件替代,进一步简化了 SPI 机制并提升了性能。这些激动人心的变化将在我们后续的 AOT 专篇 中详细展开。
附录:Spring Boot 启动流程速查表
| 启动阶段 | 核心方法 | 关键扩展点 | 使用的设计模式 | 关联系列篇章 |
|---|---|---|---|---|
| 1. 构造与准备 | SpringApplication 构造器 | ApplicationContextInitializerApplicationListenerSpringFactoriesLoader | SPI 机制 | 第 9 篇: SPI与插件化 |
| 2. 全生命周期 | SpringApplication.run() | SpringApplicationRunListener | 观察者模式 | 第 11 篇: 设计模式全景 |
| 3. 环境准备 | prepareEnvironment() | EnvironmentPostProcessorMutablePropertySources | 策略模式 | 第 1 篇: 容器抽象 第 10 篇: 类型转换 |
| 4. 上下文创建与准备 | prepareContext() | ApplicationContextInitializer | 工厂方法模式 | 第 1 篇: 容器抽象 第 7 篇: 扩展点体系 |
| 5. 核心刷新 | refreshContext() | BeanFactoryPostProcessorBeanPostProcessor@Import 选择器 | 模板方法模式 | 第 2, 7, 8, 9, 11 篇 |
| 6. Web服务器启动 | refreshContext() -> onRefresh() | ServletWebServerFactoryTomcatWebServer 等 | 模板方法模式 | 第 11 篇: 设计模式全景 |
| 7. 后置回调 | callRunners() | ApplicationRunnerCommandLineRunner | 命令模式 (变体) | 第 7 篇: 扩展点体系 |