
SpringBoot启动过程在大多数开发者眼里就是main方法里那一行SpringApplication.run(Application.class, args)跑起来就完事了。可真到排查启动异常、或者想搞明白自动装配为什么在某个环境里没生效的时候你就必须把SpringApplication的run方法从头到尾读一遍。这篇文章我打算把run方法的执行链路拆开讲清楚new SpringApplication阶段做了什么、run内部几个关键环节的顺序、事件监听怎么穿插其中、refreshContext为什么是启动成败的分水岭。适合对Spring Boot有一定使用经验、想深入理解启动原理的开发者也适合正在被启动问题折磨、准备系统性排查的运维同学。先说一个很多人忽略的前提SpringApplication.run其实不是一个方法而是两步——先new SpringApplication再调用它的run方法。构造函数里做的事情直接决定了run方法后面能不能正确启动。所以源码阅读不能从run开头得从构造函数开头。1. new SpringApplication做了什么run之前的那半截真相1.1 WebApplicationType一上来就猜你要跑哪种应用你在main方法里写的SpringApplication.run第一步走的是构造函数。构造函数里有一行关键代码this.webApplicationType WebApplicationType.deduceFromClasspath();WebApplicationType这个枚举有三个值SERVLET、REACTIVE、NONE。Spring Boot会通过ClassLoader判断当前classpath里到底有没有Servlet相关类、有没有WebFlux相关类然后自动决定你要跑的是哪种应用。判断逻辑大致是这样的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; }这个推断不是随随便便做的。后续createApplicationContext要创建哪种ApplicationContext完全取决于这个类型——普通应用创建AnnotationConfigApplicationContextServlet应用创建AnnotationConfigServletWebServerApplicationContext响应式应用创建AnnotationConfigReactiveWebServerApplicationContext。我实际排查过一个问题一个老项目里引入了某款敏感词过滤组件那个组件为了兼容不同场景间接把一整套WebFlux依赖带进了classpath结果原本正常的Servlet应用启动时报错说找不到Servlet容器。看堆栈怎么也看不明白最后发现就是webApplicationType被推断成了REACTIVESpring Boot按响应式应用去创建了容器。所以遇到启动方式明显不对的情况先查你有没有引入多余的WebFlux依赖。1.2 初始化器和监听器从spring.factories里批量收货构造函数紧接着做的两件事是从classpath里批量加载ApplicationContextInitializer和ApplicationListenersetInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));getSpringFactoriesInstances底层用的是SpringFactoriesLoader它会扫描所有jar包里的META-INF/spring.factoriesSpring Boot 2.7之后也支持META-INF/spring/...下的imports文件把你声明的实现类全部实例化出来。这套机制的价值在于扩展启动过程不需要改业务代码。某一个中间件依赖只要放进classpath它自己就能通过spring.factories注册自己的初始化和监听逻辑。这也是为什么很多第三方组件加个依赖、写几行配置就能生效的原因之一。很多开发者以为这是Spring容器注入的其实容器都还没创建呢这一步发生在创建容器之前。1.3 mainApplicationClass用异常栈反推入口类构造函数最后还藏着一个有意思的操作确定mainApplicationClass。它的实现思路非常朴素——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) { // 忽略 } return null; }直接new一个RuntimeException取它的栈信息遍历找方法名是main的那个类就认为是启动类。这个技巧虽然看起来有点野路子但在很多场景下很实用。如果你在自己的业务代码里想动态定位入口类完全可以复刻这个思路。需要注意的是Java 9之后获取栈信息的方式多了StackWalker但Spring Boot为了兼容老版本保留了这种老写法你在阅读源码时看到它不必惊讶。2. run方法主线的执行顺序计时、环境、容器三件大事2.1 启动计时、监听器集合与BootstrapContext构造函数处理完之后进入run方法。我梳理的是Spring Boot 3.x的实现2.x整体结构类似但细节略有差异。run方法开头做了三件事创建DefaultBootstrapContext、获取SpringApplicationRunListeners、启动计时。DefaultBootstrapContext bootstrapContext createBootstrapContext(); SpringApplicationRunListeners listeners getRunListeners(args); listeners.starting(bootstrapContext, mainApplicationClass);DefaultBootstrapContext是Spring Boot 3.x新增的引导上下文主要服务于BootstrapRegistryInitializer大部分业务代码不会直接接触。真正值得关注的是getRunListeners——它加载的是SpringApplicationRunListener接口的实现类默认实现是EventPublishingRunListener它的作用是把Spring Boot启动过程的生命周期节点转换成Spring容器事件再广播出去。这一步同时开始计时。Spring Boot的启动日志里那句Started Application in X seconds就是从stopWatch里读出来的。2.2 prepareEnvironment配置从哪来合并顺序怎么定run方法里第二个关键环节是准备ConfigurableEnvironment。Environment在Spring里就是配置属性的集合它管理着一堆PropertySource每个PropertySource可以理解为一份配置来源。在web环境下Spring Boot会先创建一个StandardServletEnvironment里面默认包含ServletContext参数、JNDI属性、Java系统属性、操作系统环境变量、random占位配置源。随后会触发ApplicationEnvironmentPreparedEvent事件由ConfigDataEnvironmentPostProcessor等监听器在这个阶段加载外部的application.yml、application.properties、Profile配置等。整体配置来源的合并优先级简化成一张日常够用的表就是这样优先级配置来源最高命令行参数--keyvalue往下Java系统属性System.getProperties()往下操作系统环境变量往下jar包外的application-{profile}.yml/application-{profile}.properties往下jar包内的application-{profile}.yml/application-{profile}.properties往下jar包外的application.yml/application.properties更低jar包内的application.yml/application.properties最低SpringApplication.setDefaultProperties()设置的默认值这个顺序在生产环境排查配置问题时太重要了。我见过不止一次我在application.yml里配了端口和数据源为什么还是用的默认值——十有八九是操作系统环境变量里有一个同名变量或者启动命令里带了一个覆盖参数。另外prepareEnvironment还会把spring.main.*等配置绑定回SpringApplication对象本身所以你可以通过配置文件控制spring.main.web-application-typenone这类属性。2.3 创建并准备ApplicationContext类型推断的最终兑现环境准备好之后run方法打印Banner然后调用createApplicationContext。这里就是之前WebApplicationType推断结果的兑现WebApplicationType.SERVLET创建AnnotationConfigServletWebServerApplicationContextWebApplicationType.REACTIVE创建AnnotationConfigReactiveWebServerApplicationContextWebApplicationType.NONE创建AnnotationConfigApplicationContext。容器创建完不代表就能用接下来最繁重的一步是prepareContext。它要做的事情包括给上下文设置Environment、执行所有ApplicationContextInitializer的initialize方法、把ApplicationArguments和Banner注册成单例Bean、最后把primarySources也就是你的启动类通过AnnotatedBeanDefinitionReader注册成Bean定义。这一串动作做完Spring容器仍然只处于原料已备齐的状态真正的化学反应要到刷新阶段才会发生。3. 事件驱动启动为什么启动过程里藏着那么多钩子3.1 两套监听体系别把SpringApplicationRunListener和ApplicationListener混了读run方法时最容易混的一点是SpringApplicationRunListener和ApplicationListener的区别。SpringApplicationRunListener是Spring Boot启动过程中专用的监听器接口它只有少数几个生命周期方法starting、environmentPrepared、contextPrepared、contextLoaded、started、ready、failed。这个接口的作用是感知启动进度而不是处理Spring容器内部事件。默认实现EventPublishingRunListener做的事情是把这些生命周期节点翻译成Spring事件。比如starting对应ApplicationStartingEventstarted对应ApplicationStartedEventready对应ApplicationReadyEvent。也就是说你在业务代码里写一个ApplicationListenerApplicationReadyEvent最终就是靠这层桥接在启动链路里被触发的。3.2 启动过程事件时间线什么时机适合干什么理解了桥接关系之后另一件要搞清楚的事情是这几个事件分别对应容器的什么状态适合做什么操作。我整理了一个实战经常用到的时间线生命周期对应事件容器状态适合干什么startingApplicationStartingEvent容器还没创建设置全局标记、清理临时文件environmentPreparedApplicationEnvironmentPreparedEvent容器未创建Environment就绪动态修改配置源、加载外部配置contextPreparedApplicationContextInitializedEvent容器已创建但未刷新注册特殊的BeanFactoryPostProcessorcontextLoadedApplicationPreparedEventBean定义已加载未刷新检查Bean定义、补充注册Configuration类startedApplicationStartedEvent容器已刷新单例Bean就绪初始化缓存、预热线程池readyApplicationReadyEvent应用完全可对外服务启动消息监听、建连、健康检查上报这个表格背后的逻辑是事件越靠后容器可用的东西越多但可用的程度也不同。比如ApplicationStartedEvent发生时容器已经refresh过普通单例Bean能拿到了但是内嵌WebServer的对外流量可能还没完全准备好ApplicationReadyEvent意味着应用真正进入可服务状态。3.3 自定义监听器的实战姿势与顺序控制业务里最常见的做法是注册一个ApplicationListenerApplicationReadyEvent在应用起来后做初始化。一个典型写法Bean public ApplicationListenerApplicationReadyEvent readyListener() { return event - { // 做缓存预热或者建连 }; }需要注意两点。第一如果你在ApplicationStartingEvent阶段通过ApplicationListener去做事情此时ApplicationContext还不存在不能依赖任何Spring Bean也不能用Autowired。第二如果同类型监听器有多个可以用Order控制执行顺序Order(1)会先于Order(2)执行。这个顺序在初始化多组数据的时候很容易踩坑比如先初始化基础数据再初始化业务缓存顺序反了会导致缓存里缺数据。另外ApplicationRunner和CommandLineRunner的触发时机也在这里备受误解它们不是刷新时触发而是在started事件之后、ready事件之前由callRunners统一调用。后面单独讲。4. refreshContext整个启动过程最重的那一下4.1 refresh方法里自动装配是在哪一步被激活的run方法里调用refreshContext(context)其实就是执行AbstractApplicationContext中的refresh()方法。这是整个Spring容器的核心逻辑也是Spring Boot启动过程里最重的一下。refresh方法内部的步骤很多但跟Spring Boot自动装配关系最密切的是invokeBeanFactoryPostProcessors这一步。标准执行顺序大致是prepareRefresh、obtainFreshBeanFactory、prepareBeanFactory、postProcessBeanFactory、invokeBeanFactoryPostProcessors、registerBeanPostProcessors、initMessageSource、initApplicationEventMulticaster、onRefresh、registerListeners、finishBeanFactoryInitialization、finishRefresh。invokeBeanFactoryPostProcessors阶段会处理ConfigurationClassPostProcessor它是Spring处理Configuration类、扫描包、导入配置类的核心。Spring Boot的SpringBootApplication注解里整合的EnableAutoConfiguration就是靠AutoConfigurationImportSelector在这里被解析的它把所有自动配置类从AutoConfiguration.imports里读出来再按Conditional等条件过滤最终决定哪些自动配置真正生效。所以排查自动配置为什么不生效的时候脑子里要有这个概念自动配置的筛选不是发生在run方法外层而是发生在容器刷新的invokeBeanFactoryPostProcessors阶段。到了这步之后你再加Configuration类就已经晚了。4.2 内嵌Web容器启动与Spring Boot的关系refresh过程中还有一步容易被忽略但实际影响很大onRefresh。在ServletWebServerApplicationContext里onRefresh阶段会调用createWebServer内嵌的Tomcat/Jetty/Undertow就是在这里创建并启动的监听端口也是在这里完成的。所以启动日志里如果出现端口被占用这类错误报错阶段通常就在refresh的onRefresh附近。此时Spring容器已经建立但还没完全初始化完成很多Bean还没有实例化。这也解释了为什么有时候端口被占用会导致启动失败但看不到很多业务Bean的初始化日志——因为业务单例Bean的实例化在后面的finishBeanFactoryInitialization阶段内嵌容器启动比它更早。4.3 刷新完成后afterRefresh与Runner们refreshContext返回后run方法还有两个动作afterRefresh和callRunners。afterRefresh是一个模板方法默认实现为空留给子类扩展Spring Boot自身没做太多事。callRunners则是调用所有实现了ApplicationRunner和CommandLineRunner接口的Bean。这里有个容易搞混的顺序listeners.started()先触发ApplicationStartedEvent然后callRunners执行Runner最后listeners.ready()触发ApplicationReadyEvent。如果你用Runner做初始化又用ApplicationStartedEvent做初始化需要特别小心先后关系。我的一般建议是优先用ApplicationRunner做需要拿到ApplicationArguments参数的任务用ApplicationReadyEvent做对外建连类的任务避免Runner里长时间阻塞导致ready事件延迟触发。5. 失败处理与排查run方法是怎么兜底的5.1 handleRunFailure异常报告器与退出码run方法的整个主流程包在一个大try/catch里任何异常都会被handleRunFailure处理。这个方法做的事情包括打印错误报告、触发ApplicationFailedEvent、注册退出码。Spring Boot启动失败时屏幕上那段经典的APPLICATION FAILED TO START报告就是由SpringBootExceptionReporter实现类生成并打印的。在实际排查中这个错误报告比堆栈本身信息量更大。它会明确提示你Description和Action比如数据源配置失败时Action会建议你检查数据库连接、检查自动配置是否按预期排除。所以遇到启动失败不要只盯着堆栈前三行往下翻翻那段Description / Action往往更直接。5.2 常见启动失败与排查定位思路结合run方法各阶段的特征我把平时最常见的启动失败按所处阶段做了一个快速定位表报错特征大概率所处阶段排查方向WebServerException / 端口被占用refresh的onRefresh换端口、查占用进程、检查server.port配置来源BeanCurrentlyInCreationExceptionfinishBeanFactoryInitialization存在循环依赖重新审视Bean设计Failed to configure a DataSourceinvokeBeanFactoryPostProcessors阶段自动配置查看数据源自动配置是否被触发、连接配置是否正确BeanDefinitionStoreExceptionprepareContext加载Bean定义注解写错、配置类重复、扫描路径异常自动配置与预期不符invokeBeanFactoryPostProcessors用--debug启动查看自动配置匹配报告特别提一下--debug。启动Spring Boot应用时加上--debug参数或者在配置文件里设debugtrue控制台会输出一份详细的自动配置报告里面把每个自动配置类的Positive匹配和Negative匹配原因全列出来。查为什么某某配置没生效时这份报告比任何教程都可靠。另一个经验是如果启动过程卡住不报错但日志停在了某个阶段不再往下走优先看当前停在什么事件之后。比如日志已经出现Started Application但没有到Ready多半是某个ApplicationRunner或者ApplicationReadyEvent监听器在启动后阻塞了。这个时候线程dump会非常有用能直接看到阻塞在哪个业务代码里。把run方法完整读一遍之后我最深的体会是Spring Boot的启动不是一道流程而是一套有序的广播。它把原本Spring里需要手工组装的东西全部变成了可以在不同时间点被观察、被拦截、被替换的生命周期。这也是为什么排查启动问题最快的办法是先定位当前走到哪个事件、哪个阶段。对我个人来说遇到诡异启动问题时第一反应不是去搜启动报错解决方案而是先想清楚run方法在哪一个环节抛出的异常想明白之后这个问题通常就已经解了一半。