Spring Boot 4.1.1 深度源码解析-第 04 章 · `SpringApplication` 全景:`run()` 的十二步状态机

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。

本章的目标不是让你"记住"这个类,而是让你理解:

  1. 启动过程的完整阶段划分------每一步做了什么、为什么需要这一步
  2. 每一步之间的数据依赖------前一步的产物如何成为后一步的输入
  3. 扩展点在哪里------哪些步骤允许用户介入,怎么介入
  4. 失败怎么处理------启动失败时 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<>();

四个值得注意:

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;
}

这个方法有两个问题:

  1. 创建异常对象开销大 (new RuntimeException().getStackTrace() 需要填充整个栈)
  2. Class.forName 可能失败(类加载器问题)

StackWalker 的优势:

  1. 惰性 :walk() 是流式的,可以提前终止(找到就 findFirst())
  2. RETAIN_CLASS_REFERENCE :直接得到 Class 对象,不需要 Class.forName
  3. 更快: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);
}

为什么要分两步? 因为:

  1. enableShutdownHookAddition() 是一个全局开关 ,告诉 SpringApplicationShutdownHook "后续可以接受注册"
  2. 如果启动过程中失败了(还没到 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();
}

三级降级:

  1. 用户通过 setEnvironment(...) 提供 → 直接用
  2. applicationContextFactory 能创建 → 用它
  3. 否则 → 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.port
  • SERVER_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());
    }
}

三件事:

  1. 注册自定义 BeanNameGenerator(作为一个特殊名称的单例)
  2. 设置 ResourceLoader(同时设置 ClassLoader)
  3. 把 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 模式下的关键逻辑:

  1. 如果处于 AOT 模式(AotDetector.useGeneratedArtifacts())
  2. 查找已有的 AotApplicationContextInitializer
  3. 如果没有,按约定构造类名:主类名 + "__ApplicationContextInitializer"
  4. 如果该类不存在,抛 AotInitializerNotFoundException
  5. 把 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:

  1. requiredType != null ------ initializer 必须声明泛型参数。例如 class MyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>。
  2. 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;
}
  • 第一级:ExitCodeExceptionMapper Bean 映射(用户可自定义异常 → 退出码)
  • 第二级:异常自身实现 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 的失败处理体现了几个原则:

  1. 不吞掉原始异常 :handleRunFailure 最终总是返回一个包含原始异常的 RuntimeException
  2. 失败也要清理资源 :finally 中关闭上下文
  3. 失败也要通知 :listeners.failed 事件
  4. 失败要可诊断 :FailureAnalyzer 给出建议
  5. 清理失败不影响错误报告:内层 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;
}

流程:

  1. 创建 RunListener
  2. 创建 SpringApplicationHook,它的 getRunListener 返回 runListener,同时在返回前修改 springApplication(加 sources、加 profiles)
  3. 用 withHook 设置 ThreadLocal,然后调用 main
  4. 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();
    }

}

设计要点:

  1. 三个抽象方法 :startTime()、processUptime()、action() ------ 定义了"计时策略"的全部可变部分
  2. started() 是 final ------ 基类控制算法骨架,子类只提供数据(模板方法模式)
  3. timeTakenToStarted() 有 Assert.state ------ 防止未调用 started() 就读取
  4. 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(...) 就够了。需要定制的场景:

  1. 需要自定义 Banner
  2. 需要添加/替换监听器或初始化器
  3. 需要关闭命令行属性或转换服务
  4. 需要自定义 ApplicationContextFactory
  5. 需要设置默认属性
  6. 需要懒初始化或 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.starting
  • spring.boot.application.environment-prepared
  • spring.boot.application.context-prepared
  • spring.boot.application.context-loaded
  • spring.boot.application.started
  • spring.boot.application.ready
  • spring.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 表达"这是兜底值"用了三重信号,缺任何一重都会让读代码的人误判:

  1. 命名 :DefaultPropertiesPropertySource.NAME = "defaultProperties" (core/spring-boot/src/main/java/org/springframework/boot/env/DefaultPropertiesPropertySource.java:45)、 ApplicationInfoPropertySource.NAME = "applicationInfo"(ApplicationInfoPropertySource.java:40)------ 名字本身就写着"默认 / 信息";
  2. 位置 :moveToEnd(...) 把它移到链尾,使它的优先级最低(§3);
  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. 本章未逐行核实 / 待你实测的项

  1. SpringApplication 的静态工具方法与内部类未逐个展开(§6 只是清单)。
  2. StartupStep 的完整埋点集合未统计。
  3. SpringApplicationShutdownHook 的并发与超时细节未实测。
  4. CRaC 相关分支只点到,未读 spring-boot-crac 模块。
  5. 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 本章的关键发现

  1. WebApplicationType.deduce() 与 ApplicationContextFactory 都变成了 SPI(4.0.1+)。这是模块化的必然结果------能力被拆出去后,核心不能再硬编码对它的判断。

  2. deduceMainApplicationClass() 用 StackWalker 替代了 new RuntimeException().getStackTrace()。这是 Java 9+ API 替换老 hack 的典范,更快且避免 Class.forName。

  3. prepareEnvironment 十行代码九个动作 ,其中 ConfigurationPropertySources.attach() 调用了两次(因为 environment 可能被替换)。

  4. AOT 模式下的三处特殊处理:

    • createDefaultApplicationContext() 用 GenericApplicationContext(跳过注解解析)
    • prepareContext 不加载 sources
    • addAotGeneratedInitializerIfNecessary 注入 AOT initializer 到最前
  5. shutdownHook 是静态单例,因为 JVM 关闭钩子是全局的。启用与注册分两步,避免失败时误注册。

  6. CRaC 支持体现在 Startup 策略类中,用 fallback 模式处理"特性存在但当前不适用"。

  7. 失败处理有五个层次,且用嵌套 try-finally 保证"即使清理失败也要报告错误"。

  8. 结构化日志开启时 Banner 自动关闭 (ApplicationProperties.getBannerMode)------一个贴心的小设计。

9.3 三个"反直觉"结论

  1. refresh() 是 Boot 最"无为"的一步 。Boot 在这一步几乎不做任何事,只调用 applicationContext.refresh()。所有魔法都通过 BeanFactoryPostProcessor 在 refresh 内部生效。这说明 Boot 是"注入"到 Spring 的扩展点,而不是"替代"它的启动流程。

  2. callRunner 用的是两个 if 而非 if-else 。如果一个类同时实现 ApplicationRunner 和 CommandLineRunner,两个方法都会被调用。这通常是意外,但 Boot 尊重"你实现了两个接口"这个事实。

  3. ready 事件在独立的 try 块中 。因为 callRunners 后应用可能被关闭(Runner 调用了 exit),此时不应发 ready。context.isRunning() 检查体现了这一点。

9.4 动手验证清单

  1. 打印启动的每一步 :写一个 SpringApplicationRunListener 并在每个方法里打印,观察顺序:

    java 复制代码
    public class MyRunListener implements SpringApplicationRunListener {
        // 必须提供 (SpringApplication, String[]) 构造函数
        public MyRunListener(SpringApplication application, String[] args) { }
        // 实现七个方法
    }

    注册到 META-INF/spring.factories。

  2. 观察 Runner 的 @Order :写三个 CommandLineRunner,分别用类上 @Order 和方法上 @Order,看执行顺序。

  3. 验证 AOT 命名约定 :运行 ./gradlew :module:xxx:processAot,查看生成的 *__ApplicationContextInitializer 类。

  4. 验证 Banner 与结构化日志 :配置 logging.structured.format.console=ecs,看 Banner 是否消失。

  5. 验证 StackWalker :在非 main 方法(如 JUnit 测试)中 new SpringApplication(...),检查 getMainApplicationClass() 是否为 null。

  6. 打开启动度量:

    java 复制代码
    application.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)。

相关推荐
geats人山人海1 小时前
java 8 字符串
java·开发语言
鱼宵1 小时前
支付网关收官:Spring Boot 里,策略、模板方法、观察者怎么一起干活
spring boot·后端·面试
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 01 章 · Spring Boot 4 的坐标系:版本矩阵、模块拓扑与源码阅读方法论
java·后端·开源
云和数据.ChenGuang1 小时前
Golang AI 工程师路线图:从入门到实战
开发语言·人工智能·后端·golang
zyseo81 小时前
谷歌SEO 内链地图脚本:Python 画出权重分发,揪出孤岛页
java·前端·python
Sayai1 小时前
Java 进程 Unable to create new native thread 排查实战:Linux 线程数五层限制与 JVM -Xss 调优
java·linux·运维·jvm·性能优化
wuminyu1 小时前
Continuation::freeze与thaw的x86_64汇编Stack Chunk拆解源码分析
java·linux·c语言·汇编·jvm·c++
高频因子挖掘机1 小时前
分钟线跨午休后时间轴不连续,是数据错了还是 A 股交易时段本来如此?
后端·github·api
程序员夏洛1 小时前
TCP 是用来解决什么问题?
java·网络协议·http·https