☰
SpringBoot启动流程全解析:从SpringApplication到自动装配
2026/10/11 4:13:03 网站建设 项目流程

1. 从main方法到SpringApplication:入口背后的初始决策

先别急着看run(),我把SpringBoot的启动流程拆开讲。天天写SpringApplication.run(Application.class, args),但这一行背后经过了哪些阶段、每个阶段都在做什么,很多人其实是朦胧的。搞清楚这套流程,不是为了装X,而是当启动变慢、起不来、或者你想在启动阶段干点私活的时候,你知道该去哪里下手。

1.1 SpringApplication构造阶段到底做了什么准备

你调用SpringApplication.run(),第一步不是跑run,而是先new一个SpringApplication实例。这一步很多人忽略了,但信息量很大。它要做四件核心的事:

  • 从classpath下推断当前应用类型,是WebFlux(响应式Web)还是Servlet Web,还是普通非Web应用。
  • 加载META-INF/spring.factories,这个文件里注册了一堆启动阶段要用的工厂类,比如ApplicationContextInitializer、ApplicationListener。
  • 找到main方法所在类,记住它是主配置源。
  • 读取应用的主配置属性,比如spring.main.*相关配置。

这里最容易被人跳过的是setSources操作。SpringApplication在构造后会保存初始化参数,但真正的主配置类是在run阶段才加入的。很多人调试的时候发现bean没有扫描到,十有八九是主启动类位置放错了,扫描范围没有覆盖到你的业务包。

另外一个隐藏细节是WebApplicationType的推断逻辑:先去查classpath里有没有org.springframework.web.reactive.DispatcherHandler,有就判断为WebFlux;如果没有再查javax.servlet.Servlet和org.springframework.web.context.ConfigurableWebApplicationContext是否存在,来判断是不是Servlet Web。不同推断结果直接影响后面容器的创建策略。

1.2 run方法入口:整体阶段划分

进入run方法后,整个启动流程可以划成六大阶段,我先把版图铺开,后面逐段拆:

  1. 计时器启动与Java AWT headless模式设置。
  2. 通过SpringApplicationRunListeners广播Starting事件。
  3. 组装ApplicationArguments(命令行参数)和配置环境。
  4. 打印Banner。
  5. 创建并刷新ApplicationContext。
  6. 发布Started事件,执行Runner回调。

六个阶段里,最肥的一段是创建和刷新容器,我们日常遇到的大部分启动问题都埋在这一段。但前面几个"小阶段"也不是走过场,比如命令行参数的处理逻辑,直接决定了你--server.port=8081能不能生效。

2. 环境准备阶段:配置从哪里来、往哪里去

环境(Environment)是SpringBoot启动的一个核心枢纽,它管着后续所有属性值的来源。你可以把它想象成一个大水表,所有配置源往这里灌水,容器里的bean再从这里面取水。

2.1 配置源加载顺序与覆盖优先级

SpringBoot的配置源不是一个,而是一串。它们按固定优先级排列,高优先级覆盖低优先级。常见的配置源从高到低大概是:

  • 命令行参数(最高)
  • Java系统属性(System.getProperties())
  • 操作系统环境变量
  • application-{profile}.yml / application.yml
  • 带@PropertySource注解的配置

这里给个我实测过的例子。一个服务在本地跑得好好的,部署到某环境之后就出现端口不对,后来查下来是系统环境变量里有一个SERVER_PORT=8080,覆盖了application.yml里的server.port=9090。如果你不理解优先级模型,这种问题排查起来就是无头苍蝇。

配置环境还有一个值得一提的作用:profiles的激活时机。环境准备好之后才去解析spring.profiles.active,然后加载对应的profile专属配置。所以你在application.yml里配置profile相关项,和你在启动参数里--spring.profiles.active=dev指定,效果一样,但有个隐藏坑:环境变量里激活的profile优先级最高,本地开发时一旦环境变量设置了,本地配置就永远不生效。

2.2 环境准备阶段的扩展点:EnvironmentPostProcessor

环境阶段有个很少被提及但极其好用的扩展点:EnvironmentPostProcessor。这是最早期的介入窗口,在环境对象构建出来之后、容器创建之前执行。它的典型用途:动态注入自定义配置源、按机房/标签修改配置项、解密配置中心下发的密文。

实现方式不复杂,实现接口然后注册到META-INF/spring.factories的org.springframework.boot.env.EnvironmentPostProcessor键下。需要注意两点:一是在这个阶段你拿不到Spring容器,只能操作Environment对象;二是手动添加的配置源加到列表末尾,默认优先级最低,要主动调addFirst才能抬高优先级。

3. 容器创建与刷新:启动流程的主战场

环境准备就绪后,SpringBoot开始创建Spring容器(ApplicationContext)。不同类型应用创建不同容器,Servlet Web应用创建的是AnnotationConfigServletWebServerApplicationContext,普通应用创建的是AnnotationConfigApplicationContext。这一步只花了很少的代码,但后续的refresh才是大头。

3.1 refresh()的12个步骤:Spring的生命周期总动员

容器创建出来后,调用的refresh()方法是整个Spring框架最核心的方法,12个步骤我挑重点说:

  • prepareRefresh():准备刷新前的状态,设置启动时间、活跃标志、初始化占位符。
  • obtainFreshBeanFactory():告诉子类刷新内部bean工厂。
  • prepareBeanFactory():配置bean工厂的标准上下文特性,比如类加载器、SpEL解析器。
  • postProcessBeanFactory():给子类扩展点,SpringBoot的Web容器整合就在这里动手脚。
  • invokeBeanFactoryPostProcessors():执行所有BeanFactoryPostProcessor,这是给你机会在bean实例化之前修改bean定义。
  • registerBeanPostProcessors():注册所有BeanPostProcessor,它们负责在bean实例化前后做拦截。
  • initMessageSource()、initApplicationEventMulticaster():初始化国际化资源和事件广播器。
  • onRefresh():这是个模板方法,SpringBoot在这里创建Web服务器。
  • registerListeners():把ApplicationListener注册到广播器。
  • finishBeanFactoryInitialization():剩下的非懒加载单例bean在这里全部实例化。
  • finishRefresh():发布ContextRefreshed事件,启动生命周期处理器。

要理解SpringBoot的启动流程,最短路径就是把refresh()这12步吃透。因为SpringBoot所做的"自动"几乎全是在这些扩展点里塞了自己的逻辑。

3.2 内置Web容器的创建:Tomcat是在什么时候诞生的

很多刚接触SpringBoot的人有个误解:Web服务器是"启动之前"就起来的。其实不是。Tomcat的创建发生在refresh的onRefresh()阶段。也就是说,容器把大部分基础设施准备好之后,才轮到内嵌服务器出场。

Tomcat的创建过程可以细化为四步:

  1. getWebServerFactory():从容器里拿ServletWebServerFactory,默认是TomcatServletWebServerFactory。
  2. 配置端口、协议、连接器参数(从Environment里读)。
  3. getTomcatWebServer():实例化Tomcat,设置工作目录、初始化连接器。
  4. 调用tomcat.start()启动服务。

这里有个实战场景值得多说一句:如果你的业务代码里有用到@PostConstruct且它触发了对外的HTTP请求,而这个请求打回自己的端口,会出现Connection refused。原因很简单:@PostConstruct是在bean初始化阶段执行的,此时Tomcat还没到onRefresh阶段启动,端口根本没开。

3.3 单例Bean实例化阶段:启动时间的大头

finishBeanFactoryInitialization()是启动流程里最耗时的一步,因为所有非懒加载单例bean都要在这里创建。bean多、依赖链深的应用,启动慢基本都是卡在这一段。

这段逻辑的核心是DefaultListableBeanFactory.preInstantiateSingletons():按注册顺序遍历bean定义,逐个实例化。遇到循环依赖(A依赖B、B依赖A)时,Spring通过三级缓存解决,而不是报错——除非你设置了allowCircularReferences=false。

从排查启动性能的角度,你可以用Java Flight Recorder(JFR)录制启动过程,然后在事件里看ClassLoad、InstanceCreation的高耗时分布。实际经验中,启动慢的元凶往往是某个初始化逻辑里做了远程调用、数据库连接池预热或者无脑扫描,而不是Spring框架本身的效率问题。

4. 自动装配的底层机制:为什么你的bean能被找到

SpringBoot最亮眼的特性是"自动装配"。很多人知道它的存在,但搞不清它和普通@ComponentScan的区别。说白了:组件扫描负责找你自己写的bean,自动装配负责加载第三方库的bean。两者协同完成了一个SpringBoot应用的装配。

4.1 组合注解的层层拆解

主启动类上的@SpringBootApplication其实是一个复合注解,它内部组合了三个核心注解:

  • @SpringBootConfiguration:本质上是一个@Configuration,标志当前类为配置类。
  • @EnableAutoConfiguration:启动自动装配功能的开关。
  • @ComponentScan:扫描主类所在包及其子包下的组件。

值得注意的是@EnableAutoConfiguration内部还有一个@AutoConfigurationPackage注解,它的作用是把主类所在的包注册成一个"自动配置包"的基础包。后面自动配置类里要扫描组件时,会拿这个基础包作为起点。这就是为什么主类包路径要被顶在项目最外层,否则服务层、控制器层全部扫不到。

4.2 自动配置类的加载与条件装配

@EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)导入了一个选择器。这个选择器在启动时会读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(老版本是spring.factories),把所有注册的自动配置类全部加载进来,这一步是"候选",不代表每个配置都会生效。

真正的"生效"靠的是条件注解,比如:

  • @ConditionalOnClass:classpath下存在某个类才生效。
  • @ConditionalOnMissingBean:容器里没有指定bean才生效。
  • @ConditionalOnProperty:配置了指定属性才生效。
  • @ConditionalOnWebApplication:当前是Web应用才生效。

这就像一份大菜单,餐厅把所有菜品都列出来(加载候选),但后厨只做客人实际的菜(条件判断)、食材有货的菜(classpath存在性判断)。一整套下来,既灵活又不会让你引入的库之间互相打架。

我自己调试自动装配问题时的做法是:在启动配置里加--debug参数,启动后控制台会打印一份"自动装配报告",哪些生效、哪些不生效、为什么不生效,写得清清楚楚。排查第三方starter没生效的问题,第一步就靠它。

5. 监听器机制与启动后的Runner:启动流程的可观测性

启动流程不是闷头跑完就完了,SpringBoot在各个阶段都抛出了事件,给外部方一个"旁观和干预"的机会。

5.1 从Starting到Ready:启动事件的完整链条

SpringBoot的事件有明确的生命周期顺序:

  • ApplicationStartingEvent:run最开始,环境未准备。
  • ApplicationEnvironmentPreparedEvent:环境已准备,容器未创建。
  • ApplicationContextInitializedEvent:容器创建完毕,刷新未开始。
  • ApplicationPreparedEvent:刷新前一刻。
  • ApplicationStartedEvent:容器刷新完成,Runner未执行。
  • ApplicationReadyEvent:Runner执行完毕,应用可以对外提供服务。
  • ApplicationFailedEvent:启动异常时触发。

监听这些事件有两种姿势。一种是实现ApplicationListener接口并注册为Spring bean,这种方式能收到容器后续发布的事件,但前期的几个事件(Starting、EnvironmentPrepared)它收不到,因为那时候bean还没创建出来。另一种是启动时通过SpringApplication.addListeners()直接传入监听器,它能全程接收。

这一块的实际价值在于灰度环境上报、监控埋点、启动健康度检查。我曾经在一个服务里监听ApplicationReadyEvent,在收到事件后主动向注册中心上报实例元数据,确保服务完全就绪后才接流量,效果比固定sleep靠谱得多。

5.2 CommandLineRunner和ApplicationRunner的时序细节

容器刷新完成后,SpringBoot会执行容器里所有Runner的回调。两个接口功能一样,区别在于入参:CommandLineRunner拿到的是原始String数组,ApplicationRunner拿到的是封装好的ApplicationArguments对象。

执行顺序有讲究:如果多个Runner要实现优先级,可以用@Order注解。数值越小越先执行。这里还藏着一个细节:SpringBoot只认实现了ApplicationRunner或CommandLineRunner接口的bean,普通的@PostConstruct方法在bean实例化阶段就跑了,时序上比Runner更早。所以如果你要在"应用完全就绪后"做初始化,必须用Runner,不能用@PostConstruct。

需要强调的执行顺序是:@PostConstruct->InitializingBean.afterPropertiesSet()->ApplicationRunner/CommandLineRunner。搞清楚这三者的时机,能避免"在错误时机做了正确的事"这类启动问题。

6. 扩展点集锦与启动问题排查思路

很多框架难题的本质是对扩展时机不理解。SpringBoot的扩展点都锚定在启动流程的某个固定阶段,你只要知道启动到哪一步了,就知道自己该用哪个扩展点。

6.1 常用扩展点速查

扩展点时机典型用途
EnvironmentPostProcessor环境创建后、容器前修改配置源、密文解密
ApplicationContextInitializer容器创建后、refresh前给容器预置自定义bean工厂配置
BeanDefinitionRegistryPostProcessorbean定义注册后动态注册/修改bean定义
BeanFactoryPostProcessorbean定义已注册修改bean定义的属性值
BeanPostProcessorbean实例化后代理包装、属性检查
ApplicationListener事件发布时状态感知、联动逻辑
Runner容器刷新后数据预热、注册上报

按时机分成两组更好记:容器启动前用EnvironmentPostProcessor和ApplicationContextInitializer;容器启动中用BeanFactoryPostProcessor这种操作bean定义的;启动完成后用Runner和事件监听器。时序搞对了,代码不会错到哪里去。

6.2 启动失败排查的四个切入点

实际开发中SpringBoot启动失败大概分四类,排查手法各不相同:

第一类是容器初始化前就挂了,典型是端口被占用。这种问题看第一行异常就够,BindException直接告诉你答案,处理完端口冲突即可。

第二类是bean装配失败,报UnsatisfiedDependencyException或BeanCreationException。拿到异常栈,从下往上看,找到最底部的Caused by,那才是根因。常见是构造函数参数缺失、bean名称冲突、循环依赖未解决。遇到这种问题不要急着猜,先跑--debug看自动装配报告,确认你的bean是不是真的被扫描到了。

第三类是启动超时或卡住。这种最难查,因为没有什么异常可看。优先怀疑远程调用和数据库连接没设超时时间。用jstack抓线程栈看主线程停在哪一行,是百试百灵的办法。

第四类是环境问题导致启动逻辑走了错误分支。看起来是业务代码bug,其实是配置没生效或者profile激活不对。建议启动时打开--debug观察日志里配置源的加载顺序,再手动验证Environment里的值是否符合预期。

6.3 启动速度优化的一些实操心法

个人经验总结三个最有效的启动提速方向:

  • 把不必要立即初始化的bean改为懒加载,通过@Lazy或spring.main.lazy-initialization=true全局开启。代价是第一次请求时会慢,可能不符合某些场景,适合功能型应用。
  • 排查并去掉启动期的远程调用。很多服务默认初始化就会去拉配置中心、调依赖服务,这些远程操作是启动慢的元凶。给它们加超时、降级,或者干脆延迟到后台任务执行。
  • 检查classpath里的组件扫描范围。如果主类在顶层包设得太大,会把无关包全部纳入扫描,白白产生一堆不必要的bean定义。合理收窄扫描包范围,启动时间经常能砍掉三分之一。

另外分享一个排查启动时间的技巧:启动时加上-Dspring.boot.log.startup-info=true,启动完成后控制台会打印每个阶段耗时,一行一行核对,定位瓶颈特别方便。

7. 我对整套流程的理解

SpringBoot的启动流程虽然复杂,但骨架非常清晰:先构造引导类,然后准备环境,创建容器,刷新容器,最后对外暴露服务和托底回调。自动装配和事件监听都是挂在这条主线上的支线。理解它以后,你会有一种"能掌控启动行为"的底气:什么时机该做什么、什么扩展点能帮我做什么、出了问题从哪一步查,心里都有数。

这个内容后续还可以这样扩展:如果你想去折腾SpringCloud,会发现服务注册发现、配置刷新、熔断降级,本质上都在复用同一套启动扩展点体系。把SpringBoot启动流程吃透,后面学SpringCloud会轻松一大截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询