Spring Boot启动原理全解析:从SpringApplication到自动配置与内嵌容器

发布时间:2026/10/23 20:59:15
Spring Boot启动原理全解析:从SpringApplication到自动配置与内嵌容器
每次看Spring Boot的启动过程很多人的理解停留在注解一加、run一发、项目就起来了这个黑盒层面。真正被面试官问到底层链路或者线上启动变慢、自动配置不生效的时候往往说不出个所以然来。我前前后后啃过几轮源码踩过不少启动相关的坑这篇文章就把Spring Boot启动原理和相关组件串一遍从SpringApplication.run()到自动配置、内嵌容器、Starter设计再到Actuator监控和实战排障尽量说透希望能帮你建立一条完整的认知链路。1. 从SpringApplication.run()说起启动入口背后的完整链路1.1 第一步不是new容器而是感知阶段很多人以为SpringApplication.run(Application.class, args)直接就是创建容器其实在创建容器之前有个非常容易被忽略的new SpringApplication(primarySources)阶段。在这个构造函数里Spring Boot做了三件关键事情。第一件是推断Web应用类型。代码逻辑很直白如果javax.servlet.Servlet和org.springframework.web.context.ConfigurableWebApplicationContext都在类路径上就推断为Servlet类型如果reactor.netty.http.server.HttpServer存在就是Reactive类型否则是None。这个推断结果直接决定了后面创建哪种ApplicationContext。第二件是从spring.factories以及新版本的AutoConfiguration.imports读取ApplicationContextInitializer和ApplicationListener的全限定名列表然后实例化并保存起来。这两类组件在后面的prepareContext阶段会一一发挥作用很多人启动慢或者监听不到事件往往就是没有意识到这两类组件在run()方法之外就已经被加载了。第三件是确定主启动类。这里用的是deduceMainApplicationClass()它不是随便反射调用栈的而是遍历当前线程栈帧找到第一个包含main方法的类。这也是为什么Spring Boot要求主类上必须有SpringBootApplication并且通常放在包的最外层——推断出来的主类会配上主类名后续用于默认扫描包路径的锚点如果主类位置放错ComponentScan默认扫描不到兄弟包里的Bean。1.2 准备Environment配置源从哪来进入run()方法后首先看到的是StopWatch和SpringApplicationRunListeners。StopWatch是启动计时的关键它记录了从启动到假死的每个阶段耗时排启动性能问题的时候非常有帮助。然后是SpringApplicationRunListeners.starting()这一步触发了最早的监听事件。Spring Boot 2.4以后引入了DefaultBootstrapContext它先于ApplicationContext存在给ApplicationContextInitializer提供早期初始化能力比如配置在bootstrap阶段的上下文加载。这里有个细节SpringApplicationRunListener是从META-INF/spring.factories里加载的常见的就是EventPublishingRunListener它把启动过程中的节点转化成ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent、ApplicationContextInitializedEvent等事件发布出去。接下来是prepareEnvironment。这一步会把系统属性、系统环境变量、application.properties/yml等的PropertySource按优先级封装成ConfigurableEnvironment。这里有个很多人踩的坑Spring Boot默认的配置优先级是命令行参数最高然后是Java系统属性和OS环境变量最后才是配置文件。而且同一个application.yml里还能用spring.profiles.active激活不同Profile。如果线上环境变量和配置文件里同一个key冲突很容易出配置文件改了但没生效的诡异问题本质就是没搞懂这一步的PropertySource顺序。最后在prepareEnvironment末尾会调用listeners.environmentPrepared()发布ApplicationEnvironmentPreparedEvent。配置中心客户端、日志系统初始化这些扩展很多就是监听这个事件来做的。1.3 创建与刷新容器启动的真正分水岭环境准备好了才轮到createApplicationContext()。如果是Servlet类型这里创建的是AnnotationConfigServletWebServerApplicationContext这个类的名字里有ServletWebServer说明它和后面的内嵌Web容器是绑定的。prepareContext阶段做三件事注册BeanDefinition把主配置类注册进去执行之前加载好的ApplicationContextInitializer发布ApplicationPreparedEvent。如果你在启动日志里看不到自定义的ApplicationContextInitializer执行可能就是这个类的实例化和初始化顺序出了问题。真正的分水岭是refreshContext(context)它内部调用的是AbstractApplicationContext.refresh()这一步才是Spring IOC容器完整的加载流程。Spring Boot在onRefresh()阶段会调用createWebServer()把内嵌Tomcat、Jetty或Undertow创建出来并启动这也是为什么启动日志里能看到Tomcat started on port(s): 8080出现在Bean初始化之后。而SpringApplication里的afterRefresh()在新版本里已经没什么逻辑了更多是留给子类扩展大家看源码的时候不需要过度关注。refreshContext完成后还会执行callRunners运行ApplicationRunner和CommandLineRunner发布ApplicationStartedEvent和ApplicationReadyEvent。到这一步Spring Boot才真正对外宣布服务已就绪。2. 自动配置的魔法EnableAutoConfiguration与spring.factories如何联动2.1 EnableAutoConfiguration的注解层级SpringBootApplication是个组合注解核心是SpringBootConfiguration、EnableAutoConfiguration、ComponentScan三个。其中自动配置的关键就是EnableAutoConfiguration。EnableAutoConfiguration本身又用Import(AutoConfigurationImportSelector.class)导入了一个选择器。这个选择器和我们平时写的Import导入普通类最大的区别是它实现了DeferredImportSelector并且结合了AutoConfigurationGroup对自动配置类做分组、排序和去重。DeferredImportSelector的意义在于它不会立即import而是等所有普通配置类都处理完之后再执行这样用户自定义的配置类有更高的优先级处理顺序自动配置类的条件判断不会误伤用户Bean。在老版本里自动配置类的名单写在META-INF/spring.factories的EnableAutoConfiguration键下面。Spring Boot 2.7开始推荐使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件就是纯行格式每行一个自动配置类的全限定名。新版的导入文件好处是不支持键值对命名更明确不会像spring.factories那样被其他框架的key搞混。2.2 条件注解如何决定配置是否生效自动配置类加载了不代表全部生效每个自动配置类上面几乎都打满了条件注解。最常见的组合是ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty。比如DataSourceAutoConfiguration上有ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })意思是类路径上必须有javax.sql.DataSource否则整个数据源自动配置直接跳过。理解条件注解的执行顺序很重要。Spring Boot内部用ConditionEvaluator逐条判断判断时会用到MetadataReaderFactory读取类的注解元数据而不是直接加载类避免了类不存在时ClassNotFound的尴尬。但它也没法做到完全不触发类加载有些ConditionalOnClass里写的类如果在编译期就缺失需要用name属性用字符串指定否则IDE里编译都会报错。排除自动配置的方式也有讲究。可以用SpringBootApplication(exclude DataSourceAutoConfiguration.class)也可以用spring.autoconfigure.exclude配置项或者在AutoConfiguration.imports里根本不写。排除了以后excludeAutoConfiguration会建立一个排除集合在导入阶段直接过滤掉。实际排查为什么自动配置没生效的时候第一步要看的就是条件是否满足第二步就是看是否被排除。2.3 自动配置与自定义Bean的优先级自动配置设计的一个重要原则是用户自定义优先。大量自动配置类上都有ConditionalOnMissingBean这个条件的意思是只有容器里没有用户定义的指定类型Bean时自动配置的Bean才会生效。举个实际例子RedisAutoConfiguration里的RedisTemplate用了ConditionalOnMissingBean(name redisTemplate)。如果你自己在配置类里声明一个叫redisTemplate的Bean自动配置的就会自动退场。但这里有个细节ConditionalOnMissingBean对Configuration类里通过Bean方法生成的Bean判断比较严格如果用户Bean是通过Component扫描进来的判断时机可能在BeanDefinition注册阶段就已经生效了。如果你在排查我自己定义的RedisTemplate为什么没生效时发现好像被覆盖了可以先看下是不是类路径上额外引入了哪个自动配置类恰好也定义了同类型Bean。此外自动配置类之间也有先后顺序通过AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder来控制。比如RedisAutoConfiguration往往在CacheAutoConfiguration之前因为缓存自动配置可能依赖Redis的底层连接工厂。顺序不对的话模块间的依赖Bean就会在启动时出现找不到依赖的问题。3. 内嵌Web容器与条件装配为什么Tomcat会自动起来3.1 ServletWebServerFactoryAutoConfiguration的链式逻辑Web应用启动起来最直观的感受是我什么依赖Tomcat都没配它怎么就自己跑起来了。答案藏在内嵌容器自动配置里。负责这个逻辑的主配置类是ServletWebServerFactoryAutoConfiguration它上面有ConditionalOnClass(ServletRequest.class)、ConditionalOnWebApplication(type Type.SERVLET)这俩条件保证只在Servlet Web应用里才生效。真正创建具体容器工厂的是通过Import导入的EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow三个内部配置类它们是按条件注解二选一/三选一的关系。EmbeddedTomcat上有ConditionalOnClass({ Servlet.class, Tomcat.class })也就是说只要类路径上有org.apache.catalina.startup.TomcatTomcat就是默认容器。如果同时引入了tomcat-embed-core和jetty-embed-core则谁先匹配到条件就由类路径里的依赖顺序决定不过官方强烈不建议同时引入多个内嵌容器依赖。3.2 类路径检测与条件判断的先后顺序Spring Boot不是因为你在pom里加了starter-web所以Tomcat才启动更准确的表述是spring-boot-starter-web传递引入了spring-boot-starter-tomcat进而引入了tomcat-embed-core于是类路径上就存在Tomcat.class条件注解判断通过EmbeddedTomcat配置类生效返回TomcatServletWebServerFactory。这个条件装配的顺序发生在容器refresh阶段。当AnnotationConfigServletWebServerApplicationContext.onRefresh()被调用时会获取ServletWebServerFactory类型的Bean调用factory.getWebServer(getSelfInitializer())创建Tomcat实例并启动。所以说容器启动并不是一个独立于Spring生命周期之外的动作它就是onRefresh的一部分。这里要提一个排查Web端口起不来的常见问题如果看到Port already in use或者Unable to start embedded Tomcat不一定只是端口被占用。还有可能是你的ServletWebServerFactory被自定义配置覆盖了比如手动定义了TomcatServletWebServerFactory并设置了自定义TomcatConnectorCustomizer结果导致默认端口、协议等配置失效。3.3 手写一个简化版内嵌容器自动配置为了更好理解我写过一个简化版的自动配置Demо思路基本一致。核心就是一个配置类通过条件注解和导入选择器把容器工厂选出来Configuration(proxyBeanMethods false) ConditionalOnClass({ Servlet.class, Tomcat.class }) public class MyEmbeddedTomcatAutoConfiguration { Bean ConditionalOnMissingBean public ServletWebServerFactory tomcatFactory() { return new TomcatServletWebServerFactory(); } }然后把这个类写进META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件Spring Boot启动时就会自动装载这个配置类。实际项目里当然比这复杂比如ServletWebServerFactoryAutoConfiguration还会通过Autowired把TomcatConnectorCustomizer、TomcatProtocolHandlerCustomizer收集起来用于定制容器。但模式就是这一套条件判断、工厂模式、缺省Bean。理解了这层逻辑面试里遇到内嵌Tomcat和普通Tomcat有什么区别这类问题就好答了普通Tomcat由外部脚本启动应用通过web.xml或ServletContainerInitializer加载内嵌Tomcat则是在Spring容器refresh过程中由工厂创建并启动生命周期完全由Spring管理。4. 启动器Starter的设计哲学依赖、命名与传递4.1 spring-boot-starter的依赖传递机制很多人把Starter和自动配置混为一谈严格说Starter主要解决的是依赖管理和模块化的问题自动配置类才是在运行时干活的。一个Starter本质就是一个Maven模块pom里声明了一批依赖有的Starter本身没有任何Java代码。拿spring-boot-starter-web来说它的pom依赖了spring-boot-starter、spring-boot-starter-tomcat、spring-web、spring-webmvc。因为Maven依赖是传递的你只要引入一个spring-boot-starter-web整个Web开发相关的jar包括内嵌Tomcat就全部进入类路径。这也是自动配置能生效的原因之一条件注解需要类存在而Starter把类存在这个前置条件帮你配好了。依赖管理方面spring-boot-dependencies作为BOM统一管理版本号。它会指定Spring Framework、Tomcat、Jackson等各组件的版本用户不用在pom里写版本号除非有特殊要求需要覆盖。这里要注意的是依赖传递也会带来版本冲突问题比如同时引入一个老版本的第三方Starter它内部强依赖某个旧log4j版本就可能覆盖Spring Boot统一管理的版本启动时出现NoSuchMethodError。4.2 自定义一个业务场景Starter自己写Starter并不神秘核心就三步定义自动配置类、写AutoConfiguration.imports、用ConfigurationProperties配置项绑定。我之前在团队里给内部消息队列封装过一个mqops-spring-boot-starter结构是这样的目录规范mqops-spring-boot-starter (聚合模块) ├── mqops-spring-boot-autoconfigure (自动配置模块) │ ├── src/main/java/com/xx/mqops │ │ ├── MqOpsAutoConfiguration.java │ │ ├── MqOpsProperties.java │ │ └── MqOpsTemplate.java │ └── src/main/resources/META-INF/spring │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports └── mqops-spring-boot-starter (仅pom)自动配置类示例AutoConfiguration EnableConfigurationProperties(MqOpsProperties.class) ConditionalOnClass(MqOpsTemplate.class) public class MqOpsAutoConfiguration { Bean ConditionalOnMissingBean public MqOpsTemplate mqOpsTemplate(MqOpsProperties properties) { return new MqOpsTemplate(properties); } }配置属性ConfigurationProperties(prefix mqops) public class MqOpsProperties { private String brokers; private String topicPrefix; // getter/setter }而mqops-spring-boot-starter模块的pom就一句话dependencies dependency groupIdcom.xx/groupId artifactIdmqops-spring-boot-autoconfigure/artifactId /dependency /dependencies使用者只要引入mqops-spring-boot-starter在yml里配一下mqops.brokersxxxSpring Boot启动时就会自动创建MqOpsTemplate。这一套做下来团队里接入方的配置成本非常低而且不会暴露内部实现细节。4.3 启动器命名的坑与约定Starter的命名有明确官方约定Spring官方提供的Starter命名是spring-boot-starter-*第三方自定义Starter推荐使用*-spring-boot-starter。比如mybatis-spring-boot-starter、redisson-spring-boot-starter。如果反着命名成spring-boot-starter-mybatis会让人误以为这是官方维护的而且Spring Boot的自动配置扫描也不会因为名字不同就区别对待只是社区习惯和商标问题。另外注意自动配置模块里配置类的类名最好加上AutoConfiguration后缀放在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里的顺序也影响加载顺序。如果业务自动配置依赖另一个自动配置的Bean可以用AutoConfigureAfter指定否则可能出现Bean找不到的情况。5. 监控组件Actuator在启动链路中的角色与端口暴露风险5.1 启动过程中Metrics与HealthIndicator的装配时机spring-boot-starter-actuator的地位常被低估它不是一个纯运维组件它本身也深度参与Spring Boot启动链路。Actuator模块引入后actuator-autoconfigure里的MetricsAutoConfiguration、HealthContributorAutoConfiguration会在自动配置阶段注册一堆MeterBinder和HealthIndicator。ConditionEvaluationReportAutoConfiguration会记录所有自动配置类的匹配结果这也是为什么你能通过访问/actuator/conditions看到哪些自动配置生效、哪些没生效及原因。启动阶段健康检查本质上是在HealthEndpoint里聚合所有HealthIndicator的检查结果比如DiskSpaceHealthIndicator、PingHealthIndicator。一个值得注意的是HealthIndicator的Bean创建也在IOC刷新过程中完成但HealthEndpoint通常会等ApplicationReadyEvent发布后才真正对外提供完整的健康状态。这就是为什么线上有时候端口报UP但业务还有一个核心线程池没初始化完因为自定义的初始化逻辑可能放在ApplicationRunner里而HealthIndicator默认会先于Runner完成。5.2 Actuator的暴露配置与常见漏洞防御Actuator最有争议的地方是暴露端点的安全问题。早期版本默认暴露health和info但很多教程会把management.endpoints.web.exposure.include*直接抄到生产环境结果heapdump、env、jolokia这些端点全部开放轻则泄露数据库密码和密钥重则被人通过Jolokia和Logback组合直接RCE。历史上相关的漏洞通报不少网上搜actuator 漏洞就能看到一大串。我的生产配置一般是这样的management.endpoints.web.exposure.includehealth,info,metrics,prometheus,conditions,startup management.endpoint.health.show-detailsalways management.endpoint.health.probes.enabledtrue management.endpoints.web.base-path/internal/actuator然后网关或Nginx层面会把这组路径限制在运维网段并且叠加统一的身份认证。一定要记住Actuator的shutdown端点即使不在exposure里也不会被访问但要启用必需management.endpoint.shutdown.enabledtrue生产环境我基本不开启。每个端点都有自己的场景暴露面越小越好不是越全越专业。5.3 启动耗时分析利用Actuator的startup端点排查启动慢的问题除了看日志和--debugActuator还有一个低频但实用的端点/actuator/startup。它依赖SpringApplication.setApplicationStartup()设置的实现默认是ApplicationStartup的空实现不会产生任何数据。要在启动类里启用public static void main(String[] args) { SpringApplication app new SpringApplication(DemoApplication.class); app.setApplicationStartup(new BufferingApplicationStartup(2048)); app.run(args); }此时访问/actuator/startup就能看到启动过程中Bean创建、事件发布、自动配置计算的Timeline能定位是哪个Bean拖慢了启动速度。配合/actuator/conditions看哪些自动配置无效加载理论上可以把启动耗时优化得肉眼可见。不过BufferingApplicationStartup会占用额外内存生产上如果长时间开启注意调整缓冲大小和是否在启动完成后关闭。6. 启动慢、自动配置不生效、循环依赖——实战踩坑排查记录6.1 启动慢的常见根因与排查方法启动慢是我被问得最多的一类问题。Spring Boot启动慢很少是单点原因我见过的典型案例有这些第一个是类路径上存在大量不必要的自动配置。比如一个纯Redis缓存的微服务因为pom里误加了spring-boot-starter-data-jpa启动时就会去初始化数据源解析JPA实体连DataSourceAutoConfiguration、HibernateJpaAutoConfiguration全跑一遍耗时可能多出几秒。排查方法是用--debug启动看启动日志里的Positive matches哪类自动配置我觉得不该出现就去pom里查对应的传递依赖。第二个是懒初始化被全局打开。spring.main.lazy-initializationtrue确实能缩短启动时间但副作用是所有Bean在第一次调用时才创建启动后第一次请求会非常慢而且一些依赖Bean之间会出现奇怪的初始化顺序问题。生产环境不建议全局开启如果真的想优化可以对特定耗时的Bean单独设置Lazy。第三个是DNS反向解析和网络超时。比如日志Appender里配置了远程Kafka或ES地址启动时做网络探测遇到不可达地址会卡很久。排查时可以用jstack抓线程看启动线程阻塞在哪个InetAddress.getByName调用上。第四个比较隐蔽是spring.factories或AutoConfiguration.imports文件里出现了不存在的类会导致启动报错出现重复自动配置类则可能导致多个同类Bean覆盖虽然不一定会报错但会让启动日志显得混乱。6.2 自动配置不生效的检查清单遇到我加了redis starter为什么RedisTemplate是null这类问题我建议按这个清单查mvn dependency:tree确认依赖真的进来了检查是否被exclusions排除。看启动日志和/actuator/conditions定位对应的自动配置类是否Positive matches。如果Negative matches看条件注解为什么失败通常是类缺失、属性不匹配、Bean已存在。检查spring.autoconfigure.exclude有没有在配置文件里排除过。检查SpringBootApplication扫描路径如果自定义配置类和自动配置类之间有覆盖关系确认ConditionalOnMissingBean判断是否被用户Bean抢先满足条件。检查AutoConfiguration.imports文件路径和格式新版本要求放在META-INF/spring/下面否则不生效。这个清单基本能解决90%的自动配置不生效问题。6.3 循环依赖的触发时机与解决思路Spring Boot 2.6开始默认禁止循环依赖这个改动让很多老项目升级时直接启动失败报错The dependencies of some of the beans in the application context form a cycle。很多人不理解为什么Spring Boot突然变严格其实Spring Framework从4.0起就通过三级缓存解决了单例Bean的循环依赖但它无法解决构造函数注入的循环依赖而且循环依赖本身是设计问题的信号。跨模块的A依赖B、B依赖A这类循环依赖通常不是靠Lazy塞一下就完事的它往往意味着职责边界划分有问题。我的建议是如果是Async、Transactional这类代理对象引发的循环依赖优先考虑拆开或者用一个中间层门面对象如果两个Bean只是调用方法可以通过事件解耦或者把共享逻辑抽到第三个Service里这样启动链路更干净也方便测试。7. 回答面试官如何讲清楚Spring Boot启动原理7.1 一条时间线的表述框架面试被问到Spring Boot启动原理不需要逐行背源码但要有时间线意识。我会这样讲Spring Boot启动是建立在Spring Framework容器之上的一个扩展流程。入口是SpringApplication.run()它先做类型推断和应用类型判断加载ApplicationContextInitializer和ApplicationListener然后prepareEnvironment把配置文件、系统属性、环境变量封装成Environment。接着创建对应的ApplicationContext在prepareContext阶段注册主配置类并调用初始化器。然后调用refresh()进入Spring容器标准生命周期在onRefresh()时创建内嵌Web容器。refresh完成后再发布ApplicationStartedEvent和ApplicationReadyEvent整个应用才算启动就绪。这个框架能体现你没把Spring Boot当成黑盒也知道它和Spring Framework的边界。7.2 面试中的加分细节能在表述里带出下面几个点的候选人我通常会多给一些加分一是DeferredImportSelector的作用。自动配置类为什么晚于普通配置类加载因为要等用户配置处理完避免ConditionalOnMissingBean判断过早失效。二是AutoConfiguration.imports和spring.factories的区别。能讲出新版本为什么要换文件格式说明关注过版本演进。三是内嵌Web容器的启动并不是Tomcat独立进程而是Spring容器onRefresh阶段通过ServletWebServerFactory创建的生命周期随Spring容器走。四是SpringApplicationRunListener和SpringApplicationRunListeners的关系它是Spring Boot自己定义的运行过程监听机制和Spring的事件监听器是两套东西但监听器会通过EventPublishingRunListener转发成Spring事件。五是ApplicationReadyEvent和ApplicationRunner、CommandLineRunner的执行顺序以及它们在灰度发布、预热缓存场景里的应用。7.3 常见追问与应答面试官经常会顺着你的回答往下问这三个方向第一个自动配置类是如何被加载的回答思路是被Import导入的AutoConfigurationImportSelector读取imports文件经过过滤、排序、去重后注册为BeanDefinition再由条件注解逐个决定是否生效。第二个如果自动配置不生效你怎么办回答思路是看--debug报告或者/actuator/conditions判断是条件不满足、被排除、还是用户Bean冲突。第三个Spring Boot启动大概分几个阶段回答思路是Environment准备、Context创建、Context刷新、Web容器启动、Runner执行、事件发布这几个阶段再补充每个阶段的关键组件。我个人在实际排查和面试中还有一个习惯尽量自己动手在项目里写一个极简自动配置类然后靠/actuator/conditions观察它有没有被加载。这样Run点基础再复杂的问题也能落地因为你对启动的每个环节都有了亲自动过的感知。最后再分享一个小技巧每次启动都用--debug看一次自动配置报告别嫌日志长。在我看来看明白Positive matches和Negative matches的过程比背十遍源码更能建立对Spring Boot启动原理和组件的真实理解。