1. 项目概述:一次对Spring Boot启动过程的深度“解剖”

如果你是一位Java后端开发者,或者正在向这个方向努力,那么“Spring Boot启动流程”这个主题你一定不陌生。面试时它是高频考点,工作中它是理解框架、排查问题的基石。但很多时候,我们可能只是记住了“SpringApplication.run()”这个入口,对于其内部究竟如何一步步将我们的应用从几行代码变成一个可对外服务的进程,却只有一个模糊的印象。

今天,我们不谈空泛的概念,就以一个资深开发者的视角,深入 SpringApplication.run() 方法的每一行“肌理”,进行一次彻底的流程“解剖”。这不仅仅是为了应付面试,更是为了让你在遇到诸如“Bean为什么没加载?”、“配置文件为什么不生效?”、“自定义的监听器为什么没执行?”这类问题时,能像福尔摩斯一样,沿着启动链的线索,快速定位到问题的根源。理解这个过程,是你从“会用Spring Boot”到“懂Spring Boot”的关键一步。

2. 核心流程总览与设计思想拆解

在深入代码之前,我们先从宏观上把握 SpringApplication.run() 的骨架。它的核心使命是 引导和启动一个Spring应用上下文(ApplicationContext) ,并最终运行一个嵌入式的Web服务器(如Tomcat)。这个过程不是一蹴而就的,而是被精心设计成一系列可扩展、可观测的阶段。

2.1 为什么是“事件驱动”的启动模型?

Spring Boot启动流程最精妙的设计之一,就是其 事件驱动机制 。整个启动过程被抽象为多个 ApplicationEvent (应用事件),例如 ApplicationStartingEvent ApplicationEnvironmentPreparedEvent ApplicationPreparedEvent ApplicationStartedEvent ApplicationReadyEvent 等。

这种设计的好处显而易见:

  1. 高扩展性 :我们可以在应用的任何生命周期阶段插入自定义逻辑,只需监听对应的事件即可。比如,在环境准备完成后( ApplicationEnvironmentPreparedEvent )动态修改配置,或在应用完全就绪后( ApplicationReadyEvent )执行一些数据初始化工作。
  2. 解耦 :核心启动逻辑与各种初始化操作(如日志系统初始化、监听器触发、报告器生成)通过事件解耦,使得代码结构清晰,各司其职。
  3. 可观测性 :事件就像启动流程中的一个个“路标”,通过监听这些事件,我们可以非常清晰地了解应用当前启动到了哪个阶段,这对于调试和监控至关重要。

SpringApplication 类内部维护了一个 SpringApplicationRunListeners 对象,它负责在整个启动流程的关键节点广播这些事件。我们后续的流程解析,也会紧紧围绕这些事件的触发时机来展开。

2.2 启动流程的四大核心阶段

虽然细节繁多,但整个 run 方法的主体流程可以归纳为四个核心阶段:

  1. 准备阶段(Preparation) :初始化监听器、准备应用环境、打印Banner等。这是启动的“热身”环节。
  2. 上下文创建阶段(Context Creation) :核心步骤,创建并配置 ApplicationContext 。这是Spring IoC容器的“诞生”时刻。
  3. 上下文刷新阶段(Context Refresh) :最复杂、最核心的一步,调用 AbstractApplicationContext.refresh() 方法。Bean的定义加载、实例化、依赖注入、后置处理器调用、内嵌Web服务器启动等,都发生在这里。这是容器的“启动”与“装配”环节。
  4. 收尾与运行阶段(After Refresh & Run) :启动后回调、监听器通知、命令行Runner执行等。标志着应用已“就绪”,可以开始处理请求。

接下来,我们就沿着这条主线,深入到每一个阶段的细节中去。

3. 启动流程的逐帧解析与核心源码剖析

让我们打开 SpringApplication 的源码,从 run(String... args) 这个静态方法开始。

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

可以看到,静态 run 方法最终创建了一个 SpringApplication 实例,并调用其实例方法 run(args) 。所以,我们的分析重点在于实例的构造和实例 run 方法。

3.1 SpringApplication实例的构造:启动的“蓝图”

new SpringApplication(primarySources) 时,框架做了一系列重要的初始化工作,为后续启动绘制“蓝图”:

  1. 推断应用类型 :根据类路径下存在的类,判断是普通的非Web应用( NONE )、Servlet Web应用( SERVLET )还是响应式Web应用( REACTIVE )。这决定了后续创建哪种类型的 ApplicationContext (例如 AnnotationConfigServletWebServerApplicationContext )。
  2. 加载“应用上下文初始化器” :从 spring.factories 配置文件和自定义来源中,加载 ApplicationContextInitializer 。这些初始化器会在 ApplicationContext refresh 之前执行,用于对上下文进行一些编程式的配置。
  3. 加载“应用监听器” :同样从 spring.factories 和自定义来源中,加载 ApplicationListener 。这些监听器就是我们前面提到的事件驱动模型中的“听众”,它们将响应启动过程中广播的各种事件。
  4. 推断主配置类 :根据传入的 primarySources (通常就是标注了 @SpringBootApplication 的主类),确定应用的主配置类。

实操心得 :理解这个构造阶段,有助于我们定位一些“早期”的配置问题。例如,如果你自定义的 ApplicationContextInitializer 没有生效,首先应该检查它是否被正确配置在 META-INF/spring.factories 文件中,或者是否通过 SpringApplication.addInitializers() 方法添加。

3.2 run(args)方法:启动引擎点火

实例 run 方法是真正的启动引擎。其核心代码结构清晰,我们结合事件来解读:

public ConfigurableApplicationContext run(String... args) {
    // 1. 创建并启动计时器
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    // 2. 初始化引导上下文(BootstrapContext),为外部配置(如Spring Cloud)提供早期支持
    ConfigurableApplicationContext context = null;
    ConfigurableBootstrapContext bootstrapContext = null;
    SpringApplicationRunListeners listeners = getRunListeners(args);

    // 3. 发布 ApplicationStartingEvent
    listeners.starting(bootstrapContext, this.mainApplicationClass);

    try {
        // 4. 准备应用参数
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

        // 5. 准备环境,并发布 ApplicationEnvironmentPreparedEvent
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        configureIgnoreBeanInfo(environment);

        // 6. 打印Banner
        Banner printedBanner = printBanner(environment);

        // 7. 创建应用上下文
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);

        // 8. 准备上下文(应用初始化器在此执行),并发布 ApplicationContextInitializedEvent 和 ApplicationPreparedEvent
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

        // 9. 刷新上下文(核心中的核心!)
        refreshContext(context);

        // 10. 刷新后置处理(空方法,主要用于扩展)
        afterRefresh(context, applicationArguments);

        // 11. 停止计时器
        stopWatch.stop();

        // 12. 发布 ApplicationStartedEvent (上下文已刷新) 和 ApplicationReadyEvent (应用已就绪)
        listeners.started(context);
        listeners.ready(context, applicationArguments);

    } catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }

    return context;
}

下面,我们拆解其中几个最关键的环节。

3.2.1 环境准备: prepareEnvironment

这是启动流程中第一个“重量级”操作。它的主要职责是创建和配置应用的运行环境 ConfigurableEnvironment

  1. 创建环境对象 :根据之前推断的应用类型(Servlet/Reactive/None),创建对应的 StandardServletEnvironment StandardEnvironment 等。
  2. 配置PropertySources :将命令行参数、JVM系统属性、操作系统环境变量、以及应用配置文件(如 application.properties , application.yml )等,按优先级顺序加载到环境的 PropertySources 列表中。 高优先级的源会覆盖低优先级的同名属性
  3. 配置Profiles :激活在 spring.profiles.active 中指定的配置文件(Profiles)。
  4. 发布事件 :在环境对象创建并初步配置完成后,广播 ApplicationEnvironmentPreparedEvent 。这是一个非常重要的扩展点,像 ConfigFileApplicationListener (负责加载 application.properties )就是监听这个事件来工作的。

注意事项 :很多配置加载不生效的问题,都源于对这个阶段优先级的不理解。记住顺序:命令行参数 > JVM系统属性 > 环境变量 > 配置文件。同时,确保你的自定义 EnvironmentPostProcessor (如果存在)监听了正确的事件并正确设置了执行顺序。

3.2.2 上下文创建: createApplicationContext

根据应用类型,通过反射实例化对应的 ApplicationContext 。对于最常见的Servlet Web应用,创建的是 AnnotationConfigServletWebServerApplicationContext 。这个上下文对象内部包含了一个 BeanFactory (真正的Bean容器)和一个 WebServer (如Tomcat)的启动控制逻辑。

3.2.3 上下文准备: prepareContext

在调用 refresh() 之前,对创建好的 ApplicationContext 进行“战前准备”:

  1. 设置环境 :将准备好的 Environment 对象关联到上下文中。
  2. 执行初始化器 :按顺序调用所有 ApplicationContextInitializer initialize 方法。这是进行早期编程式配置的最后机会。
  3. 发布事件 :广播 ApplicationContextInitializedEvent
  4. 注册Bean定义
    • 将主配置类( primarySources )注册为Bean定义。
    • 调用 load 方法,可能会注册更多的源(例如,在Spring Boot 1.x风格中)。
  5. 发布事件 :广播 ApplicationPreparedEvent 。此时,Bean定义已加载,但Bean还未实例化。一些监听器会利用这个时机进行最后的操作。
3.2.4 上下文刷新: refreshContext -> refresh()

这是整个Spring Boot(乃至Spring Framework)启动过程中最复杂、最核心的方法,没有之一。它调用了 AbstractApplicationContext.refresh() ,我们通常称之为“IoC容器的刷新”。这个方法定义了Spring容器启动的标准流程,主要包括以下关键步骤(以下序号对应 refresh() 方法内的调用):

  1. prepareRefresh() :刷新前的准备,设置启动时间、激活状态,初始化属性源(空方法,子类可扩展)。
  2. obtainFreshBeanFactory() :获取或刷新内部的 BeanFactory 。对于 GenericApplicationContext 及其子类(Spring Boot常用),这一步只是返回已存在的 BeanFactory
  3. prepareBeanFactory(beanFactory) :对 BeanFactory 进行标准配置。例如,设置类加载器、注册几个重要的 BeanPostProcessor (如 ApplicationContextAwareProcessor ,用于处理Aware回调)、注册一些环境相关的单例Bean等。
  4. postProcessBeanFactory(beanFactory) :空方法,子类可以在此处对 BeanFactory 进行后置处理。在Servlet Web应用中,这里会注册一些与Servlet相关的Scope。
  5. invokeBeanFactoryPostProcessors(beanFactory) 极其关键的一步 。调用所有 BeanFactoryPostProcessor 。这包括:
    • ConfigurationClassPostProcessor :它是处理 @Configuration @ComponentScan @Bean @Import 等注解的核心处理器。 我们的主配置类、自动配置类( @EnableAutoConfiguration 引入的)都是在这一步被扫描、解析并注册为Bean定义的。 可以说,Spring Boot“约定大于配置”的魔法,大半发生在这里。
    • PropertySourcesPlaceholderConfigurer / PropertySourcesPlaceholderConfigurer :处理属性占位符 ${...}
    • 其他自定义的 BeanFactoryPostProcessor
  6. registerBeanPostProcessors(beanFactory) :注册所有 BeanPostProcessor BeanFactory 中。 BeanPostProcessor 负责在Bean实例化、初始化的前后进行干预。例如, AutowiredAnnotationBeanPostProcessor (处理 @Autowired )、 CommonAnnotationBeanPostProcessor (处理 @Resource , @PostConstruct )等都在这里注册。 注意,此时只是注册,还未执行。
  7. initMessageSource() :初始化国际化消息源。
  8. initApplicationEventMulticaster() :初始化应用事件广播器。
  9. onRefresh() :模板方法,子类可以在此处添加特定的刷新逻辑。 对于Spring Boot的Web应用,这是启动嵌入式Web服务器(Tomcat/Jetty/Undertow)的地方! ServletWebServerApplicationContext 会在此方法中创建并启动 WebServer
  10. registerListeners() :将早期监听器注册到事件广播器,并发布早期应用事件。
  11. finishBeanFactoryInitialization(beanFactory) 另一个核心步骤 。初始化所有剩余的单例Bean(非懒加载的)。这包括了:
    • 实例化Bean(调用构造方法)。
    • 填充属性(依赖注入)。
    • 执行Aware接口回调(如 BeanNameAware )。
    • 执行 BeanPostProcessor postProcessBeforeInitialization 方法。
    • 执行初始化方法(如 InitializingBean.afterPropertiesSet 、自定义的 init-method )。
    • 执行 BeanPostProcessor postProcessAfterInitialization 方法。
    • 至此,所有普通的业务Bean、Controller、Service、Repository等都已创建并完成注入。
  12. finishRefresh() :完成刷新。清理资源缓存,初始化生命周期处理器,发布 ContextRefreshedEvent 事件。
  13. 后续步骤(销毁已创建的Bean等)是关闭容器时的逻辑。

避坑指南 refresh() 方法异常是启动失败最常见的原因。定位问题时,可以仔细观察堆栈信息,看异常是在上述哪个步骤抛出的。例如,如果是 BeanDefinitionStoreException ,很可能是第5步扫描配置类时出了问题;如果是 BeanCreationException ,很可能是第11步实例化或注入Bean时出了问题。理解步骤划分,能帮你快速缩小排查范围。

4. 关键扩展点与自定义行为实战

理解了主流程,我们就能在正确的位置“下钩子”,实现自定义逻辑。

4.1 如何注册自定义的ApplicationListener?

你可以通过以下几种方式:

  1. META-INF/spring.factories (Spring Boot 2.7之前推荐,之后逐渐被替代):
    org.springframework.context.ApplicationListener=com.example.MyApplicationListener
    
  2. SpringApplication.addListeners(...) :在 main 方法中,构建 SpringApplication 对象后直接添加。
  3. 将监听器声明为Spring Bean :最简单直接的方式,在你的配置类中使用 @Bean 注解,或者让监听器类本身被 @Component 扫描到。Spring Boot会自动检测并注册实现了 ApplicationListener 接口的Bean。
@Component
public class MyReadyListener implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 应用已完全就绪,可以安全地调用其他Bean了
        System.out.println("Application is Ready!");
    }
}

4.2 如何利用BeanFactoryPostProcessor和BeanPostProcessor?

  • BeanFactoryPostProcessor :在Bean定义加载之后、Bean实例化之前执行。 可以修改Bean的定义信息 。常用于动态注册Bean、修改Bean的属性元数据等。实现接口并注册为Bean即可。

    @Component
    public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            BeanDefinition bd = beanFactory.getBeanDefinition("someBean");
            bd.getPropertyValues().add("propertyName", "newValue");
        }
    }
    

    注意 :在此阶段,Bean还未创建,因此不能进行Bean实例的操作。

  • BeanPostProcessor :在Bean实例化、初始化的前后执行。 可以修改或包装Bean的实例 。常用于AOP代理、自定义注解处理、属性校验等。同样实现接口并注册为Bean。

    @Component
    public class MyBeanPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            // 在初始化方法调用前执行
            return bean;
        }
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            // 在初始化方法调用后执行
            return bean;
        }
    }
    

4.3 命令行Runner与应用Runner

这两个接口( CommandLineRunner ApplicationRunner )提供了一种在应用完全启动后、开始接受流量前,执行特定代码的简便方式。它们的执行时机在 ApplicationReadyEvent 之后。区别在于接收参数的方式不同( CommandLineRunner 接收原始字符串数组, ApplicationRunner 接收封装好的 ApplicationArguments 对象)。

@Component
@Order(1) // 可以用@Order指定多个Runner的执行顺序
public class MyCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        // 执行启动后任务,如数据清理、缓存预热等
    }
}

5. 典型启动问题排查思路与实战技巧

掌握了流程,排查问题就有了地图。以下是一些常见场景的排查思路:

问题1:应用启动时报 BeanCurrentlyInCreationException (循环依赖)

  • 排查思路 :此异常通常发生在 finishBeanFactoryInitialization 阶段。Spring默认支持单例Bean的Setter注入和字段注入的循环依赖,但构造器注入的循环依赖无法解决。
  • 实战技巧
    1. 检查报错信息中提到的Bean名称,定位到你的业务代码。
    2. 分析这些Bean之间的依赖关系,尤其是 构造器注入 。尝试将其中一个Bean的注入方式改为 @Autowired 字段注入或Setter注入。
    3. 使用 @Lazy 注解延迟加载其中一个依赖,打破初始化时的循环。
    4. 重新设计代码结构,从根本上消除循环依赖,这通常是最佳实践。

问题2: @Value 注解注入的配置属性为 null 或默认值

  • 排查思路 :属性注入发生在Bean生命周期的属性填充阶段,依赖 Environment 中的属性值。
  • 实战技巧
    1. 确认配置文件( application.yml/properties )位置正确且已被加载。检查 ApplicationEnvironmentPreparedEvent 阶段是否有错误。
    2. 检查属性键名是否正确,注意大小写和连字符( - )与下划线( _ )的映射关系。
    3. 确认包含 @Value 的Bean是否被Spring管理(即是否是一个Spring Bean)。
    4. 如果使用 @PropertySource 引入自定义配置文件,确保文件路径正确,且在 BeanFactoryPostProcessor 阶段之前已被加载。

问题3:自定义的自动配置类没有生效

  • 排查思路 :自动配置类的加载依赖于 @EnableAutoConfiguration spring.factories (或Spring Boot 2.7+的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports )文件。
  • 实战技巧
    1. 确认你的自动配置类是否在正确的 spring.factories .imports 文件中列出,键名为 org.springframework.boot.autoconfigure.EnableAutoConfiguration
    2. 检查自动配置类上的条件注解(如 @ConditionalOnClass , @ConditionalOnMissingBean )是否满足。可以通过在 application.properties 中添加 debug=true 来查看自动配置报告,它会列出所有匹配和不匹配的自动配置类及其原因。
    3. 确保你的starter模块已被主项目依赖。

问题4:应用启动特别慢

  • 排查思路 :启动慢可能发生在多个阶段,需要定位瓶颈。
  • 实战技巧
    1. 在启动命令中添加JVM参数 -Dspring.application.admin.enabled=true ,或使用 SpringApplication.setApplicationStartup(ApplicationStartup) 设置一个 BufferingApplicationStartup ,可以收集启动过程中各个步骤的耗时。
    2. 常见瓶颈:
      • 类路径扫描 :检查 @ComponentScan 的范围是否过大。尽量明确指定扫描包路径。
      • Bean初始化 :检查是否有Bean在 @PostConstruct InitializingBean 中执行了耗时的同步操作(如网络调用、大量数据库查询)。考虑将其改为异步或懒加载。
      • 数据库连接池初始化 :检查数据源配置,连接池初始大小不宜过大。
      • 执行了 CommandLineRunner :检查你的Runner是否做了耗时操作。

理解 SpringApplication.run() 的流程,就像掌握了Spring Boot应用的“生命图谱”。它不仅能让你在面试中游刃有余,更能让你在复杂的项目开发和问题排查中,拥有清晰的思路和精准定位的能力。下次当你再面对一个启动失败的Spring Boot应用时,不妨沿着我们梳理的这条主线,一步步追踪下去,你很可能自己就能找到问题的答案。

更多推荐