
1. 引言为什么面试官总爱问 SpringBoot 启动流程在 Java 后端面试中SpringBoot 几乎是绕不开的话题。很多候选人能熟练地写出一个 SpringBootApplication 的启动类也能说出“约定优于配置”“自动装配”这些关键词但当面试官追问一句“这个 main 方法执行之后SpringBoot 内部到底发生了什么”时不少人就会卡壳。启动流程之所以成为面试必问题原因有三点考察源码能力启动流程是 SpringBoot 最核心的主干逻辑读懂了它基本就读懂了 SpringBoot 的骨架。考察框架理解深度自动配置、事件监听、容器刷新、内嵌容器启动都集中在启动流程里一个知识点能串起整条知识链。考察排查问题的思路线上常见的问题比如启动失败、Bean 冲突、配置不生效、端口占用等最终都要回到启动流程中定位。本文将以 SpringBoot 2.x 的源码为基准以一条 main 方法为起点把启动流程从头到尾完整拆解一遍。全文约 2 万字既有源码级细节也有高频面试题的逐题解析建议收藏后对照源码反复阅读。2. 全局视角一次启动到底经历了什么在深入源码之前先从宏观上建立一张“启动地图”。一个最普通的 SpringBoot 应用入口通常长这样javaSpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }看似只有一行代码但它触发的内部动作非常多。我们把整个过程分层来看准备阶段构造 SpringApplication 对象推断应用类型、加载初始化器和监听器、定位主类。环境准备阶段创建并准备 Environment加载配置属性、激活 Profile。容器阶段创建 ApplicationContext注册主配置类执行刷新。自动配置阶段加载 spring.factories 中的自动配置类按条件装配 Bean。Web 容器阶段创建内嵌 Tomcat暴露端口启动 Servlet 容器。收尾阶段回调 Runner发布就绪事件完成启动。如果用一张序列图来描述核心调用链大致如下这张图的每一个节点后面的章节都会逐一展开。我们先把注意力放到第一个关键类SpringApplication。3. 入口源码SpringApplication 构造过程详解SpringApplication.run(...) 是个静态方法它内部会先 new SpringApplication(primarySources).run(args)。也就是说真正的逻辑分两步构造对象和执行 run 方法。先看构造器。3.1 推断应用类型 deduceFromClasspath构造器第一步是通过 WebApplicationType.deduceFromClasspath() 判断当前是哪种 Web 应用类型javastatic 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; }判断逻辑可以总结为如果 classpath 同时存在 DispatcherServlet 和 ServletContainer判定为 SERVLET 类型如果存在 DispatcherHandler 且不存在上述 Servlet 相关类判定为 REACTIVE 响应式类型如果两者都不存在判定为 NONE即普通非 Web 应用。这个类型非常关键它直接决定后续创建的是 AnnotationConfigServletWebServerApplicationContext 还是 AnnotationConfigReactiveWebServerApplicationContext也决定 Web 服务器的创建策略。很多初学者困惑“为什么我引入了 web 依赖后它就知道要起 Tomcat”答案就在这个推断方法里。3.2 加载 ApplicationContextInitializer第二步是通过 SpringFactoriesLoader 从 META-INF/spring.factories 中加载 ApplicationContextInitializer 实现类集合javasetInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class));ApplicationContextInitializer 的作用是在 ConfigurableApplicationContext 执行 refresh() 之前对容器做一次预处理。比如修改环境、注册额外的 Bean 等。它是 Spring 留给开发者扩展容器初始化逻辑的钩子之一接口定义如下javapublic interface ApplicationContextInitializerC extends ConfigurableApplicationContext { void initialize(C applicationContext); }如果要自定义一个初始化器需要实现该接口并在 spring.factories 中声明或者通过 SpringApplication.addInitializers(...) 手动添加。注意初始化器的执行时机是容器创建之后、refresh() 开始之前。3.3 加载 ApplicationListener第三步与初始化器类似也是通过 SPI 机制加载 ApplicationListener 集合javasetListeners((Collection) getSpringFactoriesInstances( ApplicationListener.class));ApplicationListener 是 Spring 事件机制的核心接口。SpringBoot 在启动过程中会发布多个事件例如 ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent、ApplicationPreparedEvent、ApplicationReadyEvent 等。这些监听器在启动的不同阶段被回调构成了 SpringBoot 的“生命周期事件总线”。面试中常见的一个追问是“SpringBoot 的启动事件有哪些它们的执行顺序是什么”本文第 12 小节会给出完整的顺序总结。3.4 推断 main 方法所在类 deduceMainApplicationClass最后一步是找到包含 main 方法的主类。实现方式比较巧妙它不是靠注解而是通过分析当前线程的调用栈javaprivate 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; }它会向上遍历栈帧找到方法名为 main 的那一帧从而确定主类。这样做的目的之一是为了在后续打印启动日志时能正确显示应用名称同时也能在“没有显式传入主配置类”的场景下推断默认配置源。到这里SpringApplication 对象就构造完成了下面进入最核心的 run 方法。4. 核心入口SpringApplication.run 方法拆解构造完成后执行 run(args)。这个方法是整个启动流程的主干我们先把它完整列出来再逐段分析javapublic ConfigurableApplicationContext run(String... args) { StopWatch stopWatch new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context null; CollectionSpringBootExceptionReporter exceptionReporters new ArrayList(); configureHeadlessProperty(); SpringApplicationRunListeners listeners getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments new DefaultApplicationArguments(args); ConfigurableEnvironment environment prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner printBanner(environment); context createApplicationContext(); exceptionReporters getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }这段代码虽然只有几十行但每一行背后都牵连着大量细节。接下来我们严格按照执行顺序逐个展开。4.1 StopWatch 启动计时StopWatch 是 Spring 提供的简单计时器。它在 run 方法一开始 start()在所有工作完成后 stop()最后通过 StartupInfoLogger 打印出类似下面的启动信息textStarted DemoApplication in 2.536 seconds (JVM running for 3.012)这里有两个时间Started ... in 表示应用从开始启动到容器刷新完成所花的时间而 JVM running for 表示 JVM 从启动到现在运行的总时长。两者的差值基本就是 JVM 启动、类加载等前期开销。如果你希望统计某个阶段的具体耗时也可以自行创建 StopWatch 并分段 start/stop。4.2 配置 headless 模式与启动监听器configureHeadlessProperty() 负责设置 java.awt.headless 系统属性。在服务器环境下没有图形界面开启 headless 模式可以避免一些 AWT 相关组件抛出异常属于一个稳妥的默认值。紧接着是 getRunListeners(args)。它同样通过 SPI 机制加载 SpringApplicationRunListener 实现类。注意区别前面构造器里加载的是 ApplicationListener普通应用监听器而这里加载的是 SpringApplicationRunListener运行监听器二者职责不同。javaprivate SpringApplicationRunListeners getRunListeners(String[] args) { Class?[] types new Class?[] { SpringApplication.class, String[].class }; return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)); }默认实现是 EventPublishingRunListener。它的内部持有一个 SimpleApplicationEventMulticaster当运行到不同阶段时会把启动事件广播给所有已注册的 ApplicationListener。比如 listeners.starting() 被调用时就会发布 ApplicationStartingEvent。我们可以这样理解两者关系SpringApplicationRunListener 是“阶段通知器”负责在启动流程的各个关键节点发出提醒ApplicationListener 是“事件订阅者”负责接收并处理具体的启动事件。 这个区分在面试中经常考。4.3 准备 EnvironmentprepareEnvironment(...) 是启动前期的重中之重。它负责创建 Environment、加载配置属性、发布环境准备完成事件。该方法内部结构如下javaprivate ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { ConfigurableEnvironment environment getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); listeners.environmentPrepared(environment); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment new EnvironmentConverter(getClassLoader()) .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; }这里有几个值得展开的点getOrCreateEnvironment根据前面推断出的 Web 类型创建 StandardServletEnvironment 或 StandardEnvironment。前者额外扩展了 Servlet 相关的属性源。configureEnvironment把命令行参数、defaultProperties 等加入到环境中并激活指定的 Profile。listeners.environmentPrepared发布 ApplicationEnvironmentPreparedEvent。在这个阶段ConfigFileApplicationListener 会开始加载 application.properties、application.yml 等配置文件。bindToSpringApplication把环境中的 spring.main.* 前缀属性绑定回 SpringApplication 对象本身实现“用配置覆盖启动参数”的能力。关于 Environment 的更多细节放在第 5 节专门讲解。4.4 打印 Banner环境准备完成后会调用 printBanner(environment)。默认情况下如果 classpath 下存在 banner.txt会读取并打印也可以通过 spring.banner.location 指定位置或用 spring.banner.image.location 指定图片 Banner。如果什么都没有就打印默认的 Spring 字符画。如果你不想显示 Banner可以设置 spring.main.banner-modeoff或在代码里调用 setBannerMode(Banner.Mode.OFF)。这个细节虽然不影响核心流程但在面试里偶尔会被当作“对 SpringBoot 熟悉程度”的小彩蛋来问。4.5 创建 ApplicationContext接下来就是创建容器。方法内部根据 Web 类型做了分支选择javaprotected ConfigurableApplicationContext createApplicationContext() { Class? contextClass this.applicationContextClass; if (contextClass null) { switch (this.webApplicationType) { case SERVLET: contextClass Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS); break; case REACTIVE: contextClass Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS); break; default: contextClass Class.forName(DEFAULT_CONTEXT_CLASS); } } return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass); }对于最常见的 Servlet Web 应用创建的是 AnnotationConfigServletWebServerApplicationContext。它继承自 ServletWebServerApplicationContext在标准 AnnotationConfigApplicationContext 能力的基础上额外承担了内嵌 Servlet 容器的创建与生命周期管理职责这也是后续 refresh 阶段能够拉起 Tomcat 的重要前提。4.6 准备上下文 prepareContext容器创建完成后还不能立刻执行 refresh()因为很多上下文信息还没有设置。prepareContext(...) 承担的工作就是把前面准备好的环境、监听器、参数、Banner 全部装配到新容器中主要步骤包括设置环境调用 context.setEnvironment(environment)让容器能够读取前面准备好的 Environment。注册主配置类把启动时传入的 primarySources如 DemoApplication.class注册为 Bean 定义。因为主类上带有 SpringBootApplication后续组件扫描和自动配置都会围绕它展开。执行初始化器遍历构造器阶段加载的 ApplicationContextInitializer逐个调用 initialize(context)。注册特殊 Bean把 SpringApplicationRunListeners、ApplicationArguments、Banner 等注册为单例 Bean方便后续组件获取。发布上下文准备事件发布 ApplicationPreparedEvent。核心代码简化如下javaprivate void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { context.setEnvironment(environment); postProcessApplicationContext(context); applyInitializers(context); listeners.contextPrepared(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() null); logStartupProfileInfo(context); } ConfigurableListableBeanFactory beanFactory context.getBeanFactory(); beanFactory.registerSingleton(springApplicationArguments, applicationArguments); if (printedBanner ! null) { beanFactory.registerSingleton(springBootBanner, printedBanner); } SetObject sources getAllSources(); load(context, sources.toArray(new Object[0])); listeners.contextLoaded(context); }这个阶段本身并不创建业务 Bean它更像是“把启动所需的信息装进一个空容器”。真正让所有单例 Bean 完成实例化的是下一步 refreshContext。4.7 刷新上下文 refreshContextrefreshContext(context) 是整个启动流程中最核心的一步。它最终调用的是 Spring 框架的 AbstractApplicationContext#refresh()。SpringBoot 并没有重写这个方法而是通过子类扩展了其中的若干扩展点尤其是 onRefresh()。我们先用一张清单记住 refresh() 的主干步骤prepareRefresh()准备刷新校验必要属性、初始化 earlyApplicationListeners。obtainFreshBeanFactory()获取新的 BeanFactory。对于注解式 Web 容器这里主要确认 BeanFactory 已可用。prepareBeanFactory()为 BeanFactory 设置类加载器、SpEL 解析器、默认后置处理器等。postProcessBeanFactory()留给子类进一步定制 BeanFactory例如注册 Servlet 相关的特殊作用域。invokeBeanFactoryPostProcessors()执行 BeanFactoryPostProcessor自动配置导入正是发生在这里。registerBeanPostProcessors()注册 BeanPostProcessor它们会在后续 Bean 初始化前后生效。initMessageSource()初始化国际化消息源。initApplicationEventMulticaster()初始化应用事件广播器。onRefresh()初始化主题等特殊 Bean。SpringBoot 的 ServletWebServerApplicationContext 在这里创建并启动内嵌 Web 服务器。registerListeners()注册监听器并广播早期事件。finishBeanFactoryInitialization()实例化所有非懒加载单例 Bean。finishRefresh()完成刷新发布 ContextRefreshedEvent。很多候选人把 SpringBoot 启动等同于 ApplicationContext 创建但真正的 Bean 生命周期、自动配置、Web 服务器创建都集中在这 12 个步骤里。接下来我们把其中与 SpringBoot 强相关的部分单独拆开讲透。5. Environment 详解配置属性究竟从哪里来在第 4.3 节中我们看到了 prepareEnvironment 的调用但没有深入属性源细节这一节把这块补齐。Spring 的 Environment 由两部分组成PropertySources 负责存放实际配置Profiles 负责标识当前激活的环境。PropertySources 是一个有序集合越靠前的属性源优先级越高。SpringBoot 常见属性源顺序大致如下命令行参数Java 系统属性操作系统环境变量application.properties / application.yml 等配置文件默认属性例如如果你既在 application.yml 中写了 server.port8080又在启动命令中加了 --server.port9090最终生效的会是 9090因为命令行参数的优先级更高。除了默认的 StandardServletEnvironmentSpringBoot 还提供了 EnvironmentPostProcessor 等扩展机制允许开发者在环境刷新前自定义属性源。理解属性源优先级对排查“配置为什么不生效”这类问题非常关键。关于 Profilespring.profiles.active 用于指定激活的环境同一个应用可以同时激活多个 Profile。SpringBoot 会在环境准备阶段把 Profile 信息绑定到容器后续自动配置也会根据 Profile 决定哪些 Bean 生效。6. 自动配置启动流程中最精妙的一环很多面经会把“自动配置”单独拿出来讲但它其实和启动流程强绑定。自动装配的入口在主类上的 SpringBootApplication这是一个组合注解javaSpringBootConfiguration EnableAutoConfiguration ComponentScan(...) public interface SpringBootApplication { }其中真正负责自动装配的是 EnableAutoConfiguration。它通过 Import(AutoConfigurationImportSelector.class) 把自动配置类的选择逻辑导入容器。在 refresh() 的 invokeBeanFactoryPostProcessors 阶段AutoConfigurationImportSelector 会被解析并执行核心流程是从 META-INF/spring.factories 读取 EnableAutoConfiguration 对应的候选配置类列表根据 ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty 等条件注解进行过滤对保留的配置类排序、去重并返回为需要导入的类这些配置类随后被注册为 Bean 定义最终在容器刷新过程中实例化。例如引入 spring-boot-starter-web 后WebMvcAutoConfiguration、DispatcherServletAutoConfiguration 等都会进入候选列表。它们每个类内部又有条件判断只有当前环境满足条件时才真正生效。这就是“引入依赖即获得能力”的本质。需要注意从 Spring Boot 2.7 开始官方逐步推荐使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 替代传统的 spring.factoriesSpring Boot 3.x 中这一改变已经落地。面试中如果能主动提到这一点会是加分项。7. 内嵌 Web 容器Tomcat 是如何偷偷启动的回到 refresh() 的第 9 步 onRefresh()。对于 Servlet 应用真正执行它的是 ServletWebServerApplicationContextjavaprotected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException(Unable to start web server, ex); } }createWebServer() 的逻辑也很直观先从容器中获取 ServletWebServerFactory默认是 TomcatServletWebServerFactory调用 factory.getWebServer(getSelfInitializer()) 创建 WebServerTomcat 工厂内部会创建 Tomcat 实例、配置 Connector 端口、应用 Context、初始化 Servlet。概括地说SpringBoot 并没有独立发明一个 Web 服务器而是把“创建并启动 Tomcat/Jetty/Undertow”封装成了可替换的 ServletWebServerFactory。容器刷新到 onRefresh 这一步时内嵌服务器就被创建出来并在随后真正开始接收请求。这里也解释了为什么 Bean 初始化完成后应用就能对外提供服务Web 服务器创建发生在业务 Bean 实例化完成之前当所有单例 Bean 完整实例化后服务器已经准备好接收流量。如果某个关键 Bean 初始化失败异常会向上传播整个启动也会随之失败。8. 启动收尾Runner 与 Ready 事件当 refreshContext 返回后容器已经基本可用但 SpringBoot 还有一些收尾工作afterRefresh()默认是空方法留给子类在刷新后做自定义扩展。listeners.started(context)发布 ApplicationStartedEvent表示容器已刷新完成。callRunners(context, applicationArguments)有序调用 ApplicationRunner 和 CommandLineRunner。listeners.running(context)发布 ApplicationReadyEvent这是应用真正“就绪”的标志。关于 ApplicationRunner 和 CommandLineRunner它们的共同点是都在容器启动完成、所有 Bean 就绪后执行一段业务初始化逻辑。区别在于参数封装CommandLineRunner.run(String... args)拿到的是最原始的命令行参数。ApplicationRunner.run(ApplicationArguments args)拿到的是已封装好的 ApplicationArguments支持解析 --keyvalue 等格式。如果同类型 Runner 有多个还可以通过 Order 注解排序。它们适合做缓存预热、启动数据检查、外部资源探测等操作。需要特别注意的是Runner 执行发生在 ApplicationReadyEvent 发布之前因此卡在 Runner 里不会触发 ready 事件健康检查也会显示未就绪。9. SpringBoot 启动事件完整时序总结第 3.3 节提到启动事件是一个高频追问现在把整条链路串起来序号事件触发阶段1ApplicationStartingEventrun 方法开始listeners.starting()2ApplicationEnvironmentPreparedEventEnvironment 准备完成后3ApplicationPreparedEvent容器创建并完成准备后4ApplicationStartedEvent容器刷新完成后listeners.started()5ApplicationReadyEventRunner 执行完成后listeners.running()此外还有一个特殊事件值得补充ApplicationFailedEvent。如果启动过程抛出异常SpringBoot 会进入 handleRunFailure发布失败事件帮助资源清理和异常上报。从顺序可以看出越靠后的阶段系统越完整。ApplicationStartingEvent 时连 Environment 都还没准备好而 ApplicationReadyEvent 时所有 Bean、内嵌容器都已就绪。面试中常考“Started 和 Ready 有什么区别”答案就在它们的发布时机上一个在 Runner 之前一个在 Runner 之后。10. 高频面试题逐题解析10.1 说说 SpringBoot 启动流程的主线标准回答可以按“构造、准备、刷新、收尾”四步展开先通过 SpringApplication 构造器推断应用类型、加载初始化器和监听器再创建 Environment 并加载配置接着创建容器、装载主配置类、执行 refresh()在刷新过程中完成自动配置与 Bean 实例化最后调用 Runner、发布 Ready 事件完成启动。10.2 SpringBoot 如何判断是 Web MVC 还是 WebFlux它通过 WebApplicationType.deduceFromClasspath() 检查 classpath 是否包含特定类。存在 Servlet 相关类时判定为 SERVLET存在 DispatcherHandler 且没有 Servlet 类时判定为 REACTIVE都没有则为 NONE。10.3 ApplicationContextInitializer 和 ApplicationListener 有什么区别前者是在容器 refresh() 之前对上下文进行预处理后者是订阅并处理 Spring 事件。一个偏“配置容器”一个偏“响应事件”执行时机和职责都不相同。10.4 自动配置的原理是什么通过 EnableAutoConfiguration 导入 AutoConfigurationImportSelector读取 spring.factories 或新的 imports 文件中的候选配置类并根据条件注解过滤后再注册为 Bean。10.5 内嵌 Tomcat 是什么时候启动的在 AbstractApplicationContext#refresh() 的 onRefresh() 扩展点中启动。由 ServletWebServerApplicationContext 调用 createWebServer()进而创建并启动内嵌 Web 服务器。10.6 CommandLineRunner 和 ApplicationRunner 有什么区别执行时机都在容器就绪、Ready 事件之前区别是参数封装不同前者是原始 String[]后者是封装后的 ApplicationArguments。通过 Order 可以控制多个 Runner 的执行顺序。11. 常见启动失败排查启动失败是线上最常见的问题之一。结合启动流程可以把常见问题归到具体阶段现象可能原因定位阶段端口被占用内嵌 Tomcat 启动失败onRefreshBean 冲突多个同类型 Bean 未指定优先级finishBeanFactoryInitialization配置不生效属性源优先级、拼写错误prepareEnvironment循环依赖构造器注入形成闭环finishBeanFactoryInitialization自动配置不生效条件注解不满足invokeBeanFactoryPostProcessors类加载失败缺少依赖或版本冲突类加载阶段排查思路先看异常堆栈最底部的 Caused by再看失败阶段的日志SpringBoot 通常会打印条件评估报告最后用 --debug 参数启动查看自动配置报告。12. 总结整篇文章到现在已经把一条 main 方法从执行到应用就绪的全过程串联起来。如果只能记一条主线请记住这句话SpringBoot 启动 构造 SpringApplication → 准备 Environment → 创建 ApplicationContext → prepareContext → refreshContext → afterRefresh → Runner → Ready。在这条主线上需要重点掌握以下节点SpringApplication 构造推断应用类型、加载 Initializer 和 Listener、定位主类。Environment 准备加载配置文件、解析命令行参数、激活 Profile。ApplicationContext 创建根据 Web 类型创建对应的容器。prepareContext设置环境、注册主配置类、执行 Initializer。refresh12 步刷新最核心的是 invokeBeanFactoryPostProcessors自动配置和 onRefresh内嵌容器。Runner 与 Ready执行 ApplicationRunner / CommandLineRunner最后发布 Ready 事件。启动事件Starting、EnvironmentPrepared、Prepared、Started、Ready逐一对应启动的关键阶段。