Spring 深度内核-Spring Boot 内核与自动配置-Spring Boot 启动流程:SpringApplication.run() 全解

0 阅读42分钟

概述

本文将不仅拆解启动流程,更将用我们在前文所学的 IoC、DI、扩展点、设计模式等核心知识,作为“X光机”来透视每一步的设计初衷与内部机制。 这将是一次“知其然,更知其所以然”的深度巡游,目标是让您真正打通 Spring 核心容器与 Spring Boot 之间的任督二脉,构建起完整的知识体系。

核心观点导读

任何一个 Spring Boot 应用都始于一行 SpringApplication.run()。这看似简单的一行代码,内部却是一场精心编排的交响乐,它由多个乐章组成:应用类型的智能推断、精密的多层环境构建、IoC 容器的创建与刷新、自动配置的触发、嵌入式 Web 服务器的启动。Spring Boot 并未重复造轮子,而是巧妙地运用了模板方法、观察者、责任链等设计模式,将 Spring 核心容器中的各个组件重新编织,形成了一个清晰、可控且高度可扩展的启动流程。本文将带您深入这场交响乐的幕后,一探究竟。

核心要点提炼

  • 启动骨架SpringApplication 构造 → run() 八阶段生命周期广播 → 环境准备 → 上下文创建与准备 → 刷新上下文(核心容器觉醒)→ 启动 Web 服务器 → 回调执行。
  • 智能推断:基于类路径(Classpath)中的关键类,自动推断应用类型为 SERVLET、REACTIVE 或 NONE。
  • 观察者模式(Observer)SpringApplicationRunListener 是贯穿 run() 方法的全局事件广播器,实现了启动生命周期各阶段的完全解耦。
  • 模板方法模式(Template Method)AbstractApplicationContext.refresh() 依然是核心容器的启动骨架,Spring Boot 只是在子类中通过重写 onRefresh() 等钩子方法植入 Web 服务器启动逻辑。
  • 自动配置入口@EnableAutoConfiguration 注解通过 @Import 机制引入 AutoConfigurationImportSelector,在刷新阶段借助 ConfigurationClassPostProcessor (BDRPP) 批量加载并筛选自动配置类。
  • 用 Spring 知识解读 Boot:理解 Spring Boot 的关键,在于用前文的 IoC、BDRPP、BD、扩展点、SPI 等知识去解构其启动流程。Boot 的每一步都是在管理和驱动 Spring 核心组件,而非凭空创造。

文章组织架构图

graph TD
    Start["开端 一键启动 SpringApplication点run"] --> M1

    subgraph M1["1 启动流程总览"]
        M1_1["宏观骨架与StopWatch"]
        M1_2["观察者模式 SpringApplicationRunListeners"]
    end

    M1 --> M2

    subgraph M2["2 启动前准备 SpringApplication构造"]
        M2_1["推断应用类型"]
        M2_2["SPI加载Initializer与Listener"]
        M2_3["可定制性总结"]
    end

    M2 --> M3

    subgraph M3["3 启动阶段一 环境准备"]
        M3_1["Environment创建"]
        M3_2["PropertySource优先级与整合"]
        M3_3["EnvironmentPostProcessor扩展点"]
        M3_4["激活Profiles"]
    end

    M3 --> M4

    subgraph M4["4 启动阶段二 上下文创建与准备"]
        M4_1["根据类型创建ApplicationContext"]
        M4_2["调用ApplicationContextInitializer"]
        M4_3["注册主配置类为BeanDefinition"]
    end

    M4 --> M5

    subgraph M5["5 启动阶段三 refreshContext 核心容器觉醒"]
        M5_1["模板方法 AbstractApplicationContext点refresh"]
        M5_2["自动配置触发"]
        M5_2_1["ConfigurationClassPostProcessor BDRPP执行"]
        M5_2_2["Import解析与AutoConfigurationImportSelector"]
        M5_2_3["条件注解Conditional筛选"]
    end

    M5 --> M6

    subgraph M6["6 启动阶段四 嵌入式Web服务器启动"]
        M6_1["模板方法钩子 onRefresh"]
        M6_2["WebServer创建与启动"]
    end

    M6 --> M7

    subgraph M7["7 启动阶段五 Runner回调"]
        M7_1["ApplicationRunner与CommandLineRunner"]
        M7_2["发布ApplicationReadyEvent"]
    end

    M7 --> M8["8 全链路协作与知识解读"]
    M8 --> M9["9 可定制性总结"]
    M9 --> M10["10 生产事故排查"]
    M10 --> M11["11 面试高频题"]

架构图分层说明

  1. 总览说明:全文共 11 个核心模块。我们从宏观的 run() 骨架和观察者模式出发(模块 1、2),然后遵循启动的生命周期,逐一拆解环境准备(模块 3)、上下文创建与准备(模块 4)、核心刷新与自动配置触发(模块 5)、Web 服务器启动(模块 6)和最后的 Runner 回调(模块 7)。最后,我们通过一张“全链路协作图”(模块 8)将启动流程与 Spring 核心容器知识进行缝合,并总结其可定制性(模块 9)、排查生产问题(模块 10)和应对面试挑战(模块 11)。

  2. 逐模块说明

    • 模块 1 & 2:奠定基础,展示 run() 的骨架和 SpringApplication 的准备工作,这是后续所有步骤的基石。
    • 模块 3 & 4:进入 run() 方法的早期阶段,构建应用的环境和 IoC 容器,是“运行时”的准备过程。
    • 模块 5本文重点。深入剖析 refresh() 模板方法和自动配置的触发内幕,揭示 Spring Boot 如何复用并增强 Spring 核心容器。
    • 模块 6 & 7:完成应用启动的最后冲刺,启动 Web 服务并执行用户自定义的回调。
    • 模块 8, 9, 10, 11:从实践角度进行拔高和闭环,通过知识整合、事故复盘和面试突击,将技术内化为能力。
  3. 关键结论Spring Boot 的启动流程,本质上是在 Spring 核心容器的生命周期骨架之上,通过 SPI 机制、@Import 机制和各种扩展点接口,构建了一套精密的“自动配置”封装层。理解它,需要全面打通 IoC、Bean 生命周期、后处理器、设计模式和 SPI 等多个核心知识领域,这也正是本文的价值所在。


1. 启动流程总览:SpringApplication.run() 的宏观骨架与观察者模式

1.1 宏观骨架:run() 方法的六阶段交响乐

SpringApplication.run() 方法的内部实现,将所有复杂性都委托给了同一个类的实例方法。我们将该方法的核心流程抽象为六个关键阶段,通过泳道图来直观展示。

graph TD
    subgraph Swimlane_App["应用主线程"]
        A["开始: SpringApplication.run()"] --> B
        B["阶段1: 启动监听器广播"] --> C
        C["阶段2: 准备环境"] --> D
        D["阶段3: 创建并准备上下文"] --> E
        E["阶段4: 刷新上下文 核心: 模板方法refresh()"] --> F
        F["阶段5: 启动嵌入式Web服务器"] --> G
        G["阶段6: 执行Runner回调"] --> H["结束: 应用已就绪"]
    end

    subgraph Legend["图例: 设计模式与扩展点"]
        I["观察者模式: SpringApplicationRunListener"]
        J["模板方法模式: AbstractApplicationContext.refresh()"]
        K["扩展点: ApplicationContextInitializer"]
        L["扩展点: EnvironmentPostProcessor"]
        M["扩展点: ApplicationRunner/CommandLineRunner"]
    end

    B -.-> I
    C -.-> L
    D -.-> K
    E -.-> J
    G -.-> I
  • 图表主旨概括:本图展示了 SpringApplication.run() 方法内部按时间顺序执行的六个核心阶段,并标注了每个阶段应用的关键设计模式或扩展点。

  • 逐层/逐元素分解

    • 主流程:从应用调用 run() 开始,流程严格地、顺序地通过六个阶段。每个阶段都有明确的职责边界。StopWatch 对象在阶段 1 启动,用于精确记录整个启动过程的耗时,体现了 Spring 对性能和可观测性的重视。
    • 阶段 1 (启动监听器广播):它贯穿整个生命周期,通过 SpringApplicationRunListeners 向所有注册的实现 SpringApplicationRunListener 接口的监听器广播事件(如 starting())。
    • 阶段 2 (准备环境):创建和配置应用的运行环境 Environment,这是一个策略模式的应用,根据应用类型创建不同类型的 Environment
    • 阶段 3 (创建并准备上下文):根据应用类型创建对应的 ApplicationContext,并调用 ApplicationContextInitializer 对其进行预配置。
    • 阶段 4 (刷新上下文)整个启动流程的核心。它调用 AbstractApplicationContext.refresh() 模板方法,完成 Bean 的创建、依赖注入、后处理器执行等一系列 IoC 容器的标准工序。这是 Spring 核心容器的领域。
    • 阶段 5 (启动 Web 服务器):如果是 Web 应用,则在此阶段创建并启动内嵌的 Tomcat、Jetty 或 Undertow 服务器。这是在 onRefresh() 钩子中完成的。
    • 阶段 6 (执行 Runner 回调):执行所有实现了 ApplicationRunnerCommandLineRunner 接口的 Bean,这是容器完全启动后,执行一次性初始化任务的入口。
  • 设计原理映射

    • 观察者模式 (Observer)SpringApplicationRunListener 是典型的观察者模式。SpringApplicationRunListeners 作为被观察者(Subject),维护了一个观察者列表。每当进入新的启动阶段,它就通知所有观察者。
    • 模板方法模式 (Template Method):阶段 4 的 refresh() 方法是 Spring 框架中模板方法模式的典范,Boot 完全遵守并复用了这个骨架。
  • 工程联系与关键结论理解这六个阶段,就掌握了 SpringApplication.run() 的顶层设计。每个阶段的实现都体现了高内聚、低内聚的软件设计原则,并通过扩展点机制实现了高度的可定制性。这个骨架是调试任何 Spring Boot 应用启动问题的导航图。

1.2 深入 SpringApplicationRunListener:贯穿始终的观察者

SpringApplicationRunListener 接口是 Spring Boot 启动流程中最核心的扩展点之一,它完美地体现了观察者模式。它允许开发者在应用启动的各个关键里程碑处插入自定义逻辑,而无需修改 Spring Boot 的核心代码,符合开闭原则。

接口方法定义与生命周期事件对照表:

接口方法触发时机 (生命周期阶段)用途说明
starting()run() 方法开始执行后立刻调用,此时还未进行任何处理。记录启动开始、环境准备前的初始化。
environmentPrepared(ConfigurableEnvironment)Environment 对象创建并配置完毕,但在将其应用到 ApplicationContext 之前调用。可以对 Environment 做最后的检查或修改。
contextPrepared(ConfigurableApplicationContext)ApplicationContext 创建并完成初始化,但在加载任何 Bean 定义、调用 refresh() 之前。对刚创建好的上下文进行预配置。
contextLoaded(ConfigurableApplicationContext)ApplicationContext 已经加载了所有 Bean 定义,但尚未调用 refresh() 方法。可以检查和修改已加载的 Bean 定义。
started(ConfigurableApplicationContext)ApplicationContext 已完成 refresh() 方法,应用上下文已刷新,ApplicationRunnerCommandLineRunner 被调用之前。上下文已完全就绪,可以执行一些依赖于所有 Bean 已初始化的逻辑。
running(ConfigurableApplicationContext)ApplicationRunnerCommandLineRunner 都执行完毕,应用完全启动后。执行应用完全启动后的最终逻辑,如发送通知。
failed(ConfigurableApplicationContext, Throwable)启动过程中任何阶段发生错误时调用。处理启动失败,记录错误信息、执行清理或报警。
  • 用 Spring 核心知识解读SpringApplicationRunListeners 并不直接实现 List 接口,而是在内部聚合了一个 List<SpringApplicationRunListener>,并通过组合的方式广播事件。这又是一个经典的组合模式开闭原则的体现。当需要新增一种事件监听器时,我们无需修改 SpringApplicationRunListeners 的广播逻辑,只需在 spring.factories 文件中添加新的注册即可。

  • 内联示例:自定义启动耗时监听器 下面我们创建一个自定义的 SpringApplicationRunListener 来精确记录每个阶段的耗时。

    // MyStartupTimingListener.java
    package com.example.demo.listener;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.SpringApplicationRunListener;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.core.env.ConfigurableEnvironment;
    import org.springframework.util.StopWatch;
    
    // 构造函数是必须的,Spring Boot 会通过反射调用此含参构造器
    public class MyStartupTimingListener implements SpringApplicationRunListener {
    
        private final SpringApplication application;
        private final String[] args;
        private StopWatch stopWatch;
    
        // 必须提供这个构造方法
        public MyStartupTimingListener(SpringApplication application, String[] args) {
            this.application = application;
            this.args = args;
        }
    
        @Override
        public void starting() {
            stopWatch = new StopWatch("MySpringApp");
            stopWatch.start("1. 环境准备");
            System.out.println("【自定义监听器】应用开始启动,计时开始...");
        }
    
        @Override
        public void environmentPrepared(ConfigurableEnvironment environment) {
            stopWatch.stop();
            stopWatch.start("2. 上下文创建与准备");
            System.out.printf("【自定义监听器】环境准备完成,耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis());
        }
    
        @Override
        public void contextPrepared(ConfigurableApplicationContext context) {
            stopWatch.stop();
            stopWatch.start("3. 上下文刷新(refresh)");
            System.out.printf("【自定义监听器】上下文创建并准备完成,耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis());
        }
    
        @Override
        public void contextLoaded(ConfigurableApplicationContext context) {
            // 不在此处停止计时,因为 contextLoaded 和 refresh 紧密相连
        }
    
        @Override
        public void started(ConfigurableApplicationContext context) {
            stopWatch.stop();
            stopWatch.start("4. Runner回调");
            System.out.printf("【自定义监听器】上下文刷新完成,耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis());
        }
    
        @Override
        public void running(ConfigurableApplicationContext context) {
            stopWatch.stop();
            System.out.printf("【自定义监听器】Runner回调执行完成,总耗时[%d]ms%n", stopWatch.getLastTaskTimeMillis());
            // 打印漂亮的格式化报表
            System.out.println(stopWatch.prettyPrint());
        }
    
        @Override
        public void failed(ConfigurableApplicationContext context, Throwable exception) {
             stopWatch.stop();
             System.err.println("【自定义监听器】应用启动失败!");
             exception.printStackTrace();
        }
    }
    

    注册此监听器:在 src/main/resources/META-INF/spring.factories 文件中添加:

    org.springframework.boot.SpringApplicationRunListener=\
    com.example.demo.listener.MyStartupTimingListener
    

    启动应用,即可在控制台看到详细的阶段耗时分析报告。这验证了 SpringApplicationRunListener 的观察者模式机制,其回调时机与我们分析的启动阶段完全一致。


2. 启动前的准备:SpringApplication 的构造、推断与可定制性

2.1 SpringApplication 构造过程源码全解

SpringApplication.run() 是一个静态方法,它内部会先创建一个 SpringApplication 实例,然后调用其 run() 方法。因此,启动的第一个关键步骤就是 SpringApplication 的构造。

sequenceDiagram
    participant User as 用户代码/SpringApplication
    participant SA as SpringApplication(构造器)
    participant WR as WebApplicationType
    participant SFL as SpringFactoriesLoader
    participant FS as spring.factories文件

    User->>SA: new SpringApplication(primarySources)
    SA->>WR: deduceFromClasspath()  <br> <strong>第一步:推断应用类型</strong>
    WR-->>SA: 返回 WebApplicationType (SERVLET/REACTIVE/NONE)
    SA->>SA: setInitializers(null)  <br> <strong>第二步:加载Initializer</strong>
    SA->>SFL: loadFactoryNames(ApplicationContextInitializer.class, classLoader)
    SFL->>FS: 读取 META-INF/spring.factories
    FS-->>SFL: 返回 Initializer 全类名列表
    SFL-->>SA: 实例化并设置
    SA->>SA: setListeners(null)  <br> <strong>第三步:加载Listener</strong>
    SA->>SFL: loadFactoryNames(ApplicationListener.class, classLoader)
    SFL->>FS: 读取 META-INF/spring.factories
    FS-->>SFL: 返回 Listener 全类名列表
    SFL-->>SA: 实例化并设置
    SA->>SA: deduceMainApplicationClass()  <br> <strong>第四步:推断主配置类</strong>
    SA-->>User: 构造完成

下面是 SpringApplication 构造器的核心源码(为简化已去除辅助逻辑):

// org.springframework.boot.SpringApplication
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    
    // 步骤一:推断应用类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    
    // 步骤二:通过 SPI 机制加载并实例化 ApplicationContextInitializer
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    
    // 步骤三:通过 SPI 机制加载并实例化 ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    
    // 步骤四:推断主配置类
    this.mainApplicationClass = deduceMainApplicationClass();
}

源码解读

  • WebApplicationType.deduceFromClasspath():这是“智能推断”的核心。它通过检查特定类路径下是否存在关键类来决定应用类型。例如,如果 DispatcherServlet 类存在,则是 SERVLET 应用;如果 DispatcherHandler 存在但 DispatcherServlet 不存在,则是 REACTIVE 应用。

  • getSpringFactoriesInstances():这是一个非常重要的方法,它封装了 Spring Boot 的 SPI 机制。它从 META-INF/spring.factories 文件中读取指定接口的实现类全限定名,然后进行实例化。这里加载了 ApplicationContextInitializerApplicationListener,它们是启动流程早期阶段的关键扩展点。

  • deduceMainApplicationClass():这个方法通过分析 new Throwable().getStackTrace() 的调用栈,来找到一个包含 main 方法且类名匹配的类作为“主配置类”。这是一种相当巧妙但可靠的推断方式。

  • 用 Spring 核心知识解读

    • SPI 与扩展点体系(第 9、7 篇):构造过程的第二步和第三步是 Spring Boot 可插拔性设计的核心。它完全利用了我们前文讲解的 SpringFactoriesLoader,这是 Spring 框架原生 SPI 机制的增强版。ApplicationContextInitializerApplicationListener 这两个接口是优秀的扩展点契约,Spring Boot 启动时自动发现并执行它们,实现了对容器启动过程的声明式定制。
    • 设计模式:构造器本身使用了生成器(Builder)模式的思想,虽然它不是传统的 Builder 类,但其构造过程是将多个独立、复杂的组件(类型、Initializer、Listener)进行组装,形成一个复杂且可用的 SpringApplication 对象。

2.2 SpringApplication 的可定制性总结

run() 之前,SpringApplication 提供了丰富的编程式定制手段,与声明式 SPI 方式互为补充。

定制方式具体手段示例描述
编程式SpringApplication.setXxx()setBannerMode(Off)setAdditionalProfiles("dev")addInitializers()addListeners()在代码中直接修改 SpringApplication 实例的属性,优先级较高。
编程式Builder APInew SpringApplicationBuilder().sources(Parent.class).child(Child.class).run(args)为构建分层的 ApplicationContext 提供了流式 API。
声明式META-INF/spring.factoriesorg.springframework.context.ApplicationListener=\com.my.MyListener利用 SPI 机制,将扩展点实现类自动注册到 SpringApplication 中。
外部化启动参数/环境变量等--server.port=8081run() 的环境准备阶段被整合到 Environment 中,影响后续配置。

3. 启动阶段一:环境准备——PropertySource 优先级与扩展点

环境准备是 run() 方法执行的第一个实质性阶段,它为应用的运行构建了上下文——Environment 对象。

sequenceDiagram
    participant RUN as SpringApplication.run()
    participant PREP_ENV as prepareEnvironment()
    participant ENV as ApplicationServletEnvironment
    participant MPS as MutablePropertySources
    participant POST as EnvironmentPostProcessor
    participant EP_LIST as EnvironmentPostProcessors Factory

    RUN->>PREP_ENV: 调用
    PREP_ENV->>ENV: 创建 Environment (根据应用类型)
    
    Note over PREP_ENV, MPS: 整合 PropertySource (按优先级从高到低)
    PREP_ENV->>MPS: addLast(commandLineArgs)  <br> <strong>1. 命令行参数</strong>
    PREP_ENV->>MPS: addLast(servletConfigInitParams) <br>2. Servlet上下文初始化参数
    PREP_ENV->>MPS: addLast(servletContextInitParams) <br>3. Servlet上下文参数
    PREP_ENV->>MPS: addLast(systemProperties)  <br>4. 系统属性
    PREP_ENV->>MPS: addLast(systemEnvironment) <br>5. 操作系统环境变量
    PREP_ENV->>MPS: addLast(random)            <br>6. RandomValuePropertySource
    PREP_ENV->>MPS: addLast(applicationConfig) <br>7. application.properties/yml
    
    PREP_ENV->>EP_LIST: 加载并调用 EnvironmentPostProcessors
    EP_LIST->>POST: postProcessEnvironment(environment, application)
    POST-->>EP_LIST: 修改后的 Environment
    EP_LIST-->>PREP_ENV: 完成

    PREP_ENV->>ENV: 解析 spring.profiles.active 并激活相关 Profile
    PREP_ENV-->>RUN: 返回 ConfigurableEnvironment
  • 图表主旨概括:此序列图展示了 prepareEnvironment 阶段如何创建一个 Environment 对象,并按照严格的优先级顺序整合各类属性源,最后调用 EnvironmentPostProcessor 扩展点。

  • 逐层/逐元素分解

    • 参与者SpringApplication.run() 作为调用者,prepareEnvironment() 是执行者。ApplicationServletEnvironment(Web 应用默认)是 Environment 的具体实现。MutablePropertySources 是存放所有属性源的数据结构。EnvironmentPostProcessor 是关键的扩展点。
    • 属性源整合:这是图解的核心。Spring Boot 将来自不同地方的配置信息包装成一个个 PropertySource 对象,然后按照从高到低的优先级,通过 addFirstaddLast 方法插入到 MutablePropertySourcesCopyOnWriteArrayList 中。越晚被 addFirst 插入的优先级越高。因此,属性查找时,会优先从命令行参数开始,一层层找到配置文件。这种优先级设计深刻体现了“约定优于配置,但也尊重显式声明”的哲学。
    • 扩展点调用:在所有默认属性源都加载完毕后,Spring Boot 通过 SPI 机制加载并执行 EnvironmentPostProcessor,为开发者提供了一个在 ApplicationContext 创建前最后一个修改 Environment 的机会。
  • 设计原理映射

    • 策略模式:根据 webApplicationType 创建不同的 Environment 实现(ApplicationServletEnvironmentApplicationReactiveWebEnvironment)。
    • 责任链模式EnvironmentPostProcessor 列表的执行过程可以看作一个变体的责任链,每个处理器都有机会修改 Environment
  • 工程联系与关键结论掌握 PropertySource 的优先级是排查线上配置不生效问题的首要技能。 当发现一个配置值不是预期的时候,应该立即想到它被更高优先级的源覆盖了。MutablePropertySources 的有序列表结构是理解这一切的关键。

3.1 核心源码解读:属性源加载与 EnvironmentPostProcessor

// org.springframework.boot.SpringApplication
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // 1. 创建 Environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 2. 配置 Environment (属性源,profiles)
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 3. 加载并调用 EnvironmentPostProcessor (扩展点,通过SpringFactoriesLoader)
    ConfigurationPropertySources.attach(environment);
    listeners.environmentPrepared(environment);
    // ...省略将environment绑定到SpringApplication的部分
    return environment;
}

protected void configurePropertySources(MutablePropertySources propertySources, String[] args) {
    // ... existing property sources ...
    if (args.length > 0) {
        // 简化的命令行参数解析,实际上会封装为SimpleCommandLinePropertySource
        propertySources.addFirst(new SimpleCommandLinePropertySource(args));
    }
    // 整合系统属性与系统环境
    propertySources.addLast(new PropertiesPropertySource(System.getProperties()));
    propertySources.addLast(new SystemEnvironmentPropertySource(System.getenv()));
    // ...
}
  • 说明addFirstaddLast 直接决定了优先级。命令行参数通过 addFirst 被放在列表头部,获得最高优先级。

  • 内联示例:自定义 EnvironmentPostProcessor 我们可以通过一个自定义的 EnvironmentPostProcessor 在环境准备完成后动态添加一个配置源。

    // MyEnvironmentPostProcessor.java
    package com.example.demo.env;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.env.EnvironmentPostProcessor;
    import org.springframework.core.env.ConfigurableEnvironment;
    import org.springframework.core.env.PropertiesPropertySource;
    
    import java.util.Properties;
    
    public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor {
    
        @Override
        public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
            Properties props = new Properties();
            // 模拟从远程配置中心拉取配置
            props.put("app.custom.message", "Hello from Remote Config Center!");
            // 创建一个新的PropertySource
            PropertiesPropertySource pps = new PropertiesPropertySource("remoteConfig", props);
            // 添加到首位,使其拥有最高优先级
            environment.getPropertySources().addFirst(pps);
            System.out.println("【自定义EnvironmentPostProcessor】已加载远程配置并设为最高优先级!");
        }
    }
    

    注册:在 META-INF/spring.factories 中添加:

    org.springframework.boot.env.EnvironmentPostProcessor=com.example.demo.env.MyEnvironmentPostProcessor
    

    之后在应用的任何地方注入 @Value("${app.custom.message}") 这个属性值,都可以获取到“Hello from Remote Config Center!”。这验证了 EnvironmentPostProcessor 在环境准备阶段的强大扩展能力,其执行时机正是在 Environment 创建之后、ApplicationContext 创建之前。

  • 关联第 10 篇(类型转换与数据绑定)@ConfigurationProperties 注解背后的“黑魔法”正是依赖于 Environment 中的 ConversionService。当我们将 application.yml 中的字符串 “30s” 绑定到一个 Duration 类型的 Java 字段时,ConversionService 会自动完成类型转换。这背后的机制我们在前文已详细讨论。


4. 启动阶段二:ApplicationContext 的创建与准备

有了前期的类型推断和环境准备,接下来的任务就是创建并预初始化核心的 IoC 容器。

sequenceDiagram
    participant RUN as SpringApplication.run()
    participant CREATE as createApplicationContext()
    participant CTX as ApplicationContext
    participant PREP as prepareContext()
    participant INIT as ApplicationContextInitializer
    participant LIST as SpringApplicationRunListeners

    RUN->>CREATE: 调用
    CREATE->>CTX: 根据webApplicationType <br> 实例化具体类
    alt SERVLET
        CTX-->>CREATE: AnnotationConfigServletWebServerApplicationContext
    else REACTIVE
        CTX-->>CREATE: AnnotationConfigReactiveWebServerApplicationContext
    else NONE
        CTX-->>CREATE: AnnotationConfigApplicationContext
    end
    CREATE-->>RUN: 返回 context

    RUN->>PREP: prepareContext(context, environment, ...)
    PREP->>CTX: setEnvironment(environment)
    PREP->>INIT: 遍历并调用 initializers <br> <strong>步骤1: 调用ApplicationContextInitializer</strong>
    loop 每个 Initializer
        INIT->>CTX: initialize(context)
    end
    PREP->>LIST: contextPrepared(context) <br> (广播事件)
    
    PREP->>CTX: load(primarySources) <br> <strong>步骤2: 将主配置类注册为BeanDefinition</strong>
    
    PREP->>CTX: registerSingleton(...) <br> <strong>步骤3: 注册默认单例Bean</strong>
    PREP->>LIST: contextLoaded(context) <br> (广播事件)
    
    PREP-->>RUN: 准备完毕
  • 用 Spring 核心知识解读

    • 容器抽象(第 1 篇)createApplicationContext 方法根据类型推断结果,返回 ApplicationContext 接口的不同实现。AnnotationConfigServletWebServerApplicationContext 这种冗长的类名,清晰地表明了其职责:支持@Configuration 注解、面向 Servlet Web 环境、并具备启动嵌入式 Web 服务器的能力。这完美地体现了我们前文所学的“面向接口编程”和“容器抽象”的思想。上层代码(即 run() 方法)只与 ConfigurableApplicationContext 交互,而不关心其具体实现。
    • prepareContext 的职责:此阶段是 refresh() 之前最后的“准备期”。
      1. 调用 ApplicationContextInitializer:这些是在 SpringApplication 构造时加载的,现在它们对刚刚创建的 ApplicationContext 进行“预处理”,例如激活某些 Profile、设置资源加载器等。
      2. 注册主配置类:将 primarySources(即我们的主应用类,如 @SpringBootApplication 标注的类)解析并注册为 BeanDefinitionBeanDefinitionRegistry 中。这是一个关键动作。它意味着我们的主配置类变成了一个普通的 Bean 定义。为什么后续 refresh() 能处理它?因为我们的主配置类上肯定有 @ComponentScan@SpringBootApplication(它内部包含 @ComponentScan),而 refresh() 阶段的 ConfigurationClassPostProcessor 会解析这些注解,从而完成组件扫描和自动配置。Spring Boot 只负责“注册定义”,核心容器负责“解析定义并驱动生命周期”,职责分离得淋漓尽致(关联第 7 篇扩展点体系)。
  • 内联示例:自定义 ApplicationContextInitializer

    // MyApplicationContextInitializer.java
    package com.example.demo.initializer;
    
    import org.springframework.context.ApplicationContextInitializer;
    import org.springframework.context.ConfigurableApplicationContext;
    
    public class MyApplicationContextInitializer implements ApplicationContextInitializer {
        @Override
        public void initialize(ConfigurableApplicationContext context) {
            // 在Bean定义加载前,激活“test” profile
            context.getEnvironment().addActiveProfile("test");
            System.out.println("【自定义Initializer】初始化上下文,已激活[test] profile。");
            System.out.println("【自定义Initializer】上下文ID: " + context.getId());
        }
    }
    

    注册:在 META-INF/spring.factories 中添加:

    org.springframework.context.ApplicationContextInitializer=com.example.demo.initializer.MyApplicationContextInitializer
    

    启动应用,你会在日志中看到 profile 已被激活,并且该 Initializer 的日志在 contextPrepared 事件之后、contextLoaded 事件之前打印,验证了其精确的执行时机。


5. 启动阶段三:刷新上下文——核心容器的觉醒与自动配置触发(重点模块)

这是整个启动流程最核心、最复杂的阶段。Spring Boot 在此阶段并没有另起炉灶,而是通过“复用 + 增强” 的方式,将 Spring Core 的强大能力与自身的自动配置理念完美结合。

5.1 refresh() 模板方法的概要回顾

refreshContext 方法最终会调用到 AbstractApplicationContext.refresh(),这套我们熟悉的 12 步模板方法序列。Spring Boot 仅仅在 refresh() 前后增加了自己的逻辑,并在关键的钩子方法中植入了 Web 服务器的启动逻辑。

graph TD
    Start["refreshContext(context)"] --> Pre["pre-refresh: 注册shutdown hook等"]
    Pre --> CallRefresh["调用 context.refresh()"]
    
    subgraph CoreRefresh ["AbstractApplicationContext.refresh() 模板方法"]
        S1["1. prepareRefresh()"] --> S2["2. obtainFreshBeanFactory()"]
        S2 --> S3["3. prepareBeanFactory()"]
        S3 --> S4["4. postProcessBeanFactory() 模板方法钩子: 空实现 留给子类"]
        S4 --> S5["5. invokeBeanFactoryPostProcessors() 核心: 自动配置触发点"]
        S5 --> S6["6. registerBeanPostProcessors()"]
        S6 --> S7["7. initMessageSource()"]
        S7 --> S8["8. initApplicationEventMulticaster()"]
        S8 --> S9["9. onRefresh() 模板方法钩子: 启动Web服务器"]
        S9 --> S10["10. registerListeners()"]
        S10 --> S11["11. finishBeanFactoryInitialization()"]
        S11 --> S12["12. finishRefresh()"]
    end
    
    CallRefresh --> S1
    S12 --> Post["post-refresh: 可选的afterRefresh钩子"]
    Post --> End["返回上下文"]

用 Spring 核心知识解读:这个 12 步模板方法是整个 Spring 生态的基础设施(第 2、11 篇),它定义了 IoC 容器启动的固定算法骨架。Spring Boot 作为这个模板方法的“用户”或“子类”,通过在特定步骤(主要是第 5 步和第 9 步)使用的扩展机制,来注入自己的自动配置和 Web 服务器启动逻辑。

5.2 自动配置触发过程详解

这是模板方法中第 5 步 (invokeBeanFactoryPostProcessors) 的核心内容,也是 @EnableAutoConfiguration 最终发挥作用的地方。

sequenceDiagram
    participant Core as AbstractApplicationContext
    participant BDRPP as ConfigurationClassPostProcessor<br>(BDRPP)
    participant Parser as ConfigurationClassParser
    participant AIS as AutoConfigurationImportSelector
    participant Cond as ConditionalEvaluator

    Core->>BDRPP: postProcessBeanDefinitionRegistry(registry) <br> <strong>Step 1: BDRPP被执行</strong>
    BDRPP-->>Core: 开始处理所有@Configuration类

    BDRPP->>Parser: 遍历并解析每个配置类
    
    Note over Parser: 处理主配置类,发现@SpringBootApplication
    Parser->>Parser: 解析@SpringBootApplication上的元注解
    Parser->>Parser: 发现@EnableAutoConfiguration
    Parser->>Parser: 解析@EnableAutoConfiguration上的@Import
    
    Note over Parser, AIS: @Import(AutoConfigurationImportSelector.class)
    Parser->>AIS: 调用 selectImports() <br> <strong>Step 2: 触发自动配置选择器</strong>
    
    AIS->>AIS: getCandidateConfigurations() <br> <strong>Step 3: 加载候选配置</strong>
    Note over AIS: 从META-INF/spring/<br>org.springframework.boot<br>.autoconfigure.AutoConfiguration.imports<br>和 spring.factories 读取
    
    AIS->>Cond: 对每个候选配置进行条件筛选 <br> <strong>Step 4: 按条件过滤</strong>
    Cond->>Cond: 检查@ConditionalOnClass, <br>@ConditionalOnMissingBean等
    Cond-->>AIS: 返回满足条件的配置类
    
    AIS-->>Parser: 返回最终的自动配置类列表
    Parser->>Core: 将配置类解析为BeanDefinition并注册
    BDRPP-->>Core: 继续执行,进入Bean的实例化生命周期
  • 图表主旨概括:此序列图聚焦于 invokeBeanFactoryPostProcessors 阶段,详细展示了 @EnableAutoConfiguration 注解如何通过 @ImportAutoConfigurationImportSelectorSpringFactoriesLoader 等一系列联动,最终完成自动配置的加载和筛选。

  • 逐层/逐元素分解

    1. ConfigurationClassPostProcessor (BDRPP) 启动:作为 Spring 最核心的后处理器之一,它在 invokeBeanFactoryPostProcessors 中被优先执行。它的任务就是扫描所有已经注册的 @Configuration 类。我们的主配置类(带有 @SpringBootApplication)在阶段 4 已经被注册,所以此刻它被“逮个正着”。
    2. @Import 链追踪ConfigurationClassParser 解析我们的 MainApplication 类,发现它被 @SpringBootApplication 标记,进而解析出 @EnableAutoConfiguration,最终发现其上的 @Import(AutoConfigurationImportSelector.class)。这完全印证了我们前文(第 8 篇)所学的 @Import 机制。
    3. AutoConfigurationImportSelector 执行selectImports() 方法被调用,这是自动配置的黑箱核心。它内部调用 getAutoConfigurationEntry()
    4. 候选配置加载与筛选
      • 加载getCandidateConfigurations() 方法利用 SpringFactoriesLoader,从所有 jar 包的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsspring.factories 文件中提取 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键对应的所有全限定类名。这些就是“自动配置候选项”。
      • 筛选:这是最关键的一步。它遍历所有候选项,使用 ConditionalEvaluator 评估每个配置类上的 @ConditionalOnXxx 条件注解。例如,如果某个配置类标记了 @ConditionalOnClass({ MongoClient.class }),但应用的类路径下没有 MongoDB 的依赖,这个配置类就会被移除。
    5. 注册 BeanDefinition:筛选通过后的配置类,会被 ConfigurationClassParser 当作 @Configuration 类继续解析,它们内部定义的 Bean(如 DataSourceJdbcTemplate 等)最终都会被解析为 BeanDefinition 注册到 BeanFactory 中。
  • 设计原理映射

    • 模板方法模式:整个 refresh() 方法是骨架,invokeBeanFactoryPostProcessors 是其中一步。
    • 责任链模式BeanFactoryPostProcessor 的执行可以看作一个责任链,ConfigurationClassPostProcessor 是这个链上的核心一环。
    • 策略模式@ConditionalOnXxx 注解是策略模式的体现,@Conditional 是策略接口,其不同的实现类(OnClassCondition, OnBeanCondition)是具体策略,用于动态决定配置是否生效。
  • 工程联系与关键结论自动配置不是什么“魔法”,而是一套基于“SPI发现 + @Import导入 + @Conditional条件筛选”的精密机制。 如果你掌握了这三个核心知识点(分别对应第 9、8、- 篇),你就彻底理解了自动配置的内核。

5.3 核心源码解读:自动配置选择器

// org.springframework.boot.autoconfigure.AutoConfigurationImportSelector

// 入口方法,由 ConfigurationClassParser 调用
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    }
    // 获取自动配置条目
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    // ... 检查是否禁用等 ...
    
    // 1. 获取所有候选配置类名
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    
    // 2. 去重
    configurations = removeDuplicates(configurations);
    
    // 3. 获取应排除的配置
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    
    // 4. 应用条件筛选 (核心)
    checkExcludedClasses(configurations, exclusions);
    configurations = filter(configurations, autoConfigurationMetadata); 
    // filter方法内部会使用ConditionalEvaluator进行筛选
    
    // 5. 触发事件 (AutoConfigurationImportEvent)
    fireAutoConfigurationImportEvents(configurations, exclusions);
    
    return new AutoConfigurationEntry(configurations, exclusions);
}

// 通过 SpringFactoriesLoader 加载候选配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
            getBeanClassLoader());
    // ... 从 AutoConfiguration.imports 等位置加载 ...
    return configurations;
}

源码解读:这段代码清晰地展示了自动配置的加载和筛选链路。getCandidateConfigurations 负责“一网打尽”,filter 方法负责“优胜劣汰”。fireAutoConfigurationImportEvents 则体现了其可观测性,允许我们监听自动配置的导入过程。


6. 启动阶段四:嵌入式 Web 服务器的启动——模板方法钩子

sequenceDiagram
    participant Core as AbstractApplicationContext
    participant SW as ServletWebServerApplicationContext
    participant Factory as TomcatServletWebServerFactory
    participant WS as TomcatWebServer
    participant SCI as ServletContextInitializer(DispatcherServlet等)

    Note over Core: refresh()执行到第9步
    Core->>SW: onRefresh() <br> <strong>模板方法钩子被调用</strong>
    
    SW->>SW: createWebServer()
    SW->>Factory: getWebServer()
    Factory->>WS: new TomcatWebServer(...)
    activate WS
    WS->>WS: 初始化Tomcat容器
    Factory->>SCI: 获取所有 ServletContextInitializer Beans
    loop 每个 Initializer
        Factory->>SCI: customize(context) <br> (注册到ServletContext)
    end
    WS->>WS: start() <br> <strong>Tomcat正式启动,监听端口</strong>
    deactivate WS
    WS-->>SW: 返回已启动的 WebServer
    
    SW-->>Core: onRefresh() 结束
    Note over Core: refresh()继续后续步骤(finishRefresh等)
  • 图表主旨概括:本序列图展示了在 refresh() 模板方法的第 9 步 onRefresh() 钩子中,ServletWebServerApplicationContext 如何创建、配置并启动一个嵌入式 Tomcat 服务器。

  • 逐层/逐元素分解

    • 模板方法钩子onRefresh() 是一个受保护的空方法,由 AbstractApplicationContext 定义。ServletWebServerApplicationContext 重写了它,并且通常在其中只做一件事:调用 createWebServer()
    • 工厂模式应用createWebServer() 会获取一个 ServletWebServerFactory Bean。这通常是 TomcatServletWebServerFactoryJettyServletWebServerFactoryUndertowServletWebServerFactory。这是标准的工厂方法模式(不是 GOF 的,而是 Spring 的实践模式)。
    • 自适应服务器创建ServletWebServerFactory.getWebServer() 是一个创建性操作。它会 new 一个具体的 WebServer 实例(如 TomcatWebServer),并将所有实现了 ServletContextInitializer 接口的 Bean(包括最重要的 DispatcherServletRegistrationBean,它会注册 DispatcherServlet)传递给 WebServer
    • 服务器启动:最后,webServer.start() 被调用,Tomcat/Jetty/Undertow 的内置实例正式开始监听端口,准备接收 HTTP 请求。
  • 设计原理映射

    • 模板方法模式:最核心的设计。核心容器的启动算法(refresh())不变,子类通过重写 onRefresh() 来添加特定于 Web 环境的启动步骤,完全符合开闭原则。
    • 工厂方法模式ServletWebServerFactory 负责创建 WebServer 对象,将对象的创建逻辑和使用逻辑解耦。
  • 工程联系与关键结论onRefresh() 是 Spring Boot 将 Web 服务器启动逻辑嵌入到 Spring 核心容器生命周期中的唯一接口。 如果你需要自定义 Web 服务器的配置(如端口、线程池等),你应该首先想到定制 ServletWebServerFactory Bean。


7. 启动阶段五:Runner 回调与启动完成

这是启动流程的尾声,标志着应用已完全就绪,可以开始对外服务。

  • afterRefresh(Context):一个可选的钩子,在 refresh() 紧接完成后、任何 Runner 调用之前执行。默认是空实现。
  • callRunners(Context, args):从 ApplicationContext 中获取所有实现了 ApplicationRunnerCommandLineRunner 接口的 Bean,按 @Order 排序后依次调用。
    • CommandLineRunner 接收原始的 String... args
    • ApplicationRunner 接收封装好的 ApplicationArguments,它提供了更方便的选项参数(如 --foo=bar)解析能力。
  • 发布 ApplicationReadyEvent:在所有 Runner 执行完毕后,会发布这个事件,标志着应用真正“启动完成”。SpringApplicationRunListeners.running() 也在此阶段被调用。

用 Spring 核心知识解读:Runner 回调机制本质上是利用 IoC 容器的基础能力,允许在容器完全初始化之后,立即执行一次性的、依赖于容器内 Bean 的初始化任务。相比于 @PostConstruct,Runner 的执行时机更晚,更能保证外部资源(如网络、数据库)已完全就绪。


8. 全链路协作图:用 Spring 核心容器知识解读整个启动流程

下表将 Spring Boot 的每个启动步骤与 Spring 核心容器的组件、设计模式及我们系列文章的知识点进行关联,帮助您形成系统性的认知。

Spring Boot 启动步骤对应的 Spring 核心容器组件/方法使用的设计模式关联的系列篇章
1. 构造与类型推断SpringFactoriesLoader, ClassLoader生成器模式, SPI机制第 9 篇: SPI与插件化
2. 环境准备Environment, PropertySource, MutablePropertySources策略模式, 责任链模式第 1 篇: 容器抽象, 第 10 篇: 类型转换
3. 上下文创建ApplicationContext 接口及其实现工厂方法模式, 面向接口编程第 1 篇: 容器抽象
4. 上下文准备ApplicationContextInitializer, BeanDefinitionRegistry观察者模式, 扩展点机制第 7 篇: 扩展点体系
5. 刷新上下文AbstractApplicationContext.refresh()模板方法模式第 2 篇: Bean生命周期, 第 11 篇: 设计模式
5.1 自动配置触发ConfigurationClassPostProcessor, @Import, @Conditional责任链模式, 策略模式第 7 篇: BDRPP, 第 8 篇: @Import机制, 第 9 篇: SPI
6. Web服务器启动onRefresh() 钩子, ServletWebServerFactory模板方法模式, 工厂方法模式第 11 篇: 设计模式
7. Runner回调ApplicationRunner, CommandLineRunner, ApplicationEvent观察者模式第 7 篇: 扩展点体系
贯穿全流程SpringApplicationRunListener观察者模式第 11 篇: 设计模式

关键结论:从这张表中可以清晰地看到,Spring Boot 的启动流程本身就是一本 Spring 核心容器设计能力的最佳实践教科书。 它的每一步,无论是环境构建、容器刷新还是 Web 服务器启动,都严格建立在 IoC、DI、扩展点和设计模式等基础能力之上。Spring Boot 并未创造一个新世界,而是让 Spring 的世界变得更加智能和自动化。


9. SpringApplication 的可定制性总结

为了让你对如何干预启动流程了如指掌,我们将其可定制性归纳如下:

定制层面具体方式执行/生效阶段示例
编程式SpringApplication.setXxx()SpringApplication 构造后, run()setBannerMode(Off)setAdditionalProfiles("dev")
编程式SpringApplication.addXxx()SpringApplication 构造后, run()addInitializers(new MyInitializer()), addListeners(new MyListener())
编程式SpringApplicationBuilderSpringApplication 构造期间new SpringApplicationBuilder().sources(App.class).profiles("dev").run(args)
声明式(SPI)spring.factoriesSpringApplication 构造期间注册 ApplicationContextInitializer, ApplicationListener, SpringApplicationRunListener, EnvironmentPostProcessor
外部化配置命令行, 环境变量, application.ymlprepareEnvironment 阶段--server.port=8081, export SPRING_PROFILES_ACTIVE=dev
注解驱动@EnableAutoConfiguration, @ComponentScanrefreshContext 阶段主配置类上的这些注解,决定了自动配置和组件扫描的范围

10. 生产事故排查专题

理论最终要服务于实践。下面我们复盘三个典型的生产事故,并给出排查思路和工具。

案例一:启动卡住,无任何日志输出

  • 现象:应用启动后,长时间没有任何输出,既不抛出异常,也不继续执行。线程看似“假死”。
  • 排查工具/命令
    • jcmd <pid> VM.command_line:查看启动参数。
    • jstack -l <pid>:打印所有线程的堆栈信息,分析线程状态。
  • 排查思路
    1. 使用 jcmd 找到 Java 进程 PID。
    2. 多次执行 jstack -l <pid>,观察线程堆栈。重点关注 main 线程和任何处于 WAITINGBLOCKED 状态的线程。
    3. 检查 main 线程的堆栈,看它卡在哪个启动阶段。
  • 根因(结合启动流程):最常见的原因是某个 ApplicationContextInitializerSpringApplicationRunListener 中的自定义代码存在死锁、死循环或长时间的网络/IO等待。例如,一个在 starting() 方法中尝试连接不可达的外部服务且没有设置超时的 Listener。
  • 解决与最佳实践
    • 立即修复阻塞的代码。对于外部依赖调用,必须设置合理的网络超时、异步化或使用断路器。
    • 最佳实践:在关键自定义扩展点(如 Initializer, Listener)中增加明确的日志,并尽量保持逻辑轻量和幂等。

案例二:application.yml 配置不生效

  • 现象:在 application.yml 中配置了 server.port=8888,但应用依然从 8080 端口启动,或者自定义属性取不到值。
  • 排查工具/命令
    • wget/curl http://localhost:8080/actuator/env 或启用 Spring Boot Actuator 的 env 端点。
    • 在代码中注入 Environment,编写一个 CommandLineRunner 打印所有属性源。
      @Component
      public class EnvPrinter implements CommandLineRunner {
          @Autowired private ConfigurableEnvironment env;
          @Override public void run(String... args) {
              env.getPropertySources().forEach(ps -> {
                  System.out.println("Source: " + ps.getName() + " -> " + ps.getSource());
              });
          }
      }
      
  • 排查思路
    1. 使用上述方法打印出 MutablePropertySources 中的所有 PropertySource
    2. 检查 server.port 属性存在于哪个源中。你会发现它可能在 commandLineArgs 或者系统环境变量中也存在。
    3. 确认 application.yml 对应的 PropertySource 是否被加载,以及它在列表中的位置。
  • 根因(结合启动流程):优先级覆盖。根据我们第 3 节的分析,application.yml 的优先级远低于命令行参数和系统环境变量。如果同时存在,高优先级的源会覆盖低优先级的源的配置。
  • 解决与最佳实践
    • 检查是否在启动命令中传入了 --server.port=8080,或者设置了名为 SERVER_PORT 的环境变量。这是 12-Factor App 推荐的实践,但需要团队知晓其优先级。
    • 最佳实践:通过 Actuator 的 env 端点或代码打印,将运行时 Environment 的可视化作为启动后的标准检查项。

案例三:嵌入式容器端口冲突

  • 现象:应用启动时,控制台抛出 java.net.BindException: Address already in use 异常,启动失败。
  • 排查工具/命令
    • Linux/Mac: lsof -i :8080netstat -anp | grep 8080
    • Windows: netstat -ano | findstr :8080
  • 排查思路
    1. 使用上述命令,找出正在占用目标端口(如 8080)的进程 ID (PID)。
    2. 通过 ps -ef | grep <PID> (Linux) 或任务管理器 (Windows) 确认该进程是不是之前没被杀死的旧应用进程,或者其他应用。
  • 根因(结合启动流程):在 ServletWebServerApplicationContext.onRefresh() 阶段,WebServer.start() 尝试绑定到指定端口时,发现该端口已被其他进程或本应用的前一个未完全终止的实例占用。
  • 解决与最佳实践
    • 立即杀掉占用端口的非预期进程。
    • 更改本应用的端口:--server.port=8090
    • 最佳实践:启用 server.port=0 让系统随机分配可用端口(在开发测试中很方便但生产不推荐),或在生产环境的部署脚本中加入强制杀掉旧进程并等待的操作。更重要的是,确保应用有优雅停机机制,避免强行 KILL。

11. 面试高频专题

(本模块严格与正文分离,旨在从面试角度巩固知识)

  1. 问题:请完整描述 SpringApplication.run() 方法从开始到结束的主要步骤。

    • 标准回答:分六步:1. 启动监听器广播;2. 环境准备;3. 上下文创建与准备;4. 刷新上下文;5. 启动嵌入式Web服务器;6. 执行Runner回调。
    • 追问 1SpringApplication 实例是在什么时候创建的?
      回答:在调用静态 run() 方法时,内部会先 new 一个 SpringApplication 实例,再调用其实例 run() 方法。
    • 追问 2:应用类型是在哪一步被推断出来的?
      回答:在 SpringApplication 的构造过程中,通过 WebApplicationType.deduceFromClasspath() 静态方法推断。
    • 追问 3SpringApplicationRunListenerApplicationListener 有什么区别?
      回答:前者是专门为监听 run() 方法启动生命周期而设的扩展点,后者是 Spring 核心容器的通用事件监听器,监听如 ContextRefreshedEvent 等事件。前者启动全过程可用,后者在 refresh() 之后才注册其事件广播器。
    • 加分回答:能结合源码,指出 SpringApplication 构造时通过 SpringFactoriesLoader 加载 ApplicationListener,但这些监听器会一直跟随到 refresh() 阶段,被注册进 ApplicationContext 成为通用事件监听器的一部分。
  2. 问题:Spring Boot 在刷新上下文时,是如何触发自动配置的?(关联型题目)

    • 标准回答:在 invokeBeanFactoryPostProcessors 阶段,ConfigurationClassPostProcessor 解析 @SpringBootApplication 上的 @EnableAutoConfiguration 注解,该注解通过 @Import 引入了 AutoConfigurationImportSelector。该 Selector 通过 SpringFactoriesLoader 加载所有候选的自动配置类,并用 @Conditional 注解进行筛选,最后符合条件的配置类会被注册进容器。
    • 追问 1ConfigurationClassPostProcessorBeanFactoryPostProcessor 还是 BeanDefinitionRegistryPostProcessor?这有什么不同?
      回答:它是 BeanDefinitionRegistryPostProcessor。后者是前者的子接口,它允许在标准 BeanFactoryPostProcessor 执行之前,修改 BeanDefinitionRegistry,即可以注册新的 Bean 定义,这正是 @Import 和组件扫描需要的能力。
    • 追问 2@Import 机制在这里起到了什么作用?
      回答:它是“桥梁”,将 @EnableAutoConfiguration 这个标记注解与实际的逻辑处理器 AutoConfigurationImportSelector 连接起来。当解析到 @Import 时,容器会实例化并调用该 Selector。
    • 追问 3:如果一个自动配置类上有 @ConditionalOnMissingBean(DataSource.class),但项目里手动配置了一个,这个自动配置类还会生效吗?为什么?
      回答:不会。AutoConfigurationImportSelectorfilter 阶段会使用 ConditionalEvaluator 评估所有 @Conditional 注解。OnBeanCondition 会检查容器中是否已存在 DataSource bean,如果存在,则不满足 OnMissingBean 条件,该配置类会被移除。
    • 加分回答:能进一步讲清楚 AutoConfigurationImportSelector 内部使用 AutoConfigurationImportEvent 来广播导入事件,可以用于监控和调试自动配置。
  3. 问题SpringApplication.run() 内部有哪些地方用到了模板方法模式?举出至少两个。(关联型题目)

    • 标准回答:1. AbstractApplicationContext.refresh() 是整个启动的核心,它定义了 12 步的算法骨架,如 postProcessBeanFactory()onRefresh() 等都是留给子类的钩子,SpringApplication.run() 最终会调用它。2. SpringApplication 自身的 run() 方法也可以看作一个模板方法,其中包含了 afterRefresh() 钩子。
    • 追问 1onRefresh() 在 Spring Boot 中具体用来做什么?
      回答:在 ServletWebServerApplicationContext 中,onRefresh() 钩子被重写以启动嵌入式 Web 服务器(Tomcat、Jetty 等)。
    • 追问 2:为什么不直接在 run() 方法里写死启动 Web 服务器的代码,而非要用模板方法呢?
      回答:这完全是出于对开闭原则的遵守。未来如果有了新的容器类型(如 Netty),我们只需要创建新的 ApplicationContext 子类并重写 onRefresh() 即可,核心的 refresh() 框架完全不需要做任何修改。
    • 追问 3postProcessBeanFactory() 这个钩子在Boot里有什么应用?
      回答AnnotationConfigServletWebServerApplicationContext 重写了它,用于注册一个 WebApplicationContextServletContextAwareProcessor,它能自动将 ServletContextServletConfig 注入给实现了相关 Aware 接口的 Bean。
    • 加分回答:能明确指出模板方法模式的原则是“Don‘t call us, we’ll call you”,并关联好莱坞原则。
  4. 问题:如果不用 Spring Boot,如何手动实现类似的自动配置机制?(系统设计/关联型题目)

    • 标准回答:需要组合Spring核心组件:1. 创建一个 @EnableXxx 注解,在其内部用 @Import 引入一个 ImportSelector 实现类。2. 该 ImportSelector 实现类负责读取配置文件(类似 spring.factories),加载候选配置类。3. 基于 @Conditional 注解和 ConditionEvaluator 实现一个条件筛选机制。4. 在 ImportSelector 中将筛选后的配置类全名返回,Spring 容器会自动处理它们。
    • 追问 1:在这个过程中,BeanDefinitionRegistryPostProcessor 是必要的吗?
      回答:不必自己实现,但整个机制是建立在它之上的。@Import 注解的处理是由 Spring 内置的 ConfigurationClassPostProcessor(一个 BDRPP)完成的。
    • 追问 2:你如何设计条件筛选?
      回答:我会定义一个 @MyConditional 注解,并实现一个 MyConditionMatcher 来读取注解的元数据并检查当前上下文(类路径、已有Bean等)。ImportSelector 在返回类名之前,会遍历候选类并调用这些 Matcher 进行筛选。
    • 追问 3:如何让用户方便地注册他们的配置类?
      回答:借鉴 spring.factories 的设计,约定一个配置文件(比如 META-INF/my-autoconfig.properties),使用键值对方式,让用户将配置类全名写在该文件中。我的 ImportSelector 会扫描所有这类文件并加载。
    • 加分回答:能讨论到实现一个 SPI 机制时的类加载器隔离问题,以及如何通过 @Order 等注解处理配置类的加载顺序。
  5. 问题:请描述 ApplicationContextInitializerSpringApplicationRunListenerBeanFactoryPostProcessor 的执行顺序和用途。

    • 标准回答
      • SpringApplicationRunListener:执行顺序最早,贯穿 run() 全生命周期。
      • ApplicationContextInitializer:在 ApplicationContext 创建后、refresh() 之前被调用。
      • BeanFactoryPostProcessor:在 refresh()invokeBeanFactoryPostProcessors 阶段被调用。
    • 追问 1:如果想在 Bean 实例化之前修改 Bean 定义,应该用哪个?
      回答BeanFactoryPostProcessor(特别是其子接口 BeanDefinitionRegistryPostProcessor)。
    • 追问 2:如果想在环境准备后、上下文创建前修改环境参数,应该用哪个?
      回答EnvironmentPostProcessorSpringApplicationRunListenerenvironmentPrepared 事件。
    • 追问 3ApplicationContextInitializer 能用来做数据库初始化吗?
      回答:不推荐。它执行时,BeanFactory 甚至还没有初始化,无法拿到数据库连接池等 Bean。它适用于修改上下文属性、激活 Profile 等非常早期的、与 Bean 无关的配置。
    • 加分回答:能绘制出一个清晰的生命周期时间线,将这些扩展点精确地标注在 run()refresh() 的时序图上。
  6. 问题SpringApplicationBuilder 的作用是什么?

    • 标准回答:用于构建具有父子上下文关系的 Spring Boot 应用,提供了流式 API。
    • 追问 1:什么场景会用到父子上下文?
      回答:比如 Spring Boot + Spring Cloud 的应用,Spring Cloud 的 Bootstrap 上下文就是应用的父上下文。
    • 追问 2SpringApplicationBuilder 和直接 new SpringApplication 然后调用 run() 有什么区别?
      回答:Builder 可以更优雅地管理多个 SpringApplication 实例和它们之间的关系,尤其适合构建分层上下文。直接使用 SpringApplication 通常用于单层上下文。
  7. 问题:Spring Boot 如何保证 CommandLineRunnerApplicationRunner 的执行顺序?

    • 标准回答:通过实现 org.springframework.core.Ordered 接口或标注 @Order 注解来指定顺序,数字越小优先级越高。
    • 追问 1:如果两个 Runner 都没指定顺序,执行顺序是怎样的?
      回答:没有预定义的顺序,可能会受到 Bean 注册顺序的影响,但这是不确定的,不应依赖。
    • 追问 2:某个 Runner 抛出异常会发生什么?
      回答:这将导致 run() 方法退出,异常会被抛出,导致整个应用启动失败。SpringApplicationRunListener.failed() 会被调用。
    • 追问 3ApplicationRunnerCommandLineRunner 参数有何不同?
      回答ApplicationRunner.run(ApplicationArguments args) 参数提供了强大的选项参数(--foo=bar)和非选项参数(non-option-arg)的解析能力,而 CommandLineRunner.run(String... args) 只是传递了原始的字符串数组。
  8. 问题:如何在不使用 Spring Boot Actuator 的情况下,获取应用启动耗时?

    • 标准回答:可以自定义一个 SpringApplicationRunListener,在 starting() 方法中启动 StopWatch,在 running() 方法中停止计时并打印报表。
    • 追问 1:还有什么其他方法可以监控启动时间?
      回答:还可以利用 Spring Framework 的 ApplicationStartedEventApplicationReadyEvent 来手动计时。
    • 追问 2StopWatchprettyPrint() 方法输出的报表包含哪些信息?
      回答:包含每个任务名称、耗时(毫秒)以及各任务耗时占总耗时的百分比。
  9. 问题spring.factories 文件中的键值对是什么意思?

    • 标准回答:键是接口的全限定名,值是该接口的实现类的全限定名列表(逗号分隔)。
    • 追问 1:这个文件是如何被加载的?
      回答:由 SpringFactoriesLoader.loadFactories()loadFactoryNames() 方法加载。
    • 追问 2:为什么 Spring Boot 2.7 开始引入 AutoConfiguration.imports 文件?
      回答:主要是为了性能优化和职责分离。spring.factories 用途广泛,加载它时需要加载整个文件。而 AutoConfiguration.imports 专用于自动配置,加载更快,并且避免了不必要的类加载。这是向 Spring Boot 3.x 完全移除对 spring.factories 支持迈出的第一步。
  10. 问题:启动时常见的“Cannot determine embedded database driver class for database type NONE”错误怎么解决?

    • 标准回答:这个错误是因为 DataSourceAutoConfiguration 根据类路径推断需要配置嵌入式数据库,但找不到合适的驱动。解决方案:排除该自动配置类或在 application.yml 中明确配置数据源连接信息。
    • 追问 1:如何排除某个特定的自动配置类?
      回答:在 @SpringBootApplication 注解中使用 exclude 属性,如 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    • 追问 2:为什么会出现这个错误?
      回答:因为项目中引入了 Spring Data JPA 或 MyBatis 等依赖,它们触发了 DataSourceAutoConfiguration 的条件,但你又没有配置任何数据库连接信息。
    • 追问 3:如何排查是哪个自动配置类被触发了?
      回答:在 application.yml 中设置 logging.level.org.springframework.boot.autoconfigure=DEBUG,控制台会打印出详细的正负条件匹配报告。
  11. 问题@Value 注入和 @ConfigurationProperties 在底层是如何工作的?

    • 标准回答:两者都是依赖 Environment 对象抽象。@Value 是通过 AutowiredAnnotationBeanPostProcessor 在属性填充环节进行注入,其值解析依靠 ConfigurableBeanFactory 中的 resolveEmbeddedValue 方法链最终委托给 PropertySourcesPropertyResolver。而 @ConfigurationProperties 是通过 ConfigurationPropertiesBindingPostProcessor,它会在 Bean 初始化后,将 Environment 中的属性绑定到带有该注解的 Bean 上,利用 Binder API 和 ConversionService 完成类型转换。
    • 追问 1:哪个优先级更高,@Value 还是 application.properties 里的默认值?
      回答@Value 是“消费”配置的一方,它没有优先级。它的值来源是 Environment,而 application.properties 是其众多 PropertySource 之一。
    • 追问 2@ConfigurationProperties 如何做到类型安全的?
      回答:它利用了 Spring 的 ConversionService,该服务前面有各种 Converter,可以将 String 转换成 Duration, DataSize 等多种复杂类型。
    • 追问 3:如果属性名在文件中是 app-name,Java 字段应该怎么写,为什么?
      回答:应该写为 appName,这是宽松绑定(Relaxed Binding)@ConfigurationProperties 支持将短横线隔开的命名映射到驼峰命名的字段。
  12. 问题:Spring Boot 应用启动快结束时,控制台输出的彩色日志和启动信息是在哪个阶段实现的?

    • 标准回答:日志的 Banner 输出发生在 SpringApplication.run() 的最早期,由 Banner 接口及其实现类(如 SpringBootBanner)完成。而“Started XxxApplication in X.XX seconds” 这句日志是在 callRunners() 之后,由 logStarted() 方法输出的,具体时机在 SpringApplicationRunListeners.started() 回调完成后。
    • 追问 1:如何自定义 Banner?
      回答:在 resources 目录下放一个 banner.txt 文件,或者编程式地通过 SpringApplication.setBanner() 设置。
    • 追问 2:可以关闭这个日志输出吗?
      回答:可以,通过 SpringApplication.setBannerMode(Banner.Mode.OFF)setLogStartupInfo(false)
  13. 问题:如果在一个 ApplicationContextInitializersetAdditionalProfiles("dev"),和在 application.yml 中设定 spring.profiles.active: dev,哪个优先级高?

    • 标准回答ApplicationContextInitializer 中的设置优先级更高。
    • 追问 1:为什么?
      回答:因为 ApplicationContextInitializer 是在 Environment 整合 application.yml 之后执行的,它可以覆盖之前的任何设置。实际上,spring.profiles.active 本身也是从 application.yml 或别的源中读取的。Initializer 中的编程式 addActiveProfile 是最后写入的。
    • 追问 2:这会造成什么问题?
      回答:可能会造成“幽灵配置”,即团队里有人在代码或内部二方库的 spring.factories 里硬编码了 profile,导致排查配置问题时非常困难。
  14. 问题:如果在 onRefresh() 阶段,也就是 Web 服务器启动时发生 OOM,会发生什么?failed() 监听器会被调用吗?

    • 标准回答failed() 监听器被调用。
    • 追问 1:它的流程是怎样的?
      回答AbstractApplicationContext.refresh() 内部会 catch 住异常,然后关闭上下文并重新 throw。这个异常会传播到 SpringApplication.run() 方法,其 catch 块会 handleRunFailure(),在这个方法中会调用 listeners.failed(context, exception)
    • 追问 2:在这种场景下,context 对象可用吗?
      回答:可能处于部分初始化的状态,在 failed() 监听器中使用它时必须非常小心,做好判空和状态检查。
  15. 系统设计题:请设计一个 Spring Boot 应用启动分析工具,它能精确测量每个自动配置类的加载耗时,并识别出最耗时的几个 Bean 初始化过程。

    • 标准回答
      1. 测量自动配置耗时:利用 SpringApplicationRunListenerAutoConfigurationImportListener。在 contextLoaded 事件时,注册一个定制的 BeanFactoryPostProcessor。这个BDRPP在 postProcessBeanDefinitionRegistry 开始时计时。通过监听 AutoConfigurationImportEvent,可以知道哪些类被导入了。但这只能得到整体的 invokeBeanFactoryPostProcessors 耗时。更细粒度的话,需要自定义一个代理 ConfigurationClassPostProcessor 来做耗时记录,实现复杂但可行。
      2. 识别最耗时的 Bean 初始化:实现一个 BeanPostProcessor。在 postProcessBeforeInitialization 中记录开始时间,在 postProcessAfterInitialization 中记录耗时。将这些数据存入一个 ConcurrentHashMap
      3. 数据可视化:待到 ApplicationReadyEvent 事件触发后,用一个 ApplicationRunner 将所有耗时数据读取出来,按耗时排序,输出到日志或通过一个 HTTP 端点暴露,完成分析。
    • 追问 1:你的 BeanPostProcessor 会对业务 Bean 有侵入性吗?
      回答:没有侵入性,它只是一个容器级的后处理器,对业务 Bean 的代码是透明的。
    • 追问 2:如何避免你的计时工具本身成为性能瓶颈?
      回答:使用高并发容器如 ConcurrentHashMap,并尽量保持计时逻辑极简,避免在 BeanPostProcessor 中进行复杂的日志打印或 IO 操作,只在最后统一处理和输出。
    • 追问 3:如何设计使这个工具模块化,不使用时不影响启动速度?
      回答:可以将它封装成一个独立的 starter,内部使用 @ConditionalOnProperty@ConditionalOnClass 进行开关控制,只有当引入这个 starter 并设置了特定配置项(如 my.startup.analysis.enabled=true)时,才会加载并启用分析组件。

延伸阅读

  1. 《Spring Boot 编程思想》 - 小马哥 (mercyblitz): 深入 Spring Boot 源码的必读经典,对自动配置和启动流程有极为详尽的剖析。
  2. Spring Boot 官方参考文档 (第十节: SpringApplication) : 最权威的第一手资料,详细解释了 SpringApplication 的各项配置和特性。
  3. Spring Framework 官方源码 AbstractApplicationContext.refresh(): 阅读其方法上的 Javadoc 和源码注释,是理解 Spring 核心容器生命周期的关键。
  4. 《Spring 揭秘》 - 王福强: 虽然有些年头,但对 IoC、AOP 和容器设计的底层逻辑讲解依然非常透彻。
  5. 博客: "How Spring Boot’s Autoconfiguration Works" (类似 Baeldung 上的专题文章): 此类文章通常配有清晰的图和时序,有助于快速建立全局观。

展望 Spring Boot 3.x 启动流程变化 Spring Boot 3.x 基于 Spring Framework 6,其启动流程有了革命性的变化,主要体现在 AOT (Ahead-Of-Time) 编译 方面。传统的动态启动模型(JVM 启动 → 推断 → 加载配置 → 解析 → 实例化)正在被优化。借助 GraalVM native-image 技术,Spring Boot 3.x 应用可以在构建时完成大部分启动阶段的“计算”,将推断、配置解析乃至 Bean 定义的注册提前完成,生成可直接运行的本地映像。此外,spring.factories 文件已被正式废弃,完全由 AutoConfiguration.imports 文件替代,进一步简化了 SPI 机制并提升了性能。这些激动人心的变化将在我们后续的 AOT 专篇 中详细展开。


附录:Spring Boot 启动流程速查表

启动阶段核心方法关键扩展点使用的设计模式关联系列篇章
1. 构造与准备SpringApplication 构造器ApplicationContextInitializer
ApplicationListener
SpringFactoriesLoader
SPI 机制第 9 篇: SPI与插件化
2. 全生命周期SpringApplication.run()SpringApplicationRunListener观察者模式第 11 篇: 设计模式全景
3. 环境准备prepareEnvironment()EnvironmentPostProcessor
MutablePropertySources
策略模式第 1 篇: 容器抽象
第 10 篇: 类型转换
4. 上下文创建与准备prepareContext()ApplicationContextInitializer工厂方法模式第 1 篇: 容器抽象
第 7 篇: 扩展点体系
5. 核心刷新refreshContext()BeanFactoryPostProcessor
BeanPostProcessor
@Import 选择器
模板方法模式第 2, 7, 8, 9, 11 篇
6. Web服务器启动refreshContext() -> onRefresh()ServletWebServerFactory
TomcatWebServer
模板方法模式第 11 篇: 设计模式全景
7. 后置回调callRunners()ApplicationRunner
CommandLineRunner
命令模式 (变体)第 7 篇: 扩展点体系