Spring Boot 4.1.1 深度源码解析-第 04 章 · SpringApplication 全景:run() 的十二步状态机
从本章开始,我们进入运行时 。前三章(卷一)讲的是 Boot 怎么被构建、怎么组织、怎么分发;从本章起(卷二),我们讲 Boot 怎么跑起来。
本章是全系列最重要的单章之一。
SpringApplication是 Spring Boot 的入口类,也是整个框架的控制中枢。它只有 1876 行,但密度极高------几乎每一行都有设计意图。本章会逐字段、逐方法地拆解它。源码路径:
core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java
本章导读
如果你问"Spring Boot 到底做了什么",最浓缩的答案是:
Spring Boot = Spring Framework + 三个"自动化":自动配置 Bean 、自动装配环境 、自动启动容器。
而这三个"自动"的调度中心,就是 SpringApplication。
本章的目标不是让你"记住"这个类,而是让你理解:
- 启动过程的完整阶段划分------每一步做了什么、为什么需要这一步
- 每一步之间的数据依赖------前一步的产物如何成为后一步的输入
- 扩展点在哪里------哪些步骤允许用户介入,怎么介入
- 失败怎么处理------启动失败时 Boot 做了什么
一个贯穿本章的视角
读 SpringApplication 时,请始终带着这个问题:
如果让我从头写一个"启动 Spring 应用的工具类",我会怎么做?会遇到什么问题?
然后看 Boot 是怎么解决的。这样读,你会发现很多"看似多余"的代码其实都是被真实问题逼出来的。
1. 类的全貌
1.1 字段清单
SpringApplication 的字段(去掉静态常量与 logger):
java
private final Set<Class<?>> primarySources; // 主配置源
private @Nullable Class<?> mainApplicationClass; // main 方法所在类
private boolean addCommandLineProperties = true; // 是否添加命令行属性
private boolean addConversionService = true; // 是否添加转换服务
private @Nullable Banner banner; // 自定义 Banner
private @Nullable ResourceLoader resourceLoader; // 资源加载器
private @Nullable BeanNameGenerator beanNameGenerator; // Bean 名生成器
private @Nullable ConfigurableEnvironment environment; // 自定义环境
private boolean headless = true; // 是否 headless
private List<ApplicationContextInitializer<?>> initializers; // 上下文初始化器
private List<ApplicationListener<?>> listeners; // 应用监听器
private @Nullable Map<String, Object> defaultProperties; // 默认属性
private final List<BootstrapRegistryInitializer> bootstrapRegistryInitializers; // 引导注册初始化器
private Set<String> additionalProfiles = Collections.emptySet(); // 额外 Profile
private boolean isCustomEnvironment; // 是否自定义 Environment
private @Nullable String environmentPrefix; // 环境前缀
private ApplicationContextFactory applicationContextFactory; // 上下文工厂
private ApplicationStartup applicationStartup; // 启动度量
final ApplicationProperties properties; // 配置载体
这 20 个字段可以分为五组:
| 组别 | 字段 | 作用 |
|---|---|---|
| 输入源 | primarySources、mainApplicationClass |
从哪加载 Bean 定义 |
| 环境配置 | environment、isCustomEnvironment、environmentPrefix、defaultProperties、additionalProfiles |
环境怎么建 |
| 扩展点 | initializers、listeners、bootstrapRegistryInitializers、applicationContextFactory、beanNameGenerator、resourceLoader |
用户可介入的位置 |
| 行为开关 | addCommandLineProperties、addConversionService、headless、banner |
小功能开关 |
| 配置载体 | properties(ApplicationProperties) |
spring.main.* 的绑定目标 |
| 度量 | applicationStartup |
启动性能采集 |
1.2 final 与 @Nullable 的使用
注意哪些是 final:
java
private final Set<Class<?>> primarySources;
private final List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;
final ApplicationProperties properties;
只有三个。primarySources 和 properties 是"必须有"的核心,bootstrapRegistryInitializers 在构造时确定后只增不减(通过 addBootstrapRegistryInitializer)。
其余字段都可变------因为它们可能被 setter 改,也可能在运行中被重新赋值(如 environment)。
@Nullable 的分布:
java
private @Nullable Class<?> mainApplicationClass;
private @Nullable Banner banner;
private @Nullable ResourceLoader resourceLoader;
private @Nullable BeanNameGenerator beanNameGenerator;
private @Nullable ConfigurableEnvironment environment;
private @Nullable Map<String, Object> defaultProperties;
private @Nullable String environmentPrefix;
注意 applicationContextFactory 和 applicationStartup 没有 @Nullable ------它们有默认值(ApplicationContextFactory.DEFAULT、ApplicationStartup.DEFAULT),永不为 null。
这呼应第 01 章 §1.8 讲的点:4.x 的 @Nullable 是精确的,没标的就是非空。
1.3 静态成员
java
public static final String BANNER_LOCATION_PROPERTY_VALUE = SpringApplicationBannerPrinter.DEFAULT_BANNER_LOCATION;
public static final String BANNER_LOCATION_PROPERTY = SpringApplicationBannerPrinter.BANNER_LOCATION_PROPERTY;
private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";
private static final Log logger = LogFactory.getLog(SpringApplication.class);
static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();
private static final ThreadLocal<SpringApplicationHook> applicationHook = new ThreadLocal<>();
四个值得注意:
(1) Banner 相关常量的委托
java
public static final String BANNER_LOCATION_PROPERTY_VALUE = SpringApplicationBannerPrinter.DEFAULT_BANNER_LOCATION;
public static final String BANNER_LOCATION_PROPERTY = SpringApplicationBannerPrinter.BANNER_LOCATION_PROPERTY;
SpringApplication 把 Banner 常量的定义委托 给了 SpringApplicationBannerPrinter。这是为了避免两处定义不一致。
这是一个小但重要的习惯:常量的"唯一真相源"应该只有一个,其他地方引用它。
(2) shutdownHook 是 static
java
static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();
为什么是静态的? 因为 JVM 的关闭钩子是全局 的。如果每个 SpringApplication 实例都注册一个 shutdown hook,会导致:
- 多个 hook 并发执行,顺序不确定
- 无法统一处理"多个上下文"的关闭顺序
用一个静态单例 SpringApplicationShutdownHook,Boot 可以:
- 只向 JVM 注册一个
Runtime.getRuntime().addShutdownHook(...) - 内部维护多个
ConfigurableApplicationContext的列表 - 按正确顺序关闭它们
这个设计对多上下文场景(如 Actuator 管理端口子上下文)至关重要。
(3) applicationHook 是 ThreadLocal
java
private static final ThreadLocal<SpringApplicationHook> applicationHook = new ThreadLocal<>();
SpringApplicationHook 允许你在不修改 main 方法的前提下,拦截一次应用启动(用于测试)。
用 ThreadLocal 是因为:测试可能并发执行,每个测试线程可能有自己的 hook。ThreadLocal 保证了隔离性。
withHook 方法:
java
public static <T> T withHook(SpringApplicationHook hook, ThrowingSupplier<T> action) {
applicationHook.set(hook);
try {
return action.get();
}
finally {
applicationHook.remove();
}
}
注意 finally 中的 remove() ------ 这是 ThreadLocal 使用的标准姿势,防止内存泄漏(尤其在线程池场景)。
(4) logger 用的是 SpringApplication 的日志类别
java
private static final Log logger = LogFactory.getLog(SpringApplication.class);
但这个 logger 只用于"框架自身"的日志。应用的日志用的是:
java
protected Log getApplicationLog() {
if (this.mainApplicationClass == null) {
return logger;
}
return LogFactory.getLog(this.mainApplicationClass);
}
为什么要区分? 因为用户看到的日志应该以应用的类名为 logger 名。例如:
yaml
2026-10-17 INFO com.example.MyApplication : Started MyApplication in 2.3 seconds
而不是:
yaml
2026-10-17 INFO o.s.boot.SpringApplication : Started ...
这样用户在配置 logging.level.com.example=DEBUG 时才能生效。这是一个容易被忽略但对用户体验很重要的细节。
2. 构造阶段:new SpringApplication(...)
2.1 构造函数源码
java
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(@Nullable ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "'primarySources' must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.properties.setWebApplicationType(WebApplicationType.deduce());
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
六步,我们逐个讲。
2.2 第一步:primarySources 用 LinkedHashSet
java
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
为什么用 LinkedHashSet 而不是 List 或 HashSet?
Set------ 去重。如果用户不小心传了两次同一个类,不会重复注册。Linked------ 保持顺序。Bean 定义的注册顺序可能影响某些行为(虽然 Boot 会排序,但保留用户意图的顺序更安全)。
这是一个"既去重又保序"的标准选择。
2.3 第二步:WebApplicationType.deduce()
java
this.properties.setWebApplicationType(WebApplicationType.deduce());
这是 4.x 的一个新变化 (@since 4.0.1)。完整源码:
java
public static WebApplicationType deduce() {
for (Deducer deducer : SpringFactoriesLoader.forDefaultResourceLocation().load(Deducer.class)) {
WebApplicationType deduced = deducer.deduceWebApplicationType();
if (deduced != null) {
return deduced;
}
}
return isServletApplication() ? WebApplicationType.SERVLET : WebApplicationType.NONE;
}
private static boolean isServletApplication() {
for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(servletIndicatorClass, null)) {
return false;
}
}
return true;
}
private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
(1) 4.0.1 新增的 Deducer SPI
java
@FunctionalInterface
public interface Deducer {
@Nullable WebApplicationType deduceWebApplicationType();
}
这是一个 4.x 的重要扩展点。
在 3.x,deduce() 是硬编码的:
java
// 3.x(对照)
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
即:3.x 在 Boot 核心里硬编码了"WebFlux 的类名"、"WebMVC 的类名"、"Jersey 的类名"。
4.x 的问题 :WebFlux 已经被拆到 module/spring-boot-webflux 了!Boot 核心不应该(也不方便)知道 WebFlux 的类名。
4.x 的解法 :引入 Deducer SPI。spring-boot-webflux 模块自己提供一个 Deducer 实现,通过 spring.factories 注册。Boot 核心只负责"遍历所有 Deducer,取第一个非 null 结果"。
这又是模块化的必然结果------当能力被拆出去后,原来硬编码的知识必须变成可扩展的 SPI。
这个模式值得记住:
当你把某个能力模块化后,原先在核心中对该能力的"硬编码判断"必须替换为 SPI。
(2) 默认逻辑:两个指示类
java
private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
两个类都必须在 classpath 上才算 Servlet 应用:
jakarta.servlet.Servlet------ Servlet API 存在ConfigurableWebApplicationContext------ Spring 的 Web 上下文存在(来自 spring-web)
只有前者不足以判定(可能只是引入了 servlet-api 而不用 Spring Web)。
注意 :这里的默认逻辑只区分 SERVLET 和 NONE ,不区分 REACTIVE。REACTIVE 由 spring-boot-webflux 的 Deducer 判定。
(3) RuntimeHints
java
static class WebApplicationTypeRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) {
registerTypeIfPresent(servletIndicatorClass, classLoader, hints);
}
}
private void registerTypeIfPresent(String typeName, @Nullable ClassLoader classLoader, RuntimeHints hints) {
if (ClassUtils.isPresent(typeName, classLoader)) {
hints.reflection().registerType(TypeReference.of(typeName));
}
}
}
为什么需要这个? 因为 ClassUtils.isPresent(...) 在 Native Image 中需要反射提示。如果不注册,Native 环境下类探测会失败。
注意 registerTypeIfPresent:只在类确实存在时才注册。这避免了为不存在的类生成无用 hints(会让 Native 镜像变大)。
这是一个很好的实现细节:hints 的注册也要"按需"。
2.4 第三步:加载 BootstrapRegistryInitializer
java
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
从 spring.factories 加载所有 BootstrapRegistryInitializer。
BootstrapRegistry 是什么? 见第 08 章。简单说:它是在 ApplicationContext 创建之前可用的一个"临时注册表",用于在极早期(如 ConfigData 加载时)共享对象。
注意这里用了 new ArrayList<>(...) ------ 因为后续可能通过 addBootstrapRegistryInitializer 追加,所以需要可变列表。
2.5 第四、五步:加载 ApplicationContextInitializer 与 ApplicationListener
java
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
两个都从 spring.factories 加载,并通过 setter 转成可变 ArrayList:
java
public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
this.initializers = new ArrayList<>(initializers);
}
@SuppressWarnings({ "unchecked", "rawtypes" }) 是因为泛型擦除的强制转换。
重要 :这两个集合在构造时从 spring.factories 填充,之后用户还可以:
setInitializers(...)覆盖addInitializers(...)追加
2.6 第六步:deduceMainApplicationClass() ------ StackWalker 的妙用
java
private @Nullable Class<?> deduceMainApplicationClass() {
return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(this::findMainClass)
.orElse(null);
}
private Optional<Class<?>> findMainClass(Stream<StackFrame> stack) {
return stack.filter((frame) -> Objects.equals(frame.getMethodName(), "main"))
.findFirst()
.map(StackWalker.StackFrame::getDeclaringClass);
}
这是本章最优雅的一段代码。
它解决什么问题?
Boot 需要知道"哪个类是 main 类",用途:
- 日志的 logger 名(
getApplicationLog()) - 启动信息中显示的类名
- AOT 生成初始化器时的类名(
mainApplicationClass.getName() + "__ApplicationContextInitializer") ApplicationInfoPropertySource读取版本
怎么知道的?
用 StackWalker 遍历当前调用栈,找到名为 main 的方法,取其声明类。
为什么可行?因为 new SpringApplication(...) 通常就是在 main 方法里调用的:
java
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args); // ← 这里
}
所以调用栈是:
vbnet
MyApplication.main(String[]) ← 找到了!
SpringApplication.run(Class, String[])
...
为什么用 StackWalker(Java 9+)而不是传统方式?
传统做法(Boot 2.x 用过):
java
// 2.x 做法(对照)
private Class<?> deduceMainApplicationClass() {
try {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
这个方法有两个问题:
- 创建异常对象开销大 (
new RuntimeException().getStackTrace()需要填充整个栈) Class.forName可能失败(类加载器问题)
StackWalker 的优势:
- 惰性 :
walk()是流式的,可以提前终止(找到就findFirst()) RETAIN_CLASS_REFERENCE:直接得到Class对象,不需要Class.forName- 更快:JVM 层面的支持,比构造异常轻得多
这是一个"用新 API 替换老 hack"的典范 。如果你在项目里看到 new Exception().getStackTrace() 做栈分析,可以考虑换成 StackWalker。
边界情况
- 如果
main方法不在栈上(例如从测试框架启动),返回null mainApplicationClass为 null 时,Boot 会降级使用SpringApplication自己的 logger
这就是 @Nullable 的来源。
3. run() 方法:主流程全景
3.1 完整源码
java
public ConfigurableApplicationContext run(String... args) {
Startup startup = Startup.create();
if (this.properties.isRegisterShutdownHook()) {
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStarted = startup.started();
if (this.properties.isLogStartupInfo()) {
new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
}
listeners.started(context, timeTakenToStarted);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, listeners);
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, null);
}
return context;
}
3.2 十二步分解
我把 run() 拆成十二个阶段。这个分解是本系列后续所有章节的骨架。
scss
┌── 阶段 0:启动准备 ────────────────────────────────┐
│ ① Startup.create() 启动计时(CRaC 感知) │
│ ② shutdownHook.enable... 启用关闭钩子 │
│ ③ createBootstrapContext() 创建引导上下文 │
│ ④ configureHeadless() 设置 java.awt.headless│
└──────────────────────────────────────────────────┘
↓
┌── 阶段 1:环境准备 ────────────────────────────────┐
│ ⑤ getRunListeners(args) 获取运行监听器 │
│ ⑥ listeners.starting() 发布 starting 事件 │
│ ⑦ prepareEnvironment() ★ 构建 Environment │
│ ⑧ printBanner() 打印 Banner │
└──────────────────────────────────────────────────┘
↓
┌── 阶段 2:上下文创建与准备 ──────────────────────────┐
│ ⑨ createApplicationContext() ★ 创建 ApplicationContext│
│ ⑩ prepareContext() ★ 准备上下文(初始化器等)│
└──────────────────────────────────────────────────┘
↓
┌── 阶段 3:刷新与完成 ──────────────────────────────┐
│ ⑪ refreshContext() ★ 刷新(真正的 IoC 启动)│
│ ⑫ started + callRunners + ready 完成 │
└──────────────────────────────────────────────────┘
注意:官方文档常说是"九步"或"十步",我这里拆得更细(十二步),因为每个细分步骤都值得单独理解。
3.3 阶段 0 的四步
① Startup startup = Startup.create()
java
static Startup create() {
ClassLoader classLoader = Startup.class.getClassLoader();
return (ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", classLoader)
&& ClassUtils.isPresent("org.crac.management.CRaCMXBean", classLoader))
? new CoordinatedRestoreAtCheckpointStartup() : new StandardStartup();
}
CRaC(Coordinated Restore at Checkpoint) 是 OpenJDK 的一个特性:允许把 JVM 的状态"快照"到磁盘,之后从快照快速恢复(启动时间从秒级降到毫秒级)。
Boot 4 支持 CRaC:启动时会检测是否运行在 CRaC 环境,如果是,则:
- 用
getUptimeSinceRestore()而不是getUptime() - 日志输出 "Restored" 而不是 "Started"
java
private static final class CoordinatedRestoreAtCheckpointStartup extends Startup {
private final StandardStartup fallback = new StandardStartup();
@Override
protected @Nullable Long processUptime() {
Long uptime = CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore();
return (uptime >= 0) ? uptime : this.fallback.processUptime();
}
@Override
protected String action() {
return (restoreTime() >= 0) ? "Restored" : this.fallback.action();
}
private long restoreTime() {
return CRaCMXBean.getCRaCMXBean().getRestoreTime();
}
@Override
protected long startTime() {
long restoreTime = restoreTime();
return (restoreTime >= 0) ? restoreTime : this.fallback.startTime();
}
}
注意 fallback 模式 :当 CRaC API 返回负值(表示"未恢复")时,回退到标准实现。这是对可选运行时特性的标准处理方式。
同时注意 import:
java
import org.crac.management.CRaCMXBean;
这是编译期依赖 !也就是说 SpringApplication 编译时就需要 org.crac 包。Boot 通过在 build.gradle 中用 compileOnly 引入它(否则所有用户都要带这个依赖),运行时用 ClassUtils.isPresent 探测。
这是一个"编译期可选依赖"的典型处理:编译需要,运行可选。
② shutdownHook.enableShutdownHookAddition()
java
if (this.properties.isRegisterShutdownHook()) {
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
注意:这里只是"启用",不是"注册" 。真正的注册发生在 refreshContext 中:
java
private void refreshContext(ConfigurableApplicationContext context) {
if (this.properties.isRegisterShutdownHook()) {
shutdownHook.registerApplicationContext(context);
}
refresh(context);
}
为什么要分两步? 因为:
enableShutdownHookAddition()是一个全局开关 ,告诉SpringApplicationShutdownHook"后续可以接受注册"- 如果启动过程中失败了(还没到
refreshContext),就不应该注册任何上下文
这个"先启用、后注册"的设计避免了"启动失败却注册了关闭钩子"的问题。
③ createBootstrapContext()
java
private DefaultBootstrapContext createBootstrapContext() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
return bootstrapContext;
}
创建 DefaultBootstrapContext 并调用所有 BootstrapRegistryInitializer。
BootstrapContext 的生命周期:
- 创建:run() 最开始
- 使用:环境准备阶段(ConfigData 加载时用它共享对象)
- 关闭:
prepareContext中的bootstrapContext.close(context)
java
// prepareContext 中
bootstrapContext.close(context);
这是一个有意思的设计 :BootstrapContext 是一个"短命"的容器,它存在的唯一目的是在 ApplicationContext 准备好之前,提供一个共享对象的地方。一旦 ApplicationContext 就绪,它就被关闭(并把其中注册的对象转移到主上下文)。
第 08 章详述。
④ configureHeadlessProperty()
java
private void configureHeadlessProperty() {
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}
注意 System.getProperty(key, defaultValue) 的用法 :如果用户已经设置了 java.awt.headless,则保留用户的值 ;否则设为 this.headless(默认 true)。
这是一个"不覆盖用户显式配置"的标准写法。
为什么 Boot 要设置这个? 因为服务器环境通常没有显示器。如果不设 java.awt.headless=true,某些 AWT 操作(如图片处理、字体渲染)会尝试连接 X Server 并失败。
3.4 阶段 1:环境准备
⑤ getRunListeners(args)
java
private SpringApplicationRunListeners getRunListeners(String[] args) {
ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
argumentResolver = argumentResolver.and(String[].class, args);
List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
argumentResolver);
SpringApplicationHook hook = applicationHook.get();
SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null;
if (hookListener != null) {
listeners = new ArrayList<>(listeners);
listeners.add(hookListener);
}
return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup);
}
三个要点:
(a) ArgumentResolver 构造 (第 01 章 §1.3.1 已述) 提供 SpringApplication 实例与 String[] args 作为构造参数。
(b) SpringApplicationHook 的注入 如果当前线程设置了 hook,它的 listener 会被追加到末尾。
为什么追加到末尾?因为 hook 的 listener 通常是"测试观察者",应该在真实的 listener 之后执行(观察到最终结果)。
(c) 包装成 SpringApplicationRunListeners 这是一个集合类,负责遍历调用 + ApplicationStartup 埋点。第 07 章详述。
⑥ listeners.starting(bootstrapContext, this.mainApplicationClass)
发布第一个事件。
⑦ prepareEnvironment(...) ------ 最复杂的一步
java
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(bootstrapContext, environment);
ApplicationInfoPropertySource.moveToEnd(environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
十行代码,九个动作 。这是整个 run() 中最密集的部分。逐个讲:
(1) getOrCreateEnvironment()
java
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
WebApplicationType webApplicationType = this.properties.getWebApplicationType();
ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(webApplicationType);
if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
environment = ApplicationContextFactory.DEFAULT.createEnvironment(webApplicationType);
}
return (environment != null) ? environment : new ApplicationEnvironment();
}
三级降级:
- 用户通过
setEnvironment(...)提供 → 直接用 applicationContextFactory能创建 → 用它- 否则 →
new ApplicationEnvironment()
注意第 2 级中的"双保险" :如果自定义 factory 返回 null,还尝试 DEFAULT factory;如果还 null,才用 ApplicationEnvironment。
(2) configureEnvironment(environment, args)
java
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
environment.setConversionService(new ApplicationConversionService());
}
configurePropertySources(environment, args);
configureProfiles(environment, args);
}
- 设置
ApplicationConversionService(Boot 的增强转换服务,支持Duration、DataSize等) - 配置 PropertySource
- 配置 Profile
configureProfiles 在 SpringApplication 中是空实现:
java
protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
}
这是一个模板方法,留给子类覆盖。第 06 章详述 PropertySource 的配置。
(3) ConfigurationPropertySources.attach(environment) ------ 关键
java
ConfigurationPropertySources.attach(environment);
这个方法把 Environment 中"扁平的" PropertySource 包装成"支持宽松绑定(relaxed binding)"的 ConfigurationPropertySourcesPropertySource。
为什么需要? 因为 Spring 的 Environment 只支持精确名查找(getProperty("server.port")),而 Boot 的配置绑定需要支持:
server.portSERVER_PORT(环境变量)server-port
attach() 之后,Binder 就能用统一的 ConfigurationPropertyName 做宽松匹配。
注意这行出现了两次(第一次在这里,第二次在方法末尾)。因为中间可能替换了 environment 对象(见动作 8),需要重新 attach。
(4) listeners.environmentPrepared(bootstrapContext, environment)
发布第二个事件 ,也是最重要的事件之一。ConfigData 加载(application.properties 的读取)就发生在这个事件的监听器中。
(5) ApplicationInfoPropertySource.moveToEnd(environment)
把 applicationInfo(应用版本、PID)移到末尾,使其优先级最低。
(6) DefaultPropertiesPropertySource.moveToEnd(environment)
把 defaultProperties(用户通过 setDefaultProperties 设置的)移到末尾。
为什么这两个都要 moveToEnd? 因为它们都是"兜底值",不应该覆盖用户在配置文件中的设置。
moveToEnd 是 Boot 中一个反复出现的惯用法。第 06 章详述 PropertySource 优先级。
(7) Assert.state(!environment.containsProperty("spring.main.environment-prefix"), ...)
java
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
禁止通过配置文件设置 spring.main.environment-prefix。
为什么?因为环境前缀(用于系统环境变量绑定,如 SPRING_SERVER_PORT)必须在环境创建之前就确定。如果允许从配置文件读,就产生了鸡生蛋问题:读配置文件需要环境,而环境的构造需要前缀。
这是一个"防御不可能实现的特性"的断言------与其让它静默失败,不如提前报错。
(8) bindToSpringApplication(environment)
java
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties));
}
catch (Exception ex) {
throw new IllegalStateException("Cannot bind to SpringApplication", ex);
}
}
把 spring.main.* 属性绑定到 ApplicationProperties。
这是 4.x 的重大改进 ------ 一行代码搞定所有 spring.main.* 属性(第 01 章 §8.3、第 05 章详述)。
(9) EnvironmentConverter 转换
java
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
如果环境的实际类型 与目标类型不同(例如目标需要 StandardServletEnvironment 但实际是 ApplicationEnvironment),则转换。
什么时候需要转换? 当 Web 类型是 SERVLET 时,需要 StandardServletEnvironment(它知道 servletContextInitParams 等 Servlet 特有的 PropertySource)。
注意 !this.isCustomEnvironment ------ 如果用户提供了自定义 Environment,不转换(尊重用户意图)。
(10) 再次 ConfigurationPropertySources.attach(environment)
因为 environment 可能已经被替换成新对象。
⑧ printBanner(environment)
java
private @Nullable Banner printBanner(ConfigurableEnvironment environment) {
if (this.properties.getBannerMode(environment) == Banner.Mode.OFF) {
return null;
}
ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
: new DefaultResourceLoader(null);
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
if (this.properties.getBannerMode(environment) == Mode.LOG) {
return bannerPrinter.print(environment, this.mainApplicationClass, logger);
}
return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
注意 getBannerMode(environment) 的实现 (ApplicationProperties):
java
Mode getBannerMode(Environment environment) {
if (this.bannerMode != null) {
return this.bannerMode;
}
String applicationPropertyName = LoggingSystemProperty.CONSOLE_STRUCTURED_FORMAT.getApplicationPropertyName();
Assert.state(applicationPropertyName != null, "applicationPropertyName must not be null");
boolean structuredLoggingEnabled = environment.containsProperty(applicationPropertyName);
return (structuredLoggingEnabled) ? Mode.OFF : Banner.Mode.CONSOLE;
}
这是一个 4.x 的细节 :如果启用了结构化日志 (logging.structured.format.console),Banner 会自动关闭。
为什么?因为结构化日志要求输出是纯 JSON/日志格式,Banner 的 ASCII 艺术字会破坏格式。
这是一个很贴心的小设计------用户开启结构化日志时不必记得还要关 Banner。
返回的 Banner 对象会被注册为单例(见后面的 prepareContext):
java
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
4. 阶段 2:上下文创建与准备
4.1 ⑨ createApplicationContext()
java
protected ConfigurableApplicationContext createApplicationContext() {
ConfigurableApplicationContext context = this.applicationContextFactory
.create(this.properties.getWebApplicationType());
Assert.state(context != null, "ApplicationContextFactory created null context");
return context;
}
很短,但背后是 ApplicationContextFactory 体系。
4.1.1 DefaultApplicationContextFactory
java
class DefaultApplicationContextFactory implements ApplicationContextFactory {
@Override
public @Nullable Class<? extends ConfigurableEnvironment> getEnvironmentType(
@Nullable WebApplicationType webApplicationType) {
return getFromSpringFactories(webApplicationType, ApplicationContextFactory::getEnvironmentType, null);
}
@Override
public @Nullable ConfigurableEnvironment createEnvironment(@Nullable WebApplicationType webApplicationType) {
return getFromSpringFactories(webApplicationType, ApplicationContextFactory::createEnvironment, null);
}
@Override
public ConfigurableApplicationContext create(@Nullable WebApplicationType webApplicationType) {
try {
return getFromSpringFactories(webApplicationType, ApplicationContextFactory::create,
this::createDefaultApplicationContext);
}
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
}
private ConfigurableApplicationContext createDefaultApplicationContext() {
if (!AotDetector.useGeneratedArtifacts()) {
return new AnnotationConfigApplicationContext();
}
return new GenericApplicationContext();
}
@Contract("_, _, !null -> !null")
private <T> @Nullable T getFromSpringFactories(@Nullable WebApplicationType webApplicationType,
BiFunction<ApplicationContextFactory, @Nullable WebApplicationType, @Nullable T> action,
@Nullable Supplier<T> defaultResult) {
for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,
getClass().getClassLoader())) {
T result = action.apply(candidate, webApplicationType);
if (result != null) {
return result;
}
}
return (defaultResult != null) ? defaultResult.get() : null;
}
}
核心是 getFromSpringFactories:
sql
遍历所有 ApplicationContextFactory(从 spring.factories 加载)
→ 调用它的 action(create / createEnvironment / getEnvironmentType)
→ 第一个返回非 null 的胜出
→ 全部返回 null 则用 defaultResult
这又是一个"模块化 + SPI"的例子 (与 WebApplicationType.Deducer 同构):
- 3.x:
SpringApplication里硬编码AnnotationConfigApplicationContext/AnnotationConfigServletWebServerApplicationContext/AnnotationConfigReactiveWebServerApplicationContext - 4.x:这三个实现分别由
spring-boot-webmvc/spring-boot-webflux等模块提供ApplicationContextFactory,Boot 核心只做"遍历取第一个"
4.1.2 默认实现的选择
java
private ConfigurableApplicationContext createDefaultApplicationContext() {
if (!AotDetector.useGeneratedArtifacts()) {
return new AnnotationConfigApplicationContext();
}
return new GenericApplicationContext();
}
注意 AOT 判断:
- 非 AOT 模式:
AnnotationConfigApplicationContext------ 支持注解配置解析(ConfigurationClassPostProcessor) - AOT 模式:
GenericApplicationContext------ 不 做注解解析,因为 Bean 定义已经由 AOT 生成的AotApplicationContextInitializer直接注册了
这是一个重要的性能优化 :在 Native/AOT 模式下,跳过整个 ConfigurationClassPostProcessor 的解析过程(这是启动中最耗时的步骤之一)。
第 27 章详述。
4.1.3 异常信息的人性化
java
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
异常信息直接给出了解决建议 :"你可能需要自定义 ApplicationContextFactory"。
这是 Boot 错误信息风格的典型体现------不只说"失败了",还说"你可以怎么做"。
4.2 ⑩ prepareContext(...) ------ 第二密集的方法
java
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, @Nullable Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
addAotGeneratedInitializerIfNecessary(this.initializers);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences());
if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding());
}
}
applyInitializers(context);
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.properties.isLogStartupInfo()) {
logStartupInfo(context);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (this.properties.isLazyInitialization()) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
if (this.properties.isKeepAlive()) {
context.addApplicationListener(new KeepAlive());
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Set<Object> sources = getAllSources();
Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined");
load(context, sources.toArray(new Object[0]));
}
listeners.contextLoaded(context);
}
十八行代码,十五个动作。逐个讲:
(1) context.setEnvironment(environment)
把前面精心准备的环境装进上下文。
(2) postProcessApplicationContext(context)
java
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
if (this.beanNameGenerator != null) {
context.getBeanFactory()
.registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, this.beanNameGenerator);
}
if (this.resourceLoader != null) {
if (context instanceof GenericApplicationContext genericApplicationContext) {
genericApplicationContext.setResourceLoader(this.resourceLoader);
}
if (context instanceof DefaultResourceLoader defaultResourceLoader) {
defaultResourceLoader.setClassLoader(this.resourceLoader.getClassLoader());
}
}
if (this.addConversionService) {
context.getBeanFactory().setConversionService(context.getEnvironment().getConversionService());
}
}
三件事:
- 注册自定义
BeanNameGenerator(作为一个特殊名称的单例) - 设置
ResourceLoader(同时设置 ClassLoader) - 把 Environment 的 ConversionService 传给 BeanFactory
注意第 3 点 :这一步保证了 BeanFactory 中的类型转换与 Environment 使用同一个 ApplicationConversionService。这样 @Value("${my.timeout}") 到 Duration 的转换才能工作(因为 ApplicationConversionService 支持 Duration)。
这是一个容易忽略但关键的连通:Environment 的转换服务 → BeanFactory 的转换服务。
(3) addAotGeneratedInitializerIfNecessary(this.initializers)
java
private void addAotGeneratedInitializerIfNecessary(List<ApplicationContextInitializer<?>> initializers) {
if (AotDetector.useGeneratedArtifacts()) {
List<ApplicationContextInitializer<?>> aotInitializers = new ArrayList<>(
initializers.stream().filter(AotApplicationContextInitializer.class::isInstance).toList());
if (aotInitializers.isEmpty()) {
Assert.state(this.mainApplicationClass != null, "No application main class found");
String initializerClassName = this.mainApplicationClass.getName() + "__ApplicationContextInitializer";
if (!ClassUtils.isPresent(initializerClassName, getClassLoader())) {
throw new AotInitializerNotFoundException(this.mainApplicationClass, initializerClassName);
}
aotInitializers.add(AotApplicationContextInitializer.forInitializerClasses(initializerClassName));
}
initializers.removeAll(aotInitializers);
initializers.addAll(0, aotInitializers);
}
if (NativeDetector.inNativeImage()) {
NativeImageRequirementsException.throwIfNotMet();
}
}
AOT 模式下的关键逻辑:
- 如果处于 AOT 模式(
AotDetector.useGeneratedArtifacts()) - 查找已有的
AotApplicationContextInitializer - 如果没有,按约定构造类名:
主类名 + "__ApplicationContextInitializer" - 如果该类不存在,抛
AotInitializerNotFoundException - 把 AOT initializer 移到列表最前面 (
addAll(0, ...))
为什么必须在最前面? 因为 AOT initializer 负责注册所有 Bean 定义,其他 initializer 可能依赖这些 Bean。
为什么类名是 主类名__ApplicationContextInitializer? 这是 AOT 代码生成的命名约定。如果你的主类是 com.example.MyApplication,则生成 com.example.MyApplication__ApplicationContextInitializer。
AotInitializerNotFoundException 的实用价值 :当你看到这个异常时,说明你以 AOT 模式启动但没有先运行 AOT 处理(process-aot)。解决方法是先跑 AOT 生成。
(4) 循环引用与 Bean 覆盖开关
java
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences());
if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding());
}
}
两个重要的 BeanFactory 配置:
allowCircularReferences(默认 false):是否允许 Bean 循环引用。Boot 从 2.6 起默认禁止,这是推荐做法(循环引用是设计问题的信号)。allowBeanDefinitionOverriding(默认 false):是否允许 Bean 定义覆盖。Boot 从 2.1 起默认禁止。
为什么禁止 Bean 定义覆盖? 因为静默覆盖是极其难查的问题:你定义了一个 Bean,被某个自动配置或另一个配置类悄悄覆盖了,你完全不知道。
Boot 的选择是"默认严格,允许放开":
properties
spring.main.allow-bean-definition-overriding=true
spring.main.allow-circular-references=true
注意 instanceof 的嵌套 :先判断 AbstractAutowireCapableBeanFactory(有 setAllowCircularReferences),再判断 DefaultListableBeanFactory(有 setAllowBeanDefinitionOverriding)。因为并非所有 BeanFactory 都支持这些设置。
这是"能力探测"的标准写法 ------用 instanceof 判断能力,而不是假设。
(5) applyInitializers(context)
java
@SuppressWarnings({ "rawtypes", "unchecked" })
protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
ApplicationContextInitializer.class);
Assert.state(requiredType != null,
() -> "No generic type found for initializr of type " + initializer.getClass());
Assert.state(requiredType.isInstance(context), "Unable to call initializer");
initializer.initialize(context);
}
}
关键在两个 Assert:
requiredType != null------ initializer 必须声明泛型参数。例如class MyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>。requiredType.isInstance(context)------ 上下文类型必须匹配 initializer 的泛型参数。
为什么需要这些检查? 因为 ApplicationContextInitializer<C extends ConfigurableApplicationContext> 是泛型的。如果一个 initializer 声明为 ApplicationContextInitializer<WebApplicationContext>,而当前上下文是普通 AnnotationConfigApplicationContext,调用它会导致 ClassCastException。
Boot 选择提前检查并给出清晰错误 ,而不是让 ClassCastException 在后面爆发。
这是一个"类型安全的 SPI 调用"范例 :用 GenericTypeResolver 解析泛型参数,用 isInstance 校验,再调用。
(6) listeners.contextPrepared(context)
发布第三个事件。
(7) bootstrapContext.close(context)
引导上下文的生命周期结束 。它把其中注册的对象(如果有 ApplicationListener 等)转移到主上下文。
(8) 启动日志
java
if (this.properties.isLogStartupInfo()) {
logStartupInfo(context);
logStartupProfileInfo(context);
}
logStartupInfo:
java
protected void logStartupInfo(ConfigurableApplicationContext context) {
boolean isRoot = context.getParent() == null;
if (isRoot) {
new StartupInfoLogger(this.mainApplicationClass, context.getEnvironment()).logStarting(getApplicationLog());
}
}
注意 context.getParent() == null 判断:只有根上下文才打印启动信息。这避免了 Actuator 管理子上下文重复打印。
logStartupProfileInfo:
java
protected void logStartupProfileInfo(ConfigurableApplicationContext context) {
Log log = getApplicationLog();
if (log.isInfoEnabled()) {
List<String> activeProfiles = quoteProfiles(context.getEnvironment().getActiveProfiles());
if (ObjectUtils.isEmpty(activeProfiles)) {
List<String> defaultProfiles = quoteProfiles(context.getEnvironment().getDefaultProfiles());
String message = String.format("%s default %s: ", defaultProfiles.size(),
(defaultProfiles.size() <= 1) ? "profile" : "profiles");
log.info("No active profile set, falling back to " + message
+ StringUtils.collectionToDelimitedString(defaultProfiles, ", "));
}
else {
String message = (activeProfiles.size() == 1) ? "1 profile is active: "
: activeProfiles.size() + " profiles are active: ";
log.info("The following " + message + StringUtils.collectionToDelimitedString(activeProfiles, ", "));
}
}
}
注意 log.isInfoEnabled() 的前置检查------避免在日志级别不够时做无谓的字符串拼接。
注意单复数处理 (profile vs profiles,is vs are)------这是 Boot 对细节的执着。
看过 Boot 启动日志的人对这两条一定很熟:
vbnet
Starting MyApplication using Java 25 with PID 12345
No active profile set, falling back to 1 default profile: "default"
或:
sql
The following 2 profiles are active: "dev", "local"
(9) 注册 Boot 特有的单例
java
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
两个单例:
| Bean 名 | 类型 | 用途 |
|---|---|---|
springApplicationArguments |
ApplicationArguments |
让任何 Bean 都能注入应用参数 |
springBootBanner |
Banner |
让 /actuator/info 等能拿到 Banner |
这就是 ApplicationRunner 能拿到 ApplicationArguments 的原因之一------它作为单例注册在 BeanFactory 中,可以被注入。
注意 registerSingleton 与 registerBeanDefinition 的区别:
registerSingleton------ 直接注册一个已经实例化的对象registerBeanDefinition------ 注册一个定义,由容器决定何时实例化
对于"启动时就已确定"的对象(如命令行参数、Banner),用 registerSingleton 更直接。
(10) 懒初始化
java
if (this.properties.isLazyInitialization()) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
spring.main.lazy-initialization=true 时,添加一个 BeanFactoryPostProcessor 把所有 Bean 定义设为 lazy。
为什么用 BFPP 而不是直接设置? 因为 Bean 定义是在 refresh 阶段才被全部注册的(自动配置也在那时)。BFPP 在"所有 Bean 定义注册完毕、但还未实例化"时执行,正好可以统一改它们的 lazyInit 标志。
懒初始化的收益与代价:
- 收益:启动更快(不实例化非必要 Bean)
- 代价:首次请求变慢;启动期的错误延后暴露
我的建议:开发环境可以开(快速重启),生产环境慎开(会让启动期问题变成运行期问题)。
(11) KeepAlive
java
if (this.properties.isKeepAlive()) {
context.addApplicationListener(new KeepAlive());
}
KeepAlive 内部类:
java
private static final class KeepAlive implements ApplicationListener<ApplicationContextEvent> {
private final AtomicReference<@Nullable Thread> thread = new AtomicReference<>();
@Override
public void onApplicationEvent(ApplicationContextEvent event) {
if (event instanceof ContextRefreshedEvent) {
startKeepAliveThread();
}
else if (event instanceof ContextClosedEvent) {
stopKeepAliveThread();
}
}
private void startKeepAliveThread() {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(Long.MAX_VALUE);
}
catch (InterruptedException ex) {
break;
}
}
});
if (this.thread.compareAndSet(null, thread)) {
thread.setDaemon(false);
thread.setName("keep-alive");
thread.start();
}
}
private void stopKeepAliveThread() {
Thread thread = this.thread.getAndSet(null);
if (thread == null) {
return;
}
thread.interrupt();
}
}
作用 :spring.main.keep-alive=true 时,启动一个非守护线程让 JVM 保持存活。
什么时候需要? 当你启动一个应用但没有 Web 服务器或其他非守护线程时,JVM 会在 main 返回后立即退出。KeepAlive 阻止这种退出。
实现细节:
thread.setDaemon(false)------ 非守护线程,阻止 JVM 退出Thread.sleep(Long.MAX_VALUE)------ 睡眠(不消耗 CPU)AtomicReference+compareAndSet------ 保证只启动一次(线程安全)ContextClosedEvent时interrupt()停止
注意:这是 3.2.0 加入的特性。
(12) PropertySourceOrderingBeanFactoryPostProcessor
java
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
java
private static class PropertySourceOrderingBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
private final ConfigurableApplicationContext context;
PropertySourceOrderingBeanFactoryPostProcessor(ConfigurableApplicationContext context) {
this.context = context;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
DefaultPropertiesPropertySource.moveToEnd(this.context.getEnvironment());
}
}
作用 :在 refresh 阶段,把 defaultProperties 再次移到末尾。
为什么需要第二次? 因为在 prepareEnvironment 中做过一次,但之后 ConfigurationClassPostProcessor 可能因为 @PropertySource 注解添加了新的 PropertySource,改变了顺序。这个 BFPP(HIGHEST_PRECEDENCE)确保所有配置类处理完毕后,defaultProperties 仍在最后。
这是一个"事后修正"的设计------因为顺序可能在中间被改变,所以在最后再修正一次。
(13) 加载 sources
java
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Set<Object> sources = getAllSources();
Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined");
load(context, sources.toArray(new Object[0]));
}
注意 AOT 判断 :AOT 模式下不加载 sources,因为 Bean 定义已经由 AOT initializer 注册了。
getAllSources():
java
public Set<Object> getAllSources() {
Set<Object> allSources = new LinkedHashSet<>();
if (!CollectionUtils.isEmpty(this.primarySources)) {
allSources.addAll(this.primarySources);
}
if (!CollectionUtils.isEmpty(this.properties.getSources())) {
allSources.addAll(this.properties.getSources());
}
return Collections.unmodifiableSet(allSources);
}
合并两部分:
primarySources(构造时传入的Class对象)properties.getSources()(spring.main.sources配置的字符串------类名/包名/XML 路径)
这解释了 spring.main.sources 的用途:你可以通过命令行/配置额外指定要加载的配置类,无需改代码:
bash
java -jar app.jar --spring.main.sources=com.example.ExtraConfig
load() 方法(第 12 章详述):
java
protected void load(ApplicationContext context, Object[] sources) {
if (logger.isDebugEnabled()) {
logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
}
BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
loader.load();
}
(14) listeners.contextLoaded(context)
发布第四个事件。
4.3 阶段 3:刷新与完成
⑪ refreshContext(context)
java
private void refreshContext(ConfigurableApplicationContext context) {
if (this.properties.isRegisterShutdownHook()) {
shutdownHook.registerApplicationContext(context);
}
refresh(context);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}
refresh() 是整个启动过程中最重要的一步 ------它是 Spring Framework 的 AbstractApplicationContext.refresh(),包含:
- BeanFactory 准备
- BeanFactoryPostProcessor 执行(包括
ConfigurationClassPostProcessor,即自动配置的导入) - BeanPostProcessor 注册
- 消息源、事件广播器初始化
- 单例 Bean 实例化
- 生命周期处理器
Boot 在这一步几乎没做额外的事 ------它只是调用 refresh()。Boot 的所有"魔法"都通过 BeanFactoryPostProcessor 和 ApplicationListener 在 refresh 内部生效。
这是一个重要的架构特征:Boot 不是"替代" Spring 的启动流程,而是"注入"到它的扩展点中。
⑫ 完成阶段
java
afterRefresh(context, applicationArguments);
Duration timeTakenToStarted = startup.started();
if (this.properties.isLogStartupInfo()) {
new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
}
listeners.started(context, timeTakenToStarted);
callRunners(context, applicationArguments);
(a) afterRefresh ------ 空实现,模板方法
java
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
(b) 计时
java
Duration timeTakenToStarted = startup.started();
Startup.started():
java
final Duration started() {
long now = System.currentTimeMillis();
this.timeTakenToStarted = Duration.ofMillis(now - startTime());
return this.timeTakenToStarted;
}
(c) 日志
java
new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
这就是那句熟悉的:
arduino
Started MyApplication in 2.345 seconds (process running for 3.012)
(注意 process running for 部分 ------它来自 processUptime(),在 CRaC 环境下会不同。)
(d) listeners.started(context, timeTakenToStarted) ------ 第五个事件
(e) callRunners(context, applicationArguments)
java
private void callRunners(ConfigurableApplicationContext context, ApplicationArguments args) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
String[] beanNames = beanFactory.getBeanNamesForType(Runner.class);
Map<Runner, String> instancesToBeanNames = new IdentityHashMap<>();
for (String beanName : beanNames) {
instancesToBeanNames.put(beanFactory.getBean(beanName, Runner.class), beanName);
}
Comparator<Object> comparator = getOrderComparator(beanFactory)
.withSourceProvider(new FactoryAwareOrderSourceProvider(beanFactory, instancesToBeanNames));
instancesToBeanNames.keySet().stream().sorted(comparator).forEach((runner) -> callRunner(runner, args));
}
几个细节:
(1) IdentityHashMap
java
Map<Runner, String> instancesToBeanNames = new IdentityHashMap<>();
用 IdentityHashMap 而不是 HashMap------按引用相等 而非 equals() 比较。
为什么? 因为两个不同的 Runner Bean 可能 equals() 相等(如果你没重写 equals,默认就是引用相等;但如果重写了,就可能相等)。用 IdentityHashMap 保证每个实例都是独立的 key。
(2) getOrderComparator
java
private OrderComparator getOrderComparator(ConfigurableListableBeanFactory beanFactory) {
Comparator<?> dependencyComparator = (beanFactory instanceof DefaultListableBeanFactory defaultListableBeanFactory)
? defaultListableBeanFactory.getDependencyComparator() : null;
return (dependencyComparator instanceof OrderComparator orderComparator) ? orderComparator
: AnnotationAwareOrderComparator.INSTANCE;
}
优先使用 BeanFactory 自己的 comparator ,否则用 AnnotationAwareOrderComparator。
为什么? 因为 DefaultListableBeanFactory 的 dependencyComparator 通常是 AnnotationAwareOrderComparator,但用户可能自定义了。尊重 BeanFactory 的配置。
(3) FactoryAwareOrderSourceProvider
java
private static class FactoryAwareOrderSourceProvider implements OrderSourceProvider {
private final ConfigurableBeanFactory beanFactory;
private final Map<?, String> instancesToBeanNames;
@Override
public @Nullable Object getOrderSource(Object obj) {
String beanName = this.instancesToBeanNames.get(obj);
return (beanName != null) ? getOrderSource(beanName, obj.getClass()) : null;
}
private @Nullable Object getOrderSource(String beanName, Class<?> instanceType) {
try {
RootBeanDefinition beanDefinition = (RootBeanDefinition) this.beanFactory
.getMergedBeanDefinition(beanName);
Method factoryMethod = beanDefinition.getResolvedFactoryMethod();
Class<?> targetType = beanDefinition.getTargetType();
targetType = (targetType != instanceType) ? targetType : null;
return Stream.of(factoryMethod, targetType).filter(Objects::nonNull).toArray();
}
catch (NoSuchBeanDefinitionException ex) {
return null;
}
}
}
作用 :让 @Order 注解不仅能在 Runner 类上生效,还能在 @Bean 方法上生效。
例如:
java
@Bean
@Order(1) // ← 这个 @Order 在方法上
CommandLineRunner myRunner() { ... }
FactoryAwareOrderSourceProvider 通过 getResolvedFactoryMethod() 拿到 @Bean 方法,让 AnnotationAwareOrderComparator 能够读取方法上的 @Order。
这是一个很贴心但少有人知的细节 :Runner 的 @Order 可以写在 @Bean 方法上。
(4) callRunner 的双重分发
java
private void callRunner(Runner runner, ApplicationArguments args) {
if (runner instanceof ApplicationRunner) {
callRunner(ApplicationRunner.class, runner, (applicationRunner) -> applicationRunner.run(args));
}
if (runner instanceof CommandLineRunner) {
callRunner(CommandLineRunner.class, runner,
(commandLineRunner) -> commandLineRunner.run(args.getSourceArgs()));
}
}
@SuppressWarnings("unchecked")
private <R extends Runner> void callRunner(Class<R> type, Runner runner, ThrowingConsumer<R> call) {
call.throwing(
(message, ex) -> new IllegalStateException("Failed to execute " + ClassUtils.getShortName(type), ex))
.accept((R) runner);
}
注意是两个 if 而不是 if-else ------如果一个类同时 实现了 ApplicationRunner 和 CommandLineRunner,两个方法都会被调用。
ThrowingConsumer.throwing(...) 是 Spring 的函数式工具,把受检异常包装成 IllegalStateException,并带上清晰的消息:"Failed to execute ApplicationRunner"。
⑬ ready 事件(在第二个 try 块中)
java
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, null);
}
return context;
注意 listeners.ready 在独立的 try 块中 ,与 started 分开。
为什么? 因为 started 和 callRunners 之后的失败不应该影响 ready 事件。反之亦然------ready 事件失败也不应该影响已经成功启动的应用(但这里选择了抛异常,因为 ready 失败通常意味着严重问题)。
context.isRunning() 检查 :如果应用在 callRunners 中被关闭了(例如某个 Runner 调用了 SpringApplication.exit(...)),就不发 ready 事件。
ready 事件(3.4 新增)的用途 :与 started 的区别是------
started------ 上下文刷新完成、Runner 尚未执行ready------ Runner 执行完毕,应用"真正就绪"
对于需要"预热"的应用(如加载缓存),ready 才是真正的就绪信号。这是一个对 K8s readiness probe 很有用的事件。
5. 失败处理:handleRunFailure
5.1 完整源码
java
private RuntimeException handleRunFailure(@Nullable ConfigurableApplicationContext context, Throwable exception,
@Nullable SpringApplicationRunListeners listeners) {
if (exception instanceof AbandonedRunException abandonedRunException) {
return abandonedRunException;
}
try {
try {
handleExitCode(context, exception);
if (listeners != null) {
listeners.failed(context, exception);
}
}
finally {
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
context.close();
shutdownHook.deregisterFailedApplicationContext(context);
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
return (exception instanceof RuntimeException runtimeException) ? runtimeException
: new IllegalStateException(exception);
}
5.2 层次化的失败处理
五个层次:
(1) AbandonedRunException ------ 静默退出
java
if (exception instanceof AbandonedRunException abandonedRunException) {
return abandonedRunException;
}
AbandonedRunException 是一个特殊信号:
java
public static class AbandonedRunException extends RuntimeException {
private final @Nullable ConfigurableApplicationContext applicationContext;
// ...
}
用途 :当某个组件(如 SpringApplicationHook 的测试监听器)决定"这次运行到此为止,不要继续也不算失败"时,抛这个异常。
典型场景 :测试中只想获取 SpringApplication 实例而不想真正启动 应用。Augmented 机制就用了它(见 §6.2)。
它绕过所有失败处理,直接返回------这正是"静默"的含义。
(2) handleExitCode ------ 退出码
java
private void handleExitCode(@Nullable ConfigurableApplicationContext context, Throwable exception) {
int exitCode = getExitCodeFromException(context, exception);
if (exitCode != 0) {
if (context != null) {
context.publishEvent(new ExitCodeEvent(context, exitCode));
}
SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
if (handler != null) {
handler.registerExitCode(exitCode);
}
}
}
退出码的获取有两级:
java
private int getExitCodeFromException(@Nullable ConfigurableApplicationContext context, Throwable exception) {
int exitCode = getExitCodeFromMappedException(context, exception);
if (exitCode == 0) {
exitCode = getExitCodeFromExitCodeGeneratorException(exception);
}
return exitCode;
}
- 第一级:
ExitCodeExceptionMapperBean 映射(用户可自定义异常 → 退出码) - 第二级:异常自身实现
ExitCodeGenerator
这是一个两级降级 :先看有没有用户定义的映射,再看异常自己是不是 ExitCodeGenerator。
退出码的用途:让运维脚本能根据退出码判断失败类型。例如:
java
@Component
public class MyExitCodeMapper implements ExitCodeExceptionMapper {
@Override
public int getExitCode(Throwable exception) {
if (exception instanceof DatabaseException) {
return 10;
}
return 0;
}
}
(3) listeners.failed(context, exception) ------ 第六个事件
(4) reportFailure(...) ------ 异常分析(在 finally 中)
java
private void reportFailure(Collection<SpringBootExceptionReporter> exceptionReporters, Throwable failure) {
try {
for (SpringBootExceptionReporter reporter : exceptionReporters) {
if (reporter.reportException(failure)) {
registerLoggedException(failure);
return;
}
}
}
catch (Throwable ex) {
// Continue with normal handling of the original failure
}
if (logger.isErrorEnabled()) {
if (NativeDetector.inNativeImage()) {
// Depending on how early the failure was, logging may not work in a
// native image so we output the stack trace directly to System.out
// instead.
System.out.println("Application run failed");
failure.printStackTrace(System.out);
}
else {
logger.error("Application run failed", failure);
}
registerLoggedException(failure);
}
}
SpringBootExceptionReporter 是 FailureAnalyzer 的门面。它会:
- 分析异常类型
- 生成
FailureAnalysis(描述 + 行动建议) - 打印人类可读的报告
例如端口占用时的输出:
markdown
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
这就是 Boot 著名的"错误信息友好"的实现 。第 05 章详述 FailureAnalyzer。
注意 Native 分支 :在 Native Image 中,早期失败时日志系统可能还没初始化,所以直接 System.out.println + printStackTrace。
这是一个"降级输出"的考量------宁可输出到 stdout 也不能丢掉错误信息。
(5) 关闭上下文(在 finally 中)
java
finally {
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
context.close();
shutdownHook.deregisterFailedApplicationContext(context);
}
}
context.close() ------ 释放资源(关闭 Web 服务器、释放数据库连接等)。
deregisterFailedApplicationContext ------ 从 shutdown hook 中移除这个失败的上下文(避免 JVM 关闭时再次尝试关闭它)。
注意 try-finally 的嵌套:
- 外层 try 捕获所有异常,只 warn("Unable to close ApplicationContext")
- 内层 try-finally 保证
reportFailure和close一定执行
这保证了即使"关闭上下文"本身失败,错误报告也已经输出。
5.3 失败处理的哲学
Boot 的失败处理体现了几个原则:
- 不吞掉原始异常 :
handleRunFailure最终总是返回一个包含原始异常的RuntimeException - 失败也要清理资源 :
finally中关闭上下文 - 失败也要通知 :
listeners.failed事件 - 失败要可诊断 :
FailureAnalyzer给出建议 - 清理失败不影响错误报告:内层 finally 保证顺序
这是"失败路径"的工程化范本------很多框架只在"成功路径"上下功夫,Boot 在失败路径上投入同样多。
6. 静态工具方法与内部类全景
6.1 四个静态 run 相关方法
(1) run(Class, String...) 与 run(Class[], String...)
java
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
最常见的入口:
java
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
(2) main(String[] args) ------ 可以直接运行的 SpringApplication
java
public static void main(String[] args) throws Exception {
SpringApplication.run(new Class<?>[0], args);
}
注意 :SpringApplication 自己有一个 main 方法!这意味着你可以:
bash
java -cp spring-boot.jar:... org.springframework.boot.SpringApplication --spring.main.sources=com.example.MyConfig
在没有自己的 main 类的情况下启动应用。
用途:快速原型、脚本化启动。
(3) exit(ApplicationContext, ExitCodeGenerator...)
java
public static int exit(ApplicationContext context, ExitCodeGenerator... exitCodeGenerators) {
Assert.notNull(context, "'context' must not be null");
int exitCode = 0;
try {
try {
ExitCodeGenerators generators = new ExitCodeGenerators();
Collection<ExitCodeGenerator> beans = context.getBeansOfType(ExitCodeGenerator.class).values();
generators.addAll(exitCodeGenerators);
generators.addAll(beans);
exitCode = generators.getExitCode();
if (exitCode != 0) {
context.publishEvent(new ExitCodeEvent(context, exitCode));
}
}
finally {
close(context);
}
}
catch (Exception ex) {
ex.printStackTrace();
exitCode = (exitCode != 0) ? exitCode : 1;
}
return exitCode;
}
典型用法:
java
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(MyApplication.class, args);
System.exit(SpringApplication.exit(context));
}
}
注意 System.exit(...) 是必要的 ------否则 exit() 只是返回退出码,JVM 不会退出。
ExitCodeGenerators 的合并逻辑:
java
generators.addAll(exitCodeGenerators); // 传入的
generators.addAll(beans); // 上下文中的 Bean
exitCode = generators.getExitCode();
传入的 generator 优先(先添加,先取到非 0 值)。
6.2 from(...) 与 Augmented:不改 main 方法也能加配置
这是一个 3.1.0 引入、4.x 仍在用的机制。
java
public static SpringApplication.Augmented from(ThrowingConsumer<String[]> main) {
Assert.notNull(main, "'main' must not be null");
return new Augmented(main, Collections.emptySet(), Collections.emptySet());
}
用途 :在测试中,启动一个应用的 main 方法,但额外加入一些配置类。
java
@Test
void testWithExtraConfig() {
SpringApplication.from(MyApplication::main)
.with(ExtraTestConfig.class)
.withAdditionalProfiles("test")
.run("--server.port=0");
}
它是怎么工作的? 看 Augmented.run:
java
public SpringApplication.Running run(String... args) {
RunListener runListener = new RunListener();
SpringApplicationHook hook = new SingleUseSpringApplicationHook((springApplication) -> {
springApplication.addPrimarySources(this.sources);
springApplication.setAdditionalProfiles(this.additionalProfiles.toArray(String[]::new));
return runListener;
});
withHook(hook, () -> this.main.accept(args));
return runListener;
}
流程:
- 创建
RunListener - 创建
SpringApplicationHook,它的getRunListener返回runListener,同时在返回前修改springApplication(加 sources、加 profiles) - 用
withHook设置 ThreadLocal,然后调用main main内部的SpringApplication.run()会读到这个 hook,注入 listener 并应用修改
这是"不改 main 方法也能改配置"的实现 ------通过 ThreadLocal hook + 回调。
SingleUseSpringApplicationHook:
java
private static final class SingleUseSpringApplicationHook implements SpringApplicationHook {
private final AtomicBoolean used = new AtomicBoolean();
private final SpringApplicationHook delegate;
@Override
public @Nullable SpringApplicationRunListener getRunListener(SpringApplication springApplication) {
return this.used.compareAndSet(false, true) ? this.delegate.getRunListener(springApplication) : null;
}
}
保证只生效一次 ------避免 main 方法内部启动了多个 SpringApplication(罕见但可能)时重复应用。
RunListener 的作用是捕获启动的上下文:
java
private static final class RunListener implements SpringApplicationRunListener, Running {
private final List<ConfigurableApplicationContext> contexts = Collections
.synchronizedList(new ArrayList<>());
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
this.contexts.add(context);
}
@Override
public ConfigurableApplicationContext getApplicationContext() {
List<ConfigurableApplicationContext> rootContexts = this.contexts.stream()
.filter((context) -> context.getParent() == null)
.toList();
Assert.state(!rootContexts.isEmpty(), "No root application context located");
Assert.state(rootContexts.size() == 1, "No unique root application context located");
return rootContexts.get(0);
}
}
注意 synchronizedList------因为 hook 可能在多线程下触发。
注意 getApplicationContext() 的严格校验:必须有且仅有一个根上下文。如果有多个(并行启动了多个应用),报错。
6.3 withHook ------ 测试利器
java
public static void withHook(SpringApplicationHook hook, Runnable action) {
withHook(hook, () -> {
action.run();
return Void.class;
});
}
public static <T> T withHook(SpringApplicationHook hook, ThrowingSupplier<T> action) {
applicationHook.set(hook);
try {
return action.get();
}
finally {
applicationHook.remove();
}
}
用途 :在测试中拦截应用启动,获取 SpringApplication 实例或监听启动过程。
java
@Test
void customBannerIsUsed() {
SpringApplicationHook hook = (application) -> {
application.setBanner(myBanner);
return null;
};
SpringApplication.withHook(hook, () -> {
MyApplication.main(new String[0]);
});
}
注意 hook.getRunListener 返回 null 是允许的------表示"只修改,不监听"。
6.4 内部类全景
SpringApplication 有 8 个内部类,每个都有明确职责:
| 内部类 | 职责 |
|---|---|
Augmented |
增强运行(§6.2) |
Running |
提供运行中的上下文访问 |
RunListener |
捕获上下文 |
PropertySourceOrderingBeanFactoryPostProcessor |
修正 PropertySource 顺序 |
AbandonedRunException |
静默退出信号 |
NativeImageRequirementsException |
Native 环境校验 |
SingleUseSpringApplicationHook |
一次性 hook |
KeepAlive |
保活线程 |
Startup(抽象) |
启动计时基类 |
StandardStartup |
标准计时 |
CoordinatedRestoreAtCheckpointStartup |
CRaC 计时 |
FactoryAwareOrderSourceProvider |
Runner 排序源 |
(实际是 12 个,Startup 系列算 3 个。)
把它们放在一起看,能读出 Boot 的关注点:
- 1 个用于测试(
Augmented系列) - 2 个用于配置修正(PropertySource 顺序、Runner 排序)
- 2 个用于异常处理(Abandoned、NativeImage)
- 1 个用于运行时特性(KeepAlive)
- 3 个用于计时(Startup 系列,含 CRaC)
- 1 个用于测试支持(SingleUseHook)
6.5 Startup 系列:一个完整的策略模式
java
abstract static class Startup {
private @Nullable Duration timeTakenToStarted;
protected abstract long startTime();
protected abstract @Nullable Long processUptime();
protected abstract String action();
final Duration started() {
long now = System.currentTimeMillis();
this.timeTakenToStarted = Duration.ofMillis(now - startTime());
return this.timeTakenToStarted;
}
Duration timeTakenToStarted() {
Assert.state(this.timeTakenToStarted != null,
"timeTakenToStarted is not set. Make sure to call started() before this method");
return this.timeTakenToStarted;
}
private Duration ready() {
long now = System.currentTimeMillis();
return Duration.ofMillis(now - startTime());
}
static Startup create() {
ClassLoader classLoader = Startup.class.getClassLoader();
return (ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", classLoader)
&& ClassUtils.isPresent("org.crac.management.CRaCMXBean", classLoader))
? new CoordinatedRestoreAtCheckpointStartup() : new StandardStartup();
}
}
设计要点:
- 三个抽象方法 :
startTime()、processUptime()、action()------ 定义了"计时策略"的全部可变部分 started()是final------ 基类控制算法骨架,子类只提供数据(模板方法模式)timeTakenToStarted()有Assert.state------ 防止未调用started()就读取create()是静态工厂 ------ 封装了环境探测逻辑
CoordinatedRestoreAtCheckpointStartup 的 fallback 模式:
java
private final StandardStartup fallback = new StandardStartup();
@Override
protected @Nullable Long processUptime() {
Long uptime = CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore();
return (uptime >= 0) ? uptime : this.fallback.processUptime();
}
每个方法都有 fallback------当 CRaC API 返回"无效"(负数)时,回退到标准实现。
这是一个"可选特性集成"的范式:
- 探测特性是否存在
- 存在则用特性实现
- 但每个调用点都要处理"特性存在但当前不适用"的情况
7. 实战:定制 SpringApplication
7.1 什么时候需要定制?
大多数应用直接用 SpringApplication.run(...) 就够了。需要定制的场景:
- 需要自定义 Banner
- 需要添加/替换监听器或初始化器
- 需要关闭命令行属性或转换服务
- 需要自定义
ApplicationContextFactory - 需要设置默认属性
- 需要懒初始化或 keep-alive
7.2 完整示例
java
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
// 1. 关闭 Banner
application.setBannerMode(Banner.Mode.OFF);
// 2. 设置默认属性(优先级最低)
Map<String, Object> defaults = new HashMap<>();
defaults.put("server.port", "8080");
application.setDefaultProperties(defaults);
// 3. 添加监听器
application.addListeners(new MyApplicationListener());
// 4. 添加初始化器
application.addInitializers(new MyContextInitializer());
// 5. 添加引导初始化器(最早期的扩展点)
application.addBootstrapRegistryInitializer((bootstrapContext) -> {
bootstrapContext.register(MyEarlyBean.class,
InstanceSupplier.of(MyEarlyBean::new));
});
// 6. 设置额外 Profile
application.setAdditionalProfiles("prod");
// 7. 关闭命令行属性(如果不希望命令行覆盖配置)
// application.setAddCommandLineProperties(false);
// 8. 懒初始化
application.setLazyInitialization(true);
// 9. 设置启动度量
application.setApplicationStartup(new BufferingApplicationStartup(2048));
// 10. 关闭 headless(如果需要 AWT)
// application.setHeadless(false);
application.run(args);
}
}
7.3 用 spring.main.* 属性代替代码
4.x 的推荐做法:能用属性就不用代码。
properties
# application.properties
spring.main.banner-mode=off
spring.main.lazy-initialization=true
spring.main.keep-alive=false
spring.main.allow-bean-definition-overriding=false
spring.main.allow-circular-references=false
spring.main.web-application-type=servlet
spring.main.log-startup-info=true
spring.main.register-shutdown-hook=true
spring.main.sources=com.example.ExtraConfig
因为这些属性在 prepareEnvironment 阶段被绑定到 ApplicationProperties,所以它们能影响后续的所有步骤。
注意 spring.main.environment-prefix 不能这样设置(见 §3.4 动作 7 的断言)。
7.4 自定义 ApplicationContextFactory
如果你需要完全控制上下文的创建:
java
public class MyContextFactory implements ApplicationContextFactory {
@Override
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
return switch (webApplicationType) {
case SERVLET -> new MyServletWebServerApplicationContext();
case REACTIVE -> new MyReactiveWebServerApplicationContext();
case NONE -> new MyGenericApplicationContext();
};
}
@Override
public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {
return new MyEnvironment();
}
@Override
public Class<? extends ConfigurableEnvironment> getEnvironmentType(WebApplicationType webApplicationType) {
return MyEnvironment.class;
}
}
注册(在 META-INF/spring.factories):
ini
org.springframework.boot.ApplicationContextFactory=\
com.example.MyContextFactory
注意 :DefaultApplicationContextFactory 会遍历所有 factory,取第一个返回非 null 的。所以你的 factory 需要保证在适当的时候返回 non-null。
7.5 自定义 WebApplicationType.Deducer
如果你的模块需要参与 Web 类型推断(4.0.1+):
java
public class MyWebTypeDeducer implements WebApplicationType.Deducer {
@Override
public WebApplicationType deduceWebApplicationType() {
if (ClassUtils.isPresent("com.example.MyServer", null)) {
return WebApplicationType.SERVLET;
}
return null; // 让其他 Deducer 决定
}
}
注册:
ini
org.springframework.boot.WebApplicationType$Deducer=\
com.example.MyWebTypeDeducer
(注意内部接口的类名写法是 WebApplicationType$Deducer。)
返回 null 的含义:"我判断不了,交给下一个"------这是责任链模式。
8. 启动性能与度量
8.1 ApplicationStartup
java
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
ApplicationStartup 是 Spring Framework 5.3 引入的启动度量 API。Boot 用它记录:
spring.boot.application.startingspring.boot.application.environment-preparedspring.boot.application.context-preparedspring.boot.application.context-loadedspring.boot.application.startedspring.boot.application.readyspring.boot.application.failed
以及 Spring 内部的步骤(如 spring.beans.instantiate)。
默认实现 ApplicationStartup.DEFAULT 是空实现(不记录),因为记录本身有开销。
要启用度量:
java
application.setApplicationStartup(new BufferingApplicationStartup(2048));
然后可以在 Actuator 的 /actuator/startup 端点查看(第 24 章)。
8.2 记录的粒度
SpringApplicationRunListeners.doWithListeners:
java
private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,
@Nullable Consumer<StartupStep> stepAction) {
StartupStep step = this.applicationStartup.start(stepName);
this.listeners.forEach(listenerAction);
if (stepAction != null) {
stepAction.accept(step);
}
step.end();
}
注意 :记录的是"遍历所有 listener 的总耗时",而不是每个 listener 单独的耗时。
这是一个粒度选择:Boot 关心的是"环境准备花了多久",而不是"某个 listener 花了多久"。如果需要更细的粒度,可以在自己的 listener 内部再开 StartupStep。
8.3 starting 事件的 tag
java
void starting(ConfigurableBootstrapContext bootstrapContext, @Nullable Class<?> mainApplicationClass) {
doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
(step) -> {
if (mainApplicationClass != null) {
step.tag("mainApplicationClass", mainApplicationClass.getName());
}
});
}
以及 failed:
java
void failed(@Nullable ConfigurableApplicationContext context, Throwable exception) {
doWithListeners("spring.boot.application.failed",
(listener) -> callFailedListener(listener, context, exception), (step) -> {
step.tag("exception", exception.getClass().toString());
String message = exception.getMessage();
if (message != null) {
step.tag("message", message);
}
});
}
tag 的价值:让度量数据带上上下文,便于分析。例如"哪类异常导致启动失败最多"。
8.4 一个实用的性能排查流程
vbnet
Step 1: 启用 BufferingApplicationStartup
application.setApplicationStartup(new BufferingApplicationStartup(10000));
Step 2: 启动后导出数据
(通过 /actuator/startup 或代码中读取)
Step 3: 按耗时排序,找到最慢的步骤
Step 4: 如果是 spring.boot.application.environment-prepared 慢
→ 检查 ConfigData 位置数量(网络位置?大量 profile?)
→ 第 09 章
Step 5: 如果是 refresh 慢
→ 检查自动配置数量(是否引入了不必要的 starter)
→ 检查单例 Bean 数量
→ 第 13 章
Step 6: 如果是 spring.beans.instantiate 慢
→ 具体是哪个 Bean
→ 考虑懒初始化
最常见的问题:引入了过多 starter,导致自动配置候选过多。这正是 4.x 模块化要解决的问题(第 03 章)。
9. 本章新增定律
第七定律:兜底实现有三重信号(第 4 章)
prepareEnvironment 里 defaultProperties 与 applicationInfo 都被 moveToEnd 挪到 PropertySource 链的末尾(§3)。Boot 表达"这是兜底值"用了三重信号,缺任何一重都会让读代码的人误判:
- 命名 :
DefaultPropertiesPropertySource.NAME = "defaultProperties"(core/spring-boot/src/main/java/org/springframework/boot/env/DefaultPropertiesPropertySource.java:45)、ApplicationInfoPropertySource.NAME = "applicationInfo"(ApplicationInfoPropertySource.java:40)------ 名字本身就写着"默认 / 信息"; - 位置 :
moveToEnd(...)把它移到链尾,使它的优先级最低(§3); - 禁令 :
Assert.state(!environment.containsProperty("spring.main.environment-prefix"), ...)直接堵死"用配置文件改写某个兜底项"的口子(§3)。
只做"命名"这一重是最常见的自欺 :名字叫 default,实际却在链首,用户配置反而被它盖掉。
第八定律:控制流用的哨兵异常,必须在错误处理链的第一行短路(第 4 章)
handleRunFailure 的第一句就是
java
if (exception instanceof AbandonedRunException abandonedRunException) {
return abandonedRunException;
}
AbandonedRunException 不是"启动失败",而是 AOT 处理时主动放弃运行的信号(第 28 章 §1)。 它必须原样返回,不能走进 handleExitCode → listeners.failed → reportFailure 这条通用链, 否则控制台会打印一次并不存在的启动失败,并把退出码改掉(§5.1)。
凡是借用异常做控制流的,都要在 catch 的第一行处理掉,不能让它"看起来像一个错误"。
第九定律:输入顺序不可控,而结果顺序有语义时,中间必有一次"归一化排序"(第 4 章)
callRunners(§4)面对的 beanFactory.getBeanNamesForType(Runner.class) 顺序由 Bean 定义的注册顺序决定------对用户不可控,对 @Order 无感知。于是它先构造 FactoryAwareOrderSourceProvider(SpringApplication.java:1843),再统一 sorted(comparator) 后才逐个调用。
"输入顺序不可控"与"结果顺序有语义"同时成立时,中间必然要有一次显式排序; 缺了这一次排序,正确性就建立在运气上。
10. 本章未逐行核实 / 待你实测的项
SpringApplication的静态工具方法与内部类未逐个展开(§6 只是清单)。StartupStep的完整埋点集合未统计。SpringApplicationShutdownHook的并发与超时细节未实测。- CRaC 相关分支只点到,未读
spring-boot-crac模块。 - AOT 模式与 JVM 模式下
run()的分支差异未逐行对比(第 28 章)。
下一章 :第 05 章进入 ApplicationProperties 与 spring.main.*------9 个属性如何塑造整个启动前半程。
11. 本章小结
9.1 十二步的完整回顾
| 步 | 方法 | 关键产物 | 扩展点 |
|---|---|---|---|
| ① | Startup.create() |
计时器 | CRaC 感知 |
| ② | shutdownHook.enable... |
--- | spring.main.register-shutdown-hook |
| ③ | createBootstrapContext() |
DefaultBootstrapContext |
BootstrapRegistryInitializer |
| ④ | configureHeadlessProperty() |
--- | setHeadless |
| ⑤ | getRunListeners(args) |
SpringApplicationRunListeners |
SpringApplicationHook |
| ⑥ | listeners.starting(...) |
--- | SpringApplicationRunListener |
| ⑦ | prepareEnvironment(...) |
ConfigurableEnvironment |
EnvironmentPostProcessor |
| ⑧ | printBanner(environment) |
Banner |
自定义 Banner / banner.txt |
| ⑨ | createApplicationContext() |
ApplicationContext |
ApplicationContextFactory |
| ⑩ | prepareContext(...) |
就绪的上下文 | ApplicationContextInitializer |
| ⑪ | refreshContext(context) |
刷新后的上下文 | BeanFactoryPostProcessor 等 |
| ⑫ | started + callRunners + ready |
运行中的应用 | ApplicationRunner/CommandLineRunner |
六个事件 (外加 failed 共七个):
| 事件 | 时机 |
|---|---|
starting |
最早期,环境尚未创建 |
environmentPrepared |
环境就绪,ConfigData 在此加载 |
contextPrepared |
上下文创建、初始化器已应用 |
contextLoaded |
Bean 定义已加载 |
started |
刷新完成,Runner 未执行 |
ready |
Runner 执行完毕,真正就绪 |
failed |
启动失败 |
第 07 章详述事件体系。
9.2 本章的关键发现
-
WebApplicationType.deduce()与ApplicationContextFactory都变成了 SPI(4.0.1+)。这是模块化的必然结果------能力被拆出去后,核心不能再硬编码对它的判断。 -
deduceMainApplicationClass()用StackWalker替代了new RuntimeException().getStackTrace()。这是 Java 9+ API 替换老 hack 的典范,更快且避免Class.forName。 -
prepareEnvironment十行代码九个动作 ,其中ConfigurationPropertySources.attach()调用了两次(因为 environment 可能被替换)。 -
AOT 模式下的三处特殊处理:
createDefaultApplicationContext()用GenericApplicationContext(跳过注解解析)prepareContext不加载 sourcesaddAotGeneratedInitializerIfNecessary注入 AOT initializer 到最前
-
shutdownHook是静态单例,因为 JVM 关闭钩子是全局的。启用与注册分两步,避免失败时误注册。 -
CRaC 支持体现在
Startup策略类中,用 fallback 模式处理"特性存在但当前不适用"。 -
失败处理有五个层次,且用嵌套 try-finally 保证"即使清理失败也要报告错误"。
-
结构化日志开启时 Banner 自动关闭 (
ApplicationProperties.getBannerMode)------一个贴心的小设计。
9.3 三个"反直觉"结论
-
refresh()是 Boot 最"无为"的一步 。Boot 在这一步几乎不做任何事,只调用applicationContext.refresh()。所有魔法都通过 BeanFactoryPostProcessor 在 refresh 内部生效。这说明 Boot 是"注入"到 Spring 的扩展点,而不是"替代"它的启动流程。 -
callRunner用的是两个if而非if-else。如果一个类同时实现ApplicationRunner和CommandLineRunner,两个方法都会被调用。这通常是意外,但 Boot 尊重"你实现了两个接口"这个事实。 -
ready事件在独立的 try 块中 。因为callRunners后应用可能被关闭(Runner 调用了 exit),此时不应发ready。context.isRunning()检查体现了这一点。
9.4 动手验证清单
-
打印启动的每一步 :写一个
SpringApplicationRunListener并在每个方法里打印,观察顺序:javapublic class MyRunListener implements SpringApplicationRunListener { // 必须提供 (SpringApplication, String[]) 构造函数 public MyRunListener(SpringApplication application, String[] args) { } // 实现七个方法 }注册到
META-INF/spring.factories。 -
观察 Runner 的
@Order:写三个CommandLineRunner,分别用类上@Order和方法上@Order,看执行顺序。 -
验证 AOT 命名约定 :运行
./gradlew :module:xxx:processAot,查看生成的*__ApplicationContextInitializer类。 -
验证 Banner 与结构化日志 :配置
logging.structured.format.console=ecs,看 Banner 是否消失。 -
验证 StackWalker :在非 main 方法(如 JUnit 测试)中
new SpringApplication(...),检查getMainApplicationClass()是否为 null。 -
打开启动度量:
javaapplication.setApplicationStartup(new BufferingApplicationStartup(2048));启动后打印所有 StartupStep。
9.5 源码索引
| 类 | 路径 |
|---|---|
SpringApplication |
core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java |
SpringApplicationRunListeners |
同目录 |
SpringApplicationRunListener |
同目录 |
ApplicationProperties |
同目录 |
WebApplicationType |
同目录 |
ApplicationContextFactory |
同目录 |
DefaultApplicationContextFactory |
同目录 |
ApplicationInfoPropertySource |
同目录 |
Runner / ApplicationRunner / CommandLineRunner |
同目录 |
SpringApplicationShutdownHook |
同目录 |
SpringApplicationHook |
同目录 |
AotInitializerNotFoundException |
同目录 |
StartupInfoLogger |
同目录 |
BeanDefinitionLoader |
同目录 |
附录 · run() 完整时序图
scss
main(String[] args)
│
└─ SpringApplication.run(MyApp.class, args)
│
├─ new SpringApplication(MyApp.class)
│ ├─ primarySources = {MyApp}
│ ├─ WebApplicationType.deduce() ← SPI: Deducer
│ ├─ load BootstrapRegistryInitializers ← spring.factories
│ ├─ load ApplicationContextInitializers ← spring.factories
│ ├─ load ApplicationListeners ← spring.factories
│ └─ deduceMainApplicationClass() ← StackWalker
│
└─ run(args)
│
├─ ① Startup.create() ← CRaC 感知
├─ ② shutdownHook.enableShutdownHookAddition()
├─ ③ createBootstrapContext() ← BootstrapRegistryInitializers
├─ ④ configureHeadlessProperty()
├─ ⑤ getRunListeners(args) ← spring.factories + Hook
├─ ⑥ listeners.starting() ★ 事件 1
│
├─ ⑦ prepareEnvironment() ★ 核心
│ ├─ getOrCreateEnvironment() ← ApplicationContextFactory
│ ├─ configureEnvironment()
│ │ ├─ setConversionService(ApplicationConversionService)
│ │ ├─ configurePropertySources() ← 命令行属性等
│ │ └─ configureProfiles() ← 模板方法
│ ├─ ConfigurationPropertySources.attach()
│ ├─ listeners.environmentPrepared() ★ 事件 2(ConfigData 加载)
│ ├─ ApplicationInfoPropertySource.moveToEnd()
│ ├─ DefaultPropertiesPropertySource.moveToEnd()
│ ├─ Assert environment-prefix 不可通过属性设置
│ ├─ bindToSpringApplication() ← spring.main.* 绑定
│ ├─ EnvironmentConverter 转换
│ └─ ConfigurationPropertySources.attach()(再次)
│
├─ ⑧ printBanner(environment)
│
├─ ⑨ createApplicationContext() ← ApplicationContextFactory SPI
│ └─ context.setApplicationStartup()
│
├─ ⑩ prepareContext() ★ 核心
│ ├─ context.setEnvironment()
│ ├─ postProcessApplicationContext() ← ResourceLoader/ConversionService
│ ├─ addAotGeneratedInitializerIfNecessary() ← AOT
│ ├─ setAllowCircularReferences / setAllowBeanDefinitionOverriding
│ ├─ applyInitializers() ★ 扩展点
│ ├─ listeners.contextPrepared() ★ 事件 3
│ ├─ bootstrapContext.close(context)
│ ├─ logStartupInfo / logStartupProfileInfo
│ ├─ registerSingleton(springApplicationArguments / springBootBanner)
│ ├─ LazyInitializationBeanFactoryPostProcessor(可选)
│ ├─ KeepAlive listener(可选)
│ ├─ PropertySourceOrderingBeanFactoryPostProcessor
│ ├─ load(sources) ← BeanDefinitionLoader(非 AOT)
│ └─ listeners.contextLoaded() ★ 事件 4
│
├─ ⑪ refreshContext() ★ Spring 的 refresh()
│ ├─ shutdownHook.registerApplicationContext()
│ └─ context.refresh()
│
├─ ⑫ afterRefresh() ← 模板方法(空)
│ startup.started()
│ logStarted()
│ listeners.started() ★ 事件 5
│ callRunners() ← ApplicationRunner/CommandLineRunner
│
└─ listeners.ready() ★ 事件 6
return context
【失败路径】
catch (Throwable) → handleRunFailure()
├─ AbandonedRunException → 直接返回
├─ handleExitCode() ← ExitCodeExceptionMapper / ExitCodeGenerator
├─ listeners.failed() ★ 事件 7
├─ finally:
│ ├─ reportFailure() ← SpringBootExceptionReporter / FailureAnalyzer
│ └─ context.close() + deregisterFailedApplicationContext()
└─ 返回 RuntimeException 或 IllegalStateException
第 04 章完。
下一章 :第 05 章《
ApplicationProperties与spring.main.*:启动配置的统一载体》------我们将深入 4.x 新增的这个配置类,讲清spring.main.*每一个属性的含义与影响,并完整展开FailureAnalyzer错误分析体系(本章 §5.2 提到的SpringBootExceptionReporter)。