人人都会AI编程

26.4 Spring Boot 启动流程源码解析

更新时间:2026-07-11

如果你曾好奇:为什么在 main 方法里执行一行 SpringApplication.run(),整个应用就能启动并对外服务?这一节我们就深入 SpringApplication 的源码,拆解 Spring Boot 启动的完整过程。理解这些步骤,不仅有助于排查启动时的诡异问题,也能让你对自动配置、事件监听、扩展点有更立体的认识。

26.4.1 SpringApplication.run() 的两大阶段

从调用入口看:

@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

run 静态方法内部会先创建一个 SpringApplication 实例,然后调用实例方法 run。整个启动被清晰地分为 准备阶段 和 运行阶段。

26.4.2 准备阶段:new SpringApplication(primarySources)

SpringApplication 的构造器主要做了两件事:推断应用类型、加载初始化器与监听器。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath(); // ①
    this.bootstrapRegistryInitializers = new ArrayList<>(
            getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); // ②
    setInitializers(
            getSpringFactoriesInstances(ApplicationContextInitializer.class)); // ③
    setListeners(getSpringFactoriesInstances(ApplicationListener.class));    // ④
    this.mainApplicationClass = deduceMainApplicationClass();                // ⑤
}

① 推断 Web 类型:deduceFromClasspath() 会检查 classpath 下是否存在 DispatcherServlet 等关键类,决定创建 SERVLET、REACTIVE 还是普通的 ApplicationContext。
②-④ 加载扩展点:通过 SpringFactoriesLoader 从 META-INF/spring.factories(Spring Boot 2.x)或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3.x)中读取并实例化 ApplicationContextInitializer 和 ApplicationListener 等。这些是 Spring Boot 留给开发者的常用扩展接口。
⑤ 推断主类:通过检查调用栈,找到包含 main 方法的类并记录下来。

实用提醒:ApplicationContextInitializer 和 ApplicationListener 是定制启动行为的两个核心入口。如果你希望在容器刷新之前对上下文做一些个性化配置,可以实现前者并通过 spring.factories 注册。

26.4.3 运行阶段:实例方法 run(String... args)

这是真正的启动流程,核心代码在 SpringApplication.run() 实例方法中。我把整个过程拆成 10 个关键步骤,并附上对应的源码片段。

public ConfigurableApplicationContext run(String... args) {
    long startTime = System.nanoTime();
    DefaultBootstrapContext bootstrapContext = createBootstrapContext(); // 1
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);    // 2
    listeners.starting(bootstrapContext, this.mainApplicationClass);   // 3
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                bootstrapContext, applicationArguments);                // 4
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);                // 5
        context = createApplicationContext();                           // 6
        context.setApplicationStartup(this.applicationStartup);
        prepareContext(bootstrapContext, context, environment,
                listeners, applicationArguments, printedBanner);        // 7
        refreshContext(context);                                        // 8
        afterRefresh(context, applicationArguments);                    // 9
        Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
        listeners.started(context, timeTakenToStartup);                // 10
        callRunners(context, applicationArguments);                    // 11
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }
    try {
        Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
        listeners.ready(context, timeTakenToReady);                    // 12
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

下面逐一解析每个步骤。


1. 创建 BootstrapContext(Spring Boot 2.4+)

DefaultBootstrapContext 是一个轻量级的引导上下文,用于在环境准备之前共享资源(如配置信息的早期来源)。它会在环境准备好后被关闭,其生命周期比 ApplicationContext 更短。

2. 获取 SpringApplicationRunListeners

从 spring.factories 加载 SpringApplicationRunListener 的实现,默认只有 EventPublishingRunListener,它会将启动过程中的各个里程碑转化为 Spring 事件,广播给之前加载的 ApplicationListener。

3. 发布 starting 事件

listeners.starting(...) 标志着启动正式开始。此时 ApplicationContext 还未创建,开发者可以通过监听 ApplicationStartingEvent 做一些非常早期的处理(比如初始化日志框架)。

4. 准备 Environment

private ConfigurableEnvironment prepareEnvironment(
        SpringApplicationRunListeners listeners,
        DefaultBootstrapContext bootstrapContext,
        ApplicationArguments applicationArguments) {
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    listeners.environmentPrepared(bootstrapContext, environment);
    // ... 移动到SpringApplication中处理
    bindToSpringApplication(environment);
    // 如果有自定义的转换逻辑,此处还会执行
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader())
                .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}
  • 根据 Web 类型创建对应的 Environment(如 StandardServletEnvironment)。
  • 将命令行参数、系统属性、配置文件等添加到环境中。
  • 发布 ApplicationEnvironmentPreparedEvent 事件,此时 ConfigFileApplicationListener 会监听到并加载 application.properties/yml 等配置文件。

5. 打印 Banner

如果启用了 banner,控制台会打印 Spring 的 ASCII 艺术字。你可以通过 spring.banner.location 指定自定义 banner,或实现 Banner 接口编程式打印。

6. 创建 ApplicationContext

根据之前推断的 webApplicationType 实例化对应的容器:AnnotationConfigServletWebServerApplicationContext(Servlet)、AnnotationConfigReactiveWebServerApplicationContext(响应式)或 AnnotationConfigApplicationContext(普通)。

7. 准备上下文 (prepareContext)

这一步在 refresh 之前,将各种准备好的零件安装到刚创建的空容器中:

private void prepareContext(DefaultBootstrapContext bootstrapContext, 
        ConfigurableApplicationContext context, ConfigurableEnvironment environment,
        SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, 
        Banner printedBanner) {
    context.setEnvironment(environment);
    postProcessApplicationContext(context);
    applyInitializers(context);                // 调用所有 ApplicationContextInitializer
    listeners.contextPrepared(context);
    // 关闭 BootstrapContext
    bootstrapContext.close(context);
    // 将启动参数、Banner 等注册为 Bean
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    // 加载 sources,即主配置类
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[0]));
    listeners.contextLoaded(context);
}

关键点:

  • applyInitializers 会执行所有 ApplicationContextInitializer,允许你在 refresh 前对上下文进行编程式调整。
  • load(context, sources) 把主配置类(被 @SpringBootApplication 标注的类)注册为 Bean 定义,它是后续自动配置的入口。

8. 刷新上下文 (refreshContext)

调用 AbstractApplicationContext.refresh(),这是 Spring Framework 的核心启动方法,也是整个过程中最“重”的一步。主要内容包括:

  • prepareRefresh():准备刷新,设置启动时间、属性源等。
  • obtainFreshBeanFactory():解析配置类,扫描并注册所有 Bean 定义。
  • prepareBeanFactory():注册内置 Bean,如 environment、systemProperties。
  • postProcessBeanFactory():留给子类扩展,例如 Web 容器的上下文会在这里注册 ServletContext。Spring Boot 的自动配置入口 ConfigurationClassPostProcessor 也会在这一阶段解析 @Configuration 类。
  • invokeBeanFactoryPostProcessors():调用所有 BeanFactoryPostProcessor,其中 ConfigurationClassPostProcessor 负责处理 @ComponentScan、@Import、@ImportResource 等,并触发 @EnableAutoConfiguration 的加载逻辑,最终完成自动配置。
  • registerBeanPostProcessors():注册 BeanPostProcessor(如 AutowiredAnnotationBeanPostProcessor)。
  • initMessageSource()、initApplicationEventMulticaster():国际化与事件广播器。
  • onRefresh():由子类实现。Web 容器上下文在此创建内嵌的 Web 服务器(Tomcat/Jetty/Undertow)并启动。
  • registerListeners():注册已存在的 ApplicationListener Bean。
  • finishBeanFactoryInitialization():实例化所有非懒加载的单例 Bean。
  • finishRefresh():发布 ContextRefreshedEvent,启动 Web 服务器完成并接受请求。

9. afterRefresh

afterRefresh 是一个空实现,留给子类覆盖。如果你需要自定义 SpringApplication,可以在此处添加容器刷新后的额外处理。

10. 发布 started 事件

listeners.started(context, timeTakenToStartup) 发布 ApplicationStartedEvent。此时 ApplicationContext 已刷新,但 CommandLineRunner 和 ApplicationRunner 还未执行。你可以利用这个事件执行在应用真正“就绪”之前的逻辑。

11. 调用 Runner

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    for (Object runner : new LinkedHashSet<>(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
    }
}

这两个 Runner 是 Spring Boot 提供的应用启动后的回调接口。常用于启动后加载缓存、预加载数据或打印启动信息。ApplicationRunner 和 CommandLineRunner 的区别仅在于参数封装不同,前者是 ApplicationArguments,后者是原始 String... args。

12. 发布 ready 事件

listeners.ready(context, timeTakenToReady) 发布 ApplicationReadyEvent。此时整个启动流程完全结束,应用真正处于可用状态。你可以在此时执行一些必须等技术完全就绪后才能进行的操作。

26.4.4 启动过程的事件体系

Spring Boot 在启动过程中会依次发布这些事件:

  1. ApplicationStartingEvent – 启动开始
  2. ApplicationEnvironmentPreparedEvent – 环境准备完成
  3. ApplicationContextInitializedEvent – Context 创建并经过初始化后
  4. ApplicationPreparedEvent – Context 准备完成,即将刷新
  5. ApplicationStartedEvent – Context 刷新完成,Runner 执行前
  6. ApplicationReadyEvent – Runner 执行完毕,应用已就绪
  7. ApplicationFailedEvent – 启动过程中出现异常

理解这些事件顺序,就能在合适的时机插入自定义逻辑。例如,你可以在 ApplicationEnvironmentPreparedEvent 中添加自定义的 PropertySource,或者在 ApplicationReadyEvent 中记录“服务已启动”的标记。

26.4.5 总结:启动流程全景图

简而言之一句话:SpringApplication 通过事件驱动的方式,串联起 Environment 准备、Context 创建、自动配置加载、Web 服务器启动、Runner 回调等步骤。其中 refreshContext 是 Spring Framework 的经典核心,而 Spring Boot 在它的前后添加了大量便于扩展的钩子(Listener、Initializer、Runner),使得框架在保持强大能力的同时,又极为灵活。

下一次当你面对一个“启动半天没反应”的问题时,不妨顺着这个流程去排查:是 SpringFactories 加载卡住?是某个自动配置类耗时过长?还是 Web 服务器端口被占用?掌握了启动源码的脉络,你就不再是一个只会使用注解的开发者,而是可以真正驾驭框架的人。