说实话搞了几年Spring Boot绝大多数人都能熟练写启动类但真被问一句Spring Boot到底是怎么启动起来的能讲清楚的人不多。我自己也是刷了不少源码、断点跟了好几轮才把整条链路捋明白。这篇就把启动原理从入口注解到内嵌容器一层层拆开讲清楚最后再分享几个我实际排查启动问题用的调试手段希望对正在啃这块的朋友有帮助。1. 先搞清楚启动的入口SpringBootApplication不是三个注解那么简单1.1 组合注解的真正构成很多人写启动类就是照抄SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }但这行注解是个复合注解它把三个注解合并到了一起注解作用SpringBootConfiguration其实就是Configuration标明当前类是一个配置类EnableAutoConfiguration开启自动配置是启动原理里的重头戏ComponentScan默认扫描启动类所在包及其子包下的组件当初Spring Boot设计这个组合注解就是为了让使用者少写几个注解。但方便归方便代价就是很多细节被藏起来了导致一旦出了包扫描不到Bean、自动配置不生效这类问题新手完全不知道去哪查。1.2 包扫描范围的坑ComponentScan不带任何参数时它的扫描路径是当前注解所在的类所在的包也就是启动类所在的package。这解释了为什么官方一直要求启动类放在顶层包下。我自己刚转Spring Boot时曾经把启动类放在com.example.admin包下业务代码放在com.example.business包下结果业务Bean全部注入失败当时还以为是配置问题后来才发现是扫描范围压根没覆盖到。解决办法很简单要么把所有代码放在启动类所在包的子包下要么显式指定扫描路径SpringBootApplication(scanBasePackages com.example)但我建议非必要不显式改scanBasePackages因为一旦改了整个项目的包结构约束就形同虚设后续维护的人容易乱放。我见过不少项目就是因为这样最后包结构乱成一锅粥。1.3 SpringBootConfiguration和普通Configuration的区别其实从功能上看SpringBootConfiguration和Configuration没有本质区别Spring Boot给它单独起个名字只是为了在自动配置判定时能明确识别出这是启动配置类。比如Spring Boot内部检测配置类时会优先认SpringBootConfiguration标注的类这样能避免用户自己写的普通Configuration类被误当成主候选配置。这里有个冷门知识点一个项目中只能有一个SpringBootConfiguration标注的类。如果你在别的类上也标了SpringBootConfiguration启动时会直接报错提示找到多个候选主配置。用Configuration就没这个限制。2. SpringApplication.run()的执行主线每个环节到底在做什么2.1 从静态方法到SpringApplication实例SpringApplication.run(Application.class, args)这个静态方法内部其实是两步public static ConfigurableApplicationContext run(Class? primarySource, String... args) { return new SpringApplication(primarySource).run(args); }先new一个SpringApplication实例再调用实例的run方法。new的过程中会做几件事包括推断web应用类型、加载所有META-INF/spring.factories里的Initializer和Listener、推断主配置类等等。这些准备工作大部分人都不会注意到但它们是整体启动的前置条件。我记得看过一个老版本源码new SpringApplication时会通过WebApplicationType.deduceFromClasspath()判断当前是Reactive Web、Servlet Web还是纯非Web应用。这个判断逻辑是看classpath里存不存在特定的类比如有org.springframework.web.servlet.DispatcherServlet就是Servlet Web应用有org.springframework.web.reactive.DispatcherHandler但没有Servlet类就判定为Reactive应用。如果项目里同时引入Spring MVC和Spring WebFlux一般优先判为Reactive。这里有个实际教训有些人引入WebFlux后想继续用Spring MVC那套结果启动时行为完全变了排查半天才反应过来WebApplicationType选错了。所以我建议一个项目里不要混用MVC和WebFlux。2.2 run()方法的主流程我在源码里大致梳理了一下run方法的步骤可以分成以下阶段启动计时器StopWatch记录启动开始时间。通过SpringApplicationRunListeners发布starting事件。解析启动参数准备Environment对象。发布environmentPrepared事件此时Environment已经就绪。打印Banner。创建ApplicationContext。准备ApplicationContext注册一些特殊Bean。执行refreshContext()这一步是Spring容器的核心刷新过程Bean的创建都在这里。调用ApplicationRunner/CommandLineRunner。发布ready事件启动完成。单看这个列表感觉也就是走流程。但实际Debug时你会发现第8步refreshContext里藏了自动配置的入口和内嵌Web容器的启动这块才是重点。2.3 Environment准备阶段容易被忽略的细节Environment里包含Properties、Profiles等。Spring Boot启动时会先把系统属性、环境变量、application配置文件的属性全都合并进去。这个阶段的顺序很有讲究配置来源的优先级从高到低大致为命令行参数、Java系统属性、环境变量、application配置文件按profile区分后面覆盖前面等。实际踩坑点目录下同时有application.yml和application.yaml时Spring Boot默认只加载其中一个优先级跟文件顺序有关。为了省事我一般只保留application.yml。另外同一个配置项在环境变量、命令行和yml里都出现时最终生效的是命令行参数这在部署时用来覆盖端口号非常方便。java -jar app.jar --server.port8081如果配置文件里写死了server.port命令行加这个参数就能覆盖不用改文件。我之前在排查一个线上问题时发现明明配置文件写的端口是8080实际跑起来却是9090最后查出来是环境变量里有个SERVER_PORT9090在作祟。后来我学乖了遇到配置不生效先按优先级顺序逐个排除。3. 自动配置是怎么自动起来的条件注解与AutoConfiguration导入3.1 自动配置的入口EnableAutoConfigurationEnableAutoConfiguration是SpringBootApplication里最核心的注解它借助Import(AutoConfigurationImportSelector.class)来导入一批自动配置类。AutoConfigurationImportSelector会从classpath下查找候选的自动配置类然后按条件筛选后注册到容器。Spring Boot 2.7之前候选配置类的清单放在META-INF/spring.factories文件里里面有一项org.springframework.boot.autoconfigure.EnableAutoConfiguration后面跟着一堆配置类全限定名。Spring Boot 3时代改用了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件它是个纯文本文件每行一个自动配置类的全限定名。如果大家在看源码时找不到spring.factories里的自动配置列表别慌去imports文件里看那是新版本的位置。3.2 条件注解自动配置的开关自动配置类全都被加载进来了但并不是全部生效否则所有项目都会被注入几百个Bean。关键在于每个配置类和配置方法上都有条件注解最常用的几个条件注解判断时机典型场景ConditionalOnClass某个类是否在classpath中有DataSource类才配置数据源ConditionalOnMissingBean容器中是否已有某个Bean用户自定义了ThreadPoolTaskExecutor就不自动创建ConditionalOnProperty配置项是否存在且符合预期值通过配置开关控制某个功能是否生效ConditionalOnBean容器中是否存在某个Bean依赖于别的Bean存在才创建当前Bean以DataSourceAutoConfiguration为例Spring Boot内置了好几个数据源配置策略如Hikari、Tomcat JDBC、DBCP2等它通过ConditionalOnClass判断classpath里有哪些连接池类按顺序选择其中一个。如果项目里引入了spring-boot-starter-jdbc和HikariCPHikari相关类存在那数据源默认就用Hikari。这也就是为什么我们在pom里加个starter什么事也不用管数据库连接池就自动配好了。3.3 条件注解的评估顺序是坑中坑条件注解的执行顺序会直接影响判断结果。Spring Boot的AutoConfigurationImportSelector实现了DeferredImportSelector这类选择器的延迟导入机制保证了自动配置类在用户自己定义的普通配置类之后处理。这样用户可以先注册一些Bean自动配置类里的ConditionalOnMissingBean才能正确判断这个Bean是不是已经有了。举个例子我想自定义一个ObjectMapper的定制逻辑但不想让Spring Boot的JacksonAutoConfiguration覆盖我的配置。只要我通过Configuration定义一个ObjectMapper类型的Bean由于自动配置在普通配置之后处理ConditionalOnMissingBean发现容器里已经有用户定义的ObjectMapper它就不会再自动创建默认的ObjectMapper。这个机制用好了几乎可以覆盖所有自动配置的默认行为。但也有人理解反了以为自动配置先执行于是写了个普通配置类想覆盖配置结果怎么都不生效还怀疑Spring Boot的Bug。我碰到很多次这种问题最后都是通过搞清楚条件评估顺序解决的。3.4 自动配置类里常见的设计模式打开任意一个AutoConfiguration类结构基本都是AutoConfiguration(after SomeConfiguration.class) ConditionalOnClass(Some.class) EnableConfigurationProperties(SomeProperties.class) public class SomeAutoConfiguration { Bean ConditionalOnMissingBean public Some some() { return new Some(); } }配合EnableConfigurationProperties将配置属性绑定到SomeProperties上配置项就能映射到对象属性。为什么Spring Boot配置项那么多但代码很清晰靠的就是这套配置属性绑定条件注解的组合。我们自己写组件时完全可以照搬这个思路比如做一个自定义的短信发送starter通过ConfigurationProperties定义prefix为sms的配置项然后用ConditionalOnProperty(prefix sms, name enabled, havingValue true)控制开关再通过ConditionalOnMissingBean允许使用者自定义实现类覆盖默认实现。这套设计模式才是自动配置真正值得学的地方。4. ApplicationContext的创建与刷新理解Bean生命周期的主战场4.1 不同Web应用类型对应的Context实现Spring Boot会根据前面推断出的WebApplicationType选择不同的ApplicationContext实现类。Servlet Web应用一般用AnnotationConfigServletWebServerApplicationContextReactive应用用AnnotationConfigReactiveWebServerApplicationContext非Web应用用AnnotationConfigApplicationContext。不同Context最大的差异体现在refresh方法里Servlet Web的Context在刷新时多了一个onRefresh()步骤里面会调用WebServer工厂创建内嵌容器。也就是说Tomcat的启动时机并不是run()方法刚开始时而是在容器刷新阶段这个时间点很多人会记错。他们误以为写上SpringApplication.run()Tomcat立刻就起来了其实之前还有一些环境准备和Context创建的工作。4.2 refresh()方法里的关键步骤Spring Boot启动虽然封装了很多自己的逻辑但核心Context刷新还是继承自Spring Framework的AbstractApplicationContext.refresh()。整个流程可以概括为prepareRefresh准备刷新记录启动时间。obtainFreshBeanFactory获取BeanFactory。prepareBeanFactory配置BeanFactory的标准特性比如类加载器、SpEL解析器等。postProcessBeanFactory留给子类扩展。invokeBeanFactoryPostProcessors执行BeanFactoryPostProcessor包括ConfigurationClassPostProcessor这一步最重要扫描Component类、解析Bean方法、导入配置类基本都在这里完成。registerBeanPostProcessors注册BeanPostProcessor。initMessageSource初始化国际化资源。initApplicationEventMulticaster初始化事件广播器。onRefresh子类扩展点Tomcat的启动就是在这个方法里触发的。registerListeners注册监听器。finishBeanFactoryInitialization实例化所有非懒加载的单例Bean。finishRefresh完成刷新发布ContextRefreshed事件。任一步骤抛异常整个启动就会失败。不要光记步骤更重要的是理解第5步和第11步之间的时序关系。Bean是什么时候被创建的通常不是在扫描时创建而是在第11步才真正实例化。扫描阶段只是注册BeanDefinition把它当成原料清单实例化阶段才把它们变成真正的对象。这个概念不搞清楚排查循环依赖问题时往往会一头雾水。4.3 BeanFactoryPostProcessor和BeanPostProcessor为什么不是一回事很多人把这两个概念搞混它们名字像但作用差异巨大BeanFactoryPostProcessor作用于BeanDefinition阶段可以在Bean实例化前修改Bean定义比如修改属性值、改变作用域。BeanPostProcessor作用于Bean实例化阶段在Bean初始化前后介入可以做代理增强AOP就是靠BeanPostProcessor实现的。Spring Boot启动过程中内置的ConfigurationClassPostProcessor就是一个BeanFactoryPostProcessor它负责解析Configuration类、扫描Component、导入Import等。这一步执行完BeanDefinition基本齐全。而像AnnotationAwareAspectJAutoProxyCreator就是BeanPostProcessor它会在Bean初始化后创建代理对象。这个差异是理解Spring启动顺序的门槛跨过去很多启动日志里的异常就看得懂了。4.4 循环依赖在启动时怎么暴露Bean实例化阶段如果有循环依赖Spring处理方式分几种。最常见的是单例setter注入循环依赖能解决因为Spring有三级缓存singletonObjects、earlySingletonObjects、singletonFactories。但构造器注入产生的循环依赖Spring直接放弃报BeanCurrentlyInCreationException。我印象很深的一次场景两个Service通过构造器互相注入启动时一直报Requested bean is currently in creation排查了半天才发现是循环依赖。后来把其中一个改成setter注入或字段注入才解决。这里我的建议是不到万不得已不要用字段注入但循环依赖也确实应该通过重构消除而不是靠缓存机制硬扛。5. 内嵌Web容器启动Tomcat是怎么在refresh里顺手启动的5.1 从ServletWebServerFactory到WebServerSpring Boot 2.x之后Servlet Web容器启动的核心是ServletWebServerFactory接口它有多个实现对应Tomcat、Jetty、Undertow。通过依赖的starter不同自动配置会注册对应的工厂Bean。比如引入spring-boot-starter-tomcat默认就有TomcatServletWebServerFactory。在Context刷新的onRefresh()方法里Spring会调用onRefresh()创建WebServer。真正启动逻辑在ServletWebServerApplicationContext中它会从容器里拿ServletWebServerFactory然后调用factory.getWebServer(...)来生成一个WebServer并调用webServer.start()。所以流程简化后是Context刷新 → onRefresh → 获取ServletWebServerFactory Bean → getWebServer创建Tomcat实例 → 启动Tomcat → 部署DispatcherServlet。5.2 DispatcherServlet是谁注册进去的Web容器启动时需要知道怎么处理Servlet请求。Spring Boot的DispatcherServletRegistrationBean会负责把DispatcherServlet注册到内嵌容器的ServletContext中。这个注册过程发生在WebServer启动之前或启动过程中。细心的人会从启动日志里发现Tomcat started on port(s): 8080是一条晚于Root WebApplicationContext: initialization completed的日志因为WebServer就是在Context初始化到一定阶段才启动的。实际场景中如果你自定义了ServletRegistrationBean它们会在同一个阶段被注册。我曾经为了让某个Filter只拦截特定URL写了一个FilterRegistrationBean并设置了setUrlPatterns和setOrder调试时发现注册顺序完全由Order控制和定义Bean的顺序无关。这块可以通过查看ServletContextInitializerBeans的排序逻辑来验证。5.3 端口冲突和WebServer启动失败排查内嵌Tomcat最常见的启动失败原因就是端口被占用。报错长这样Web server failed to start. Port 8080 was already in use.排查思路很简单先找到占用8080端口的进程把它停掉或者换端口。Linux上lsof -i:8080 netstat -tunlp | grep 8080但有些情况更隐蔽比如端口配置在环境变量里被覆盖成未知值或者Tomcat启动了但过几秒又挂掉日志显示Connector configured to listen on port xxxx实际却监听失败。我遇到过本机多网卡导致Tomcat绑定到某个不在预期网段的IP上解决方法是显式指定server.address或者调整网络接口顺序。这里想提醒大家内嵌容器的启动不是黑盒控制台日志里只要有DEBUG级别就能看到o.apache.catalina.core下的详细启动过程排查时先开DEBUG。6. 启动过程的监听器与事件机制程序员能介入的扩展点6.1 初始化器SpringApplicationRunListener和ApplicationListenerSpring Boot启动过程不是铁板一块它留了很多事件扩展点。SpringApplicationRunListener是整个启动生命周期的事件监听入口比如started、environmentPrepared、contextPrepared、contextLoaded、started、ready、failed等。它和ApplicationListener的区别在于前者是Spring Boot内部专用的能拿到SpringApplication本身后者是Spring容器标准的事件监听可以处理ApplicationContext内部发出的各种事件。要监听启动阶段很多人的第一反应是实现ApplicationListener但是要注意如果用普通的Component方式注册ApplicationListener这个监听器本身也需要在Context刷新后才能被加载某些早期事件可能收不到。所以Spring Boot规定如果要在启动早期就介入需要把监听器通过SpringApplication.addListeners()方法注册或者用META-INF/spring.factories配置org.springframework.context.ApplicationListener项。这块的优先级和生效范围我之前测过SpringApplicationRunListener事件最全但配置最繁琐ApplicationListener加SpringApplication实例注册的方式兼顾可行性和覆盖面。6.2 ApplicationRunner和CommandLineRunner的差异启动完成、Context刷新完成后Spring Boot会执行ApplicationRunner和CommandLineRunner。两者功能类似都是run()结束前再执行一段自定义代码。区别只是参数类型CommandLineRunner接收原始String数组ApplicationRunner接收封装后的ApplicationArguments。很多项目在启动时要做初始化数据、预热缓存就喜欢在这里面写业务逻辑。经验是区分好必须在这里做还是可以用PostConstruct替代的操作。如果依赖的是整个Context已经就绪用Runner比较稳如果只是想对某个Bean自身初始化后做点操作PostConstruct就够。PostConstruct在Bean创建阶段执行那时某些依赖或许还没完全就绪容易踩到NPE。我自己通常用ApplicationRunner并且加上Order控制执行顺序这样逻辑更为可控。6.3 失败分析和启动失败退出码启动失败时Spring Boot会发布ApplicationFailedEvent并尝试找出失败原因。Spring Boot的FailureAnalyzers机制会把异常信息翻译成人类可读的提示比如端口占用、数据源配置失败等。我们自己也能注册一个FailureAnalyzer把某个特定异常转换成更清晰的提示。这个机制在写组件或框架时特别有用能显著降低使用方的排查成本。我自己的一个习惯是项目启动永远不要忽略日志里的Application run failed之前的堆栈信息经常真正的根因在中段后面只是跟随异常。遇到启动失败先翻完整堆栈找到第一行Caused by那才是源头。7. 启动性能优化和问题定位的实战手段7.1 启动计时与耗时分析Spring Boot的启动日志自带启动耗时但这只是整体时间。想定位启动瓶颈可以开启启动过程统计spring: main: lazy-initialization: true开启懒加载后启动速度确实会快但代价是第一次访问某个Bean时才初始化接口首次耗时会变长。生产环境慎用。更好的办法是使用Actuator的/trace或/metrics或者通过JFR/JMC录制启动过程。我自己比较常用的方法是在IDE里断点配合StopWatch查看每个阶段耗时或者将日志级别调成DEBUG看哪一步输出间隔最长。7.2 常见启动异常与排查清单我把平时总结的启动排查清单整理成了一张表方便遇到问题时照着查现象可能原因排查方向端口被占用其他进程占用端口或端口配置冲突netstat/lsof查端口检查环境变量覆盖自动配置不生效条件注解不满足或配置类顺序问题启动加--debug查看自动配置报告Bean注入失败包扫描范围不对或循环依赖检查启动类位置看完整堆栈配置项不生效配置来源优先级覆盖按命令行系统属性环境变量配置文件顺序排查Context刷新失败某个FactoryPostProcessor抛异常打开DEBUG定位失败步骤这里特别推荐一个技巧启动时加--debug参数不是日志级别DEBUGSpring Boot会输出一份自动配置报告里面清楚列出哪些自动配置条件匹配成功、哪些失败及其原因。这个报告对于排查为什么DataSourceAutoConfiguration没生效这类问题简直神兵利器。7.3 从启动原理反推框架设计思路我学了启动原理之后最大的收获不是能炫技讲源码而是能设计出更合理的基础框架。比如我们开发了一个内部埋点SDK就完全参照Spring Boot自动配置的思路定义AutoConfiguration.imports用ConditionalOnProperty控制埋点开关用ConditionalOnMissingBean让业务方可以注入自定义的埋点实现再通过ConfigurationProperties绑定自定义配置项。业务方只要引入依赖并配置一行开关SDK就能自动生效。这就是从原理到实践的正向循环。如果你也想把Spring Boot启动原理彻底吃透我建议直接去看以下几个关键类SpringApplication、ApplicationContext、ConfigurationClassPostProcessor、AutoConfigurationImportSelector、TomcatServletWebServerFactory。不要一个个类从头读到尾而是从一个场景出发比如为什么我加了一个StarterDataSource就自动配好了顺着调用链往下翻效率高得多。另外提醒一句源码版本不同类名和位置会有变化特别是Spring Boot 2.7到3.x的迁移自动配置文件从spring.factories换成了AutoConfiguration.imports启动流程里也调整了部分类的包名。看源码时先确定版本别拿旧文章里的类名去新版里找容易绕远路。我个人调试启动原理最常干的一件事就是在AbstractApplicationContext.refresh()里打上断点然后一步步往下走观察每个阶段BeanDefinition列表的变化。这样比单纯看代码要直观得多。建议你也试试花一天时间跟完全流程后面遇到任何启动异常心里都会特别有底。