
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方法后整个启动流程可以划成六大阶段我先把版图铺开后面逐段拆计时器启动与Java AWT headless模式设置。通过SpringApplicationRunListeners广播Starting事件。组装ApplicationArguments命令行参数和配置环境。打印Banner。创建并刷新ApplicationContext。发布Started事件执行Runner回调。六个阶段里最肥的一段是创建和刷新容器我们日常遇到的大部分启动问题都埋在这一段。但前面几个小阶段也不是走过场比如命令行参数的处理逻辑直接决定了你--server.port8081能不能生效。2. 环境准备阶段配置从哪里来、往哪里去环境Environment是SpringBoot启动的一个核心枢纽它管着后续所有属性值的来源。你可以把它想象成一个大水表所有配置源往这里灌水容器里的bean再从这里面取水。2.1 配置源加载顺序与覆盖优先级SpringBoot的配置源不是一个而是一串。它们按固定优先级排列高优先级覆盖低优先级。常见的配置源从高到低大概是命令行参数最高Java系统属性System.getProperties()操作系统环境变量application-{profile}.yml / application.yml带PropertySource注解的配置这里给个我实测过的例子。一个服务在本地跑得好好的部署到某环境之后就出现端口不对后来查下来是系统环境变量里有一个SERVER_PORT8080覆盖了application.yml里的server.port9090。如果你不理解优先级模型这种问题排查起来就是无头苍蝇。配置环境还有一个值得一提的作用profiles的激活时机。环境准备好之后才去解析spring.profiles.active然后加载对应的profile专属配置。所以你在application.yml里配置profile相关项和你在启动参数里--spring.profiles.activedev指定效果一样但有个隐藏坑环境变量里激活的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的创建过程可以细化为四步getWebServerFactory()从容器里拿ServletWebServerFactory默认是TomcatServletWebServerFactory。配置端口、协议、连接器参数从Environment里读。getTomcatWebServer()实例化Tomcat设置工作目录、初始化连接器。调用tomcat.start()启动服务。这里有个实战场景值得多说一句如果你的业务代码里有用到PostConstruct且它触发了对外的HTTP请求而这个请求打回自己的端口会出现Connection refused。原因很简单PostConstruct是在bean初始化阶段执行的此时Tomcat还没到onRefresh阶段启动端口根本没开。3.3 单例Bean实例化阶段启动时间的大头finishBeanFactoryInitialization()是启动流程里最耗时的一步因为所有非懒加载单例bean都要在这里创建。bean多、依赖链深的应用启动慢基本都是卡在这一段。这段逻辑的核心是DefaultListableBeanFactory.preInstantiateSingletons()按注册顺序遍历bean定义逐个实例化。遇到循环依赖A依赖B、B依赖A时Spring通过三级缓存解决而不是报错——除非你设置了allowCircularReferencesfalse。从排查启动性能的角度你可以用Java Flight RecorderJFR录制启动过程然后在事件里看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把所有注册的自动配置类全部加载进来这一步是候选不代表每个配置都会生效。真正的生效靠的是条件注解比如ConditionalOnClassclasspath下存在某个类才生效。ConditionalOnMissingBean容器里没有指定bean才生效。ConditionalOnProperty配置了指定属性才生效。ConditionalOnWebApplication当前是Web应用才生效。这就像一份大菜单餐厅把所有菜品都列出来加载候选但后厨只做客人实际的菜条件判断、食材有货的菜classpath存在性判断。一整套下来既灵活又不会让你引入的库之间互相打架。我自己调试自动装配问题时的做法是在启动配置里加--debug参数启动后控制台会打印一份自动装配报告哪些生效、哪些不生效、为什么不生效写得清清楚楚。排查第三方starter没生效的问题第一步就靠它。5. 监听器机制与启动后的Runner启动流程的可观测性启动流程不是闷头跑完就完了SpringBoot在各个阶段都抛出了事件给外部方一个旁观和干预的机会。5.1 从Starting到Ready启动事件的完整链条SpringBoot的事件有明确的生命周期顺序ApplicationStartingEventrun最开始环境未准备。ApplicationEnvironmentPreparedEvent环境已准备容器未创建。ApplicationContextInitializedEvent容器创建完毕刷新未开始。ApplicationPreparedEvent刷新前一刻。ApplicationStartedEvent容器刷新完成Runner未执行。ApplicationReadyEventRunner执行完毕应用可以对外提供服务。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-initializationtrue全局开启。代价是第一次请求时会慢可能不符合某些场景适合功能型应用。排查并去掉启动期的远程调用。很多服务默认初始化就会去拉配置中心、调依赖服务这些远程操作是启动慢的元凶。给它们加超时、降级或者干脆延迟到后台任务执行。检查classpath里的组件扫描范围。如果主类在顶层包设得太大会把无关包全部纳入扫描白白产生一堆不必要的bean定义。合理收窄扫描包范围启动时间经常能砍掉三分之一。另外分享一个排查启动时间的技巧启动时加上-Dspring.boot.log.startup-infotrue启动完成后控制台会打印每个阶段耗时一行一行核对定位瓶颈特别方便。7. 我对整套流程的理解SpringBoot的启动流程虽然复杂但骨架非常清晰先构造引导类然后准备环境创建容器刷新容器最后对外暴露服务和托底回调。自动装配和事件监听都是挂在这条主线上的支线。理解它以后你会有一种能掌控启动行为的底气什么时机该做什么、什么扩展点能帮我做什么、出了问题从哪一步查心里都有数。这个内容后续还可以这样扩展如果你想去折腾SpringCloud会发现服务注册发现、配置刷新、熔断降级本质上都在复用同一套启动扩展点体系。把SpringBoot启动流程吃透后面学SpringCloud会轻松一大截。