概述
本文将不仅拆解启动流程,更将用我们在前文所学的 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 核心组件,而非凭空创造。
文章组织架构图
架构图分层说明
-
总览说明 :全文共 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() 方法的内部实现,将所有复杂性都委托给了同一个类的实例方法。我们将该方法的核心流程抽象为六个关键阶段,通过泳道图来直观展示。
-
图表主旨概括 :本图展示了
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来精确记录每个阶段的耗时。java// 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文件中添加:propertiesorg.springframework.boot.SpringApplicationRunListener=\ com.example.demo.listener.MyStartupTimingListener启动应用,即可在控制台看到详细的阶段耗时分析报告。这验证了
SpringApplicationRunListener的观察者模式机制,其回调时机与我们分析的启动阶段完全一致。
2. 启动前的准备:SpringApplication 的构造、推断与可定制性
2.1 SpringApplication 构造过程源码全解
SpringApplication.run() 是一个静态方法,它内部会先创建一个 SpringApplication 实例,然后调用其 run() 方法。因此,启动的第一个关键步骤就是 SpringApplication 的构造。
第一步:推断应用类型 WR-->>SA: 返回 WebApplicationType (SERVLET/REACTIVE/NONE) SA->>SA: setInitializers(null)
第二步:加载Initializer SA->>SFL: loadFactoryNames(ApplicationContextInitializer.class, classLoader) SFL->>FS: 读取 META-INF/spring.factories FS-->>SFL: 返回 Initializer 全类名列表 SFL-->>SA: 实例化并设置 SA->>SA: setListeners(null)
第三步:加载Listener SA->>SFL: loadFactoryNames(ApplicationListener.class, classLoader) SFL->>FS: 读取 META-INF/spring.factories FS-->>SFL: 返回 Listener 全类名列表 SFL-->>SA: 实例化并设置 SA->>SA: deduceMainApplicationClass()
第四步:推断主配置类 SA-->>User: 构造完成
下面是 SpringApplication 构造器的核心源码(为简化已去除辅助逻辑):
java
// 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 对象。
1. 命令行参数 PREP_ENV->>MPS: addLast(servletConfigInitParams)
2. Servlet上下文初始化参数 PREP_ENV->>MPS: addLast(servletContextInitParams)
3. Servlet上下文参数 PREP_ENV->>MPS: addLast(systemProperties)
4. 系统属性 PREP_ENV->>MPS: addLast(systemEnvironment)
5. 操作系统环境变量 PREP_ENV->>MPS: addLast(random)
6. RandomValuePropertySource PREP_ENV->>MPS: addLast(applicationConfig)
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
java
// 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在环境准备完成后动态添加一个配置源。java// 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中添加:propertiesorg.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 容器。
实例化具体类 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
步骤1: 调用ApplicationContextInitializer loop 每个 Initializer INIT->>CTX: initialize(context) end PREP->>LIST: contextPrepared(context)
(广播事件) PREP->>CTX: load(primarySources)
步骤2: 将主配置类注册为BeanDefinition PREP->>CTX: registerSingleton(...)
步骤3: 注册默认单例Bean PREP->>LIST: contextLoaded(context)
(广播事件) 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 篇) :
-
内联示例:自定义
ApplicationContextInitializerjava// 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中添加:propertiesorg.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 服务器的启动逻辑。
用 Spring 核心知识解读:这个 12 步模板方法是整个 Spring 生态的基础设施(第 2、11 篇),它定义了 IoC 容器启动的固定算法骨架。Spring Boot 作为这个模板方法的"用户"或"子类",通过在特定步骤(主要是第 5 步和第 9 步)使用的扩展机制,来注入自己的自动配置和 Web 服务器启动逻辑。
5.2 自动配置触发过程详解
这是模板方法中第 5 步 (invokeBeanFactoryPostProcessors) 的核心内容,也是 @EnableAutoConfiguration 最终发挥作用的地方。
(BDRPP) participant Parser as ConfigurationClassParser participant AIS as AutoConfigurationImportSelector participant Cond as ConditionalEvaluator Core->>BDRPP: postProcessBeanDefinitionRegistry(registry)
Step 1: BDRPP被执行 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()
Step 2: 触发自动配置选择器 AIS->>AIS: getCandidateConfigurations()
Step 3: 加载候选配置 Note over AIS: 从META-INF/spring/
org.springframework.boot
.autoconfigure.AutoConfiguration.imports
和 spring.factories 读取 AIS->>Cond: 对每个候选配置进行条件筛选
Step 4: 按条件过滤 Cond->>Cond: 检查@ConditionalOnClass,
@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 核心源码解读:自动配置选择器
java
// 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 服务器的启动------模板方法钩子
模板方法钩子被调用 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)
(注册到ServletContext) end WS->>WS: start()
Tomcat正式启动,监听端口 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打印所有属性源。java@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 启动 → 推断 → 加载配置 → 解析 → 实例化)正在被优化。借助 GraalVMnative-image技术,Spring Boot 3.x 应用可以在构建时完成大部分启动阶段的"计算",将推断、配置解析乃至 Bean 定义的注册提前完成,生成可直接运行的本地映像。此外,spring.factories文件已被正式废弃,完全由AutoConfiguration.imports文件替代 ,进一步简化了 SPI 机制并提升了性能。这些激动人心的变化将在我们后续的 AOT 专篇 中详细展开。
附录:Spring Boot 启动流程速查表
| 启动阶段 | 核心方法 | 关键扩展点 | 使用的设计模式 | 关联系列篇章 |
|---|---|---|---|---|
| 1. 构造与准备 | SpringApplication 构造器 |
ApplicationContextInitializer ApplicationListener SpringFactoriesLoader |
SPI 机制 | 第 9 篇: SPI与插件化 |
| 2. 全生命周期 | SpringApplication.run() |
SpringApplicationRunListener |
观察者模式 | 第 11 篇: 设计模式全景 |
| 3. 环境准备 | prepareEnvironment() |
EnvironmentPostProcessor MutablePropertySources |
策略模式 | 第 1 篇: 容器抽象 第 10 篇: 类型转换 |
| 4. 上下文创建与准备 | prepareContext() |
ApplicationContextInitializer |
工厂方法模式 | 第 1 篇: 容器抽象 第 7 篇: 扩展点体系 |
| 5. 核心刷新 | refreshContext() |
BeanFactoryPostProcessor BeanPostProcessor @Import 选择器 |
模板方法模式 | 第 2, 7, 8, 9, 11 篇 |
| 6. Web服务器启动 | refreshContext() -> onRefresh() |
ServletWebServerFactory TomcatWebServer 等 |
模板方法模式 | 第 11 篇: 设计模式全景 |
| 7. 后置回调 | callRunners() |
ApplicationRunner CommandLineRunner |
命令模式 (变体) | 第 7 篇: 扩展点体系 |