Spring Boot 核心组件详解:从启动引导到自动配置的完整拆解
Spring Boot 核心组件详解:从启动引导到自动配置的完整拆解
环境基线:Java 17 · Spring Boot 3.4.x · Maven 3.9 · 内嵌 Tomcat 10.1
很多人用 Spring Boot 三五年,依然说不清「为什么加一个 starter 就能跑起来」「@SpringBootApplication 到底做了什么」「我的配置为什么被另一个配置覆盖了」。这几个问题的答案,几乎构成了 Spring Boot 的全部核心机制。本文不做 API 罗列,而是沿着一次启动的完整链路,把核心组件逐个拆开。
一、先看全景:Spring Boot 到底由哪些组件构成
如果把 Spring Boot 看成一个产品,它真正的价值不是「框架」,而是在 Spring 之上加的一层装配与约定。这层装配由下面这些组件协作完成:
| 组件 | 职责 | 关键类 / 注解 |
|---|---|---|
SpringApplication |
启动引导器,编排整个启动流程 | SpringApplication.run() |
@SpringBootApplication |
组合注解,开启自动配置与组件扫描 | @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan |
| 自动配置(Auto-Configuration) | 按条件自动注册 Bean | AutoConfigurationImportSelector |
| 条件装配(Condition) | 决定自动配置是否生效 | @ConditionalOnClass 等 |
| Starter | 依赖聚合,屏蔽版本冲突 | spring-boot-starter-* |
| 配置体系(Config Data) | 多来源属性的加载与绑定 | application.yml、@ConfigurationProperties |
| 内嵌容器 | 免部署直接运行 | TomcatServletWebServerFactory |
| Actuator | 生产级可观测能力 | /actuator/health、Micrometer |
| 日志抽象 | 统一日志门面与实现 | Logback + logback-spring.xml |
| 扩展点 | 事件、生命周期、后置处理 | ApplicationListener、BeanPostProcessor |
理解顺序建议是:启动流程 → 自动配置 → 条件装配 → 配置绑定 → 容器与 Web → 观测与扩展。前四者是「它为什么能自动跑」,后面是「跑起来之后能干什么」。
二、@SpringBootApplication 的真相
这个注解看起来是一个,其实是三个:
@SpringBootConfiguration // 本质是 @Configuration,标记主配置类
@EnableAutoConfiguration // 开启自动配置,核心中的核心
@ComponentScan // 扫描当前包及子包
public @interface SpringBootApplication {
}
拆开之后,三个注解各自负责一件事:
@SpringBootConfiguration:继承自@Configuration,声明这是一个配置类。它的存在是为了让 Spring Boot 在测试中能定位到主类(@SpringBootTest靠它找入口)。@EnableAutoConfiguration:通过@Import(AutoConfigurationImportSelector.class)把「第三方 jar 里声明好的配置类」批量导入容器。@ComponentScan:默认扫描主类所在包及其子包。这也是为什么主类必须放在根包下——放太深,别的包扫不到;放在默认包(无 package),会扫描整个 classpath 下的所有类,启动直接爆炸。
一个常见误区:@ComponentScan 和自动配置是两套完全独立的机制。前者扫你写的 @Component,后者加载 jar 里预置的配置类,二者互不相干,只是恰好被同一个注解聚合在一起。
三、SpringApplication:启动流程的骨架
SpringApplication.run() 表面上只有一行,内部却是十几个步骤的有序编排。删繁就简后,主干是这样的:
public ConfigurableApplicationContext run(String... args) {
// 1. 启动计时与引导上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
// 2. 发布 ApplicationStartingEvent
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, mainApplicationClass);
// 3. 准备环境:加载配置文件、激活 profile、发布 EnvironmentPrepared 事件
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, args);
// 4. 打印 Banner
Banner printedBanner = printBanner(environment);
// 5. 创建 ApplicationContext(Servlet / Reactive / 普通三种)
context = createApplicationContext();
context.setApplicationStartup(applicationStartup);
// 6. 准备上下文:设置环境、执行 Initializer、加载主配置类、发布 Prepared 事件
prepareContext(bootstrapContext, context, environment, listeners, args, printedBanner);
// 7. 刷新容器:Bean 实例化、自动配置生效、内嵌容器启动
refreshContext(context);
// 8. 刷新后处理(已废弃的钩子,保留兼容)
afterRefresh(context, args);
// 9. 发布 ApplicationStartedEvent
listeners.started(context, startupTime);
// 10. 执行 Runner(ApplicationRunner / CommandLineRunner)
callRunners(context, args);
// 11. 发布 ApplicationReadyEvent,应用就绪
listeners.ready(context, readyTime);
return context;
}
其中有三个关键点值得单独说明。
3.1 上下文的类型是在运行时决定的
createApplicationContext() 并不是固定创建 AnnotationConfigServletWebServerApplicationContext,而是根据 WebApplicationType 推断:
| 推断结果 | 触发条件 | 上下文类型 |
|---|---|---|
SERVLET |
存在 jakarta.servlet.Servlet 且未显式设置 reactive |
AnnotationConfigServletWebServerApplicationContext |
REACTIVE |
仅存在 WebFlux 相关类 | AnnotationConfigReactiveWebServerApplicationContext |
NONE |
两者都不存在 | AnnotationConfigApplicationContext |
这也解释了为什么只引 spring-boot-starter-webflux 时 Tomcat 不会启动——上下文类型根本就不是 Servlet 的。
3.2 事件机制是启动流程的「观察窗口」
整个启动过程会在固定节点发布事件,这是最干净的扩展入口:
| 事件 | 发布时机 | 典型用途 |
|---|---|---|
ApplicationStartingEvent |
上下文创建之前 | 注册监听器、初始化埋点 |
ApplicationEnvironmentPreparedEvent |
Environment 就绪、上下文未创建 | 动态修改配置源 |
ApplicationContextInitializedEvent |
上下文创建、Bean 定义加载之前 | 编程式注册 Bean 定义 |
ApplicationPreparedEvent |
Bean 定义加载完成、refresh 之前 | 调整 BeanDefinition |
ApplicationStartedEvent |
refresh 完成、Runner 执行前 | 容器已可用,开始统计耗时 |
ApplicationReadyEvent |
Runner 执行完成 | 对外声明「可以接流量了」 |
ApplicationFailedEvent |
启动失败 | 上报失败原因、优雅退出 |
注意:ApplicationStartedEvent 之后、ApplicationReadyEvent 之前,中间还会穿插 AvailabilityChangeEvent(LivenessState.CORRECT 与 ReadinessState.ACCEPTING_TRAFFIC),这是 Kubernetes 探针能读到状态的底层依据。
3.3 Runner 与「启动完成」的边界
ApplicationRunner 和 CommandLineRunner 在 ApplicationReadyEvent 之前执行。也就是说,只要你的 Runner 还没跑完,Kubernetes 的 readiness 探针就不会放流量进来——这是做缓存预热、字典加载的理想位置。
@Component
public class CacheWarmUpRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// 此时容器完全可用,但尚未对外标记 ready
}
}
反过来说,Runner 里千万别做长耗时阻塞操作,否则整个应用的「就绪时间」会被拉长,滚动发布时会明显变慢。
四、自动配置:Spring Boot 的灵魂
自动配置回答的问题是:我只是引了一个依赖,为什么数据源、事务管理器、MVC 就都配好了?
4.1 从 spring.factories 到 AutoConfiguration.imports
在 Spring Boot 2.7 之前,自动配置类的清单写在 META-INF/spring.factories 里:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.FooAutoConfiguration,\
com.example.BarAutoConfiguration
2.7 引入了专用文件,3.0 起彻底不再读取 spring.factories 中的自动配置声明:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件内容是一行一个全限定名:
com.example.greeting.GreetingAutoConfiguration
这是 3.x 升级时最容易踩的坑:老项目的自定义 starter 升到 3.x 后自动配置静默失效,不报错、不提示,只是 Bean 没了。原因是 spring.factories 里的那段配置被完全忽略,必须迁移到新文件。
加载链路如下:
@EnableAutoConfiguration
→ @Import(AutoConfigurationImportSelector.class)
→ AutoConfigurationImportSelector#selectImports
→ ImportCandidates.load(AutoConfiguration.class, classLoader)
→ 读取所有 jar 的 AutoConfiguration.imports
→ 去掉 @AutoConfigureAfter / @AutoConfigureBefore 排序
→ 逐个走条件装配过滤
4.2 条件装配:自动配置的「开关」
导入的配置类不是无条件生效的,每个自动配置类头上都有一堆条件注解。以数据源为例(简化):
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration {
}
常用的条件注解及其含义:
| 注解 | 生效条件 | 典型场景 |
|---|---|---|
@ConditionalOnClass |
classpath 存在指定类 | 引了依赖才装配 |
@ConditionalOnMissingBean |
容器中不存在指定 Bean | 允许用户覆盖 |
@ConditionalOnBean |
容器中存在指定 Bean | 依赖其他组件时装配 |
@ConditionalOnSingleCandidate |
指定类型只有一个或首选 Bean | 自动装配唯一数据源 |
@ConditionalOnProperty |
配置项匹配指定值 | @ConditionalOnProperty(prefix="demo", name="enabled", havingValue="true", matchIfMissing=true) |
@ConditionalOnWebApplication |
当前是 Web 应用 | 只在 Web 环境注册 Filter |
@ConditionalOnResource |
指定资源存在 | 有配置文件才启用 |
@ConditionalOnExpression |
SpEL 表达式为真 | 复杂条件 |
@ConditionalOnMissingBean 是 Spring Boot 最优雅的设计之一:它把「默认行为」和「用户覆盖」统一在同一个机制里。你只要自己声明一个同类型的 Bean,官方的那份自动退让,无需任何配置开关。
反过来,自定义自动配置里的 @ConditionalOnMissingBean 必须慎用:条件求值依赖 Bean 定义的注册顺序,写错位置会出现「有时生效有时不生效」的诡异现象。
4.3 手写一个 Starter,把机制跑一遍
这是理解自动配置最快的方式。目标:注入 greeting 前缀的配置,提供一个可覆盖的 GreetingService。
工程一:greeting-spring-boot-autoconfigure(放逻辑)
@ConfigurationProperties(prefix = "greeting")
public class GreetingProperties {
private String prefix = "Hello";
private String suffix = "!";
// getter / setter 省略
}
@AutoConfiguration
@ConditionalOnClass(GreetingService.class)
@EnableConfigurationProperties(GreetingProperties.class)
@ConditionalOnProperty(prefix = "greeting", name = "enabled",
havingValue = "true", matchIfMissing = true)
public class GreetingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GreetingService greetingService(GreetingProperties props) {
return new GreetingService(props.getPrefix(), props.getSuffix());
}
}
src/main/resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件里只写一行:com.example.greeting.GreetingAutoConfiguration。
工程二:greeting-spring-boot-starter(只做依赖聚合)
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>greeting-spring-boot-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
使用者只需要引 greeting-spring-boot-starter,配置 greeting.prefix=你好 即可生效。
这里有两条硬性经验:
- 自动配置模块里绝对不要写
@ComponentScan。一旦写了,它会扫描使用者项目的包路径,把别人的 Bean 误装进来,引发难以排查的重复注册。 - 命名规范是约定,不是建议。官方 starter 叫
spring-boot-starter-xxx,第三方必须叫xxx-spring-boot-starter,避免与官方命名冲突。
五、Starter 体系:依赖聚合的艺术
Starter 本身几乎不含代码,它的全部职责是把一组经过验证的依赖坐标聚合起来,配合 Spring Boot 的 BOM 统一版本。
| Starter | 引入内容 | 场景 |
|---|---|---|
spring-boot-starter |
核心、日志、自动配置 | 所有项目的基础 |
spring-boot-starter-web |
MVC + 内嵌 Tomcat + JSON | 传统 Web 应用 |
spring-boot-starter-webflux |
Reactor + Netty | 响应式 / 高并发网关 |
spring-boot-starter-data-jpa |
JPA + Hibernate + 事务 | 关系型数据库 |
spring-boot-starter-data-redis |
Lettuce 客户端 | 缓存 |
spring-boot-starter-security |
认证授权 | 安全控制 |
spring-boot-starter-actuator |
健康检查、指标 | 生产可观测 |
spring-boot-starter-validation |
Hibernate Validator | 参数校验 |
spring-boot-starter-test |
JUnit 5、Mockito、AssertJ | 测试 |
关于版本,记住一句话:父 POM 里的 spring-boot-dependencies 已经锁死了所有受管依赖的版本,不要自己再写 <version>。手动指定版本是依赖冲突的头号来源——尤其是当你同时引入 Spring Cloud 时,必须让 Spring Cloud 的 BOM 接管,而不是各写各的版本。
六、配置体系:属性从哪来,到哪去
6.1 加载顺序决定一切
Spring Boot 3.x 的 ConfigDataEnvironmentPostProcessor 会从多个位置加载配置,后加载的覆盖先加载的:
classpath:/ ← 最低
classpath:/config/
file:./
file:./config/
file:./config/*/ ← 最高
配合 profile 后,完整优先级(从低到高)大致是:
SpringApplication.setDefaultProperties()设置的默认值@PropertySource引入的属性application.properties/application.yml(含 profile 变体)RandomValuePropertySource(${random.int})- 操作系统环境变量
- JVM 系统属性(
-Dkey=value) SPRING_APPLICATION_JSON- 命令行参数(
--key=value) - 测试专用注解(
@TestPropertySource、@SpringBootTest(properties=...))
这解释了一个高频疑问:「我在 application.yml 里改了配置为什么没生效?」——大概率是环境变量或命令行参数里已经有同名项,优先级更高。
application-{profile}.yml 的规则是同时加载、profile 覆盖默认,不是二选一。很多人以为激活 prod 后 application.yml 就不加载了,实际上它依然作为基线存在。
6.2 @ConfigurationProperties 优于 @Value
对比一下两种绑定方式:
| 维度 | @Value |
@ConfigurationProperties |
|---|---|---|
| 松散绑定 | 不支持 | 支持(long-property / longProperty) |
| 类型转换 | 需手动处理 | 自动(Duration、DataSize、集合) |
| 校验 | 无 | 配合 @Validated + JSR-303 |
| 元数据提示 | 无 | 编译期生成,IDE 自动补全 |
| 批量绑定 | 需逐个字段写 | 一次绑定整个对象 |
@ConfigurationProperties(prefix = "app.upload")
@Validated
public class UploadProperties {
@NotBlank
private String bucket;
private DataSize maxSize = DataSize.ofMegabytes(10);
private Duration timeout = Duration.ofSeconds(30);
private List<String> allowTypes = List.of("jpg", "png");
// getter / setter
}
DataSize 和 Duration 是 Spring Boot 独有的便利类型,10MB、30s 这样的字面量可以直接绑定,比手写换算可读性高得多。
注册方式有三种:@EnableConfigurationProperties(XxxProperties.class)、@ConfigurationPropertiesScan、或者直接标 @Component + @ConfigurationProperties。前两种更推荐,因为它把配置类与组件扫描解耦。
6.3 松散绑定与元数据
松散绑定的规则是:配置名里的 -、_、大小写差异在绑定时都被归一化。所以下面三种写法等价:
app.upload.max-size=10MB
app.upload.maxSize=10MB
APP_UPLOAD_MAXSIZE=10MB
元数据(additional-spring-configuration-metadata.json)则是给 IDE 用的,用于在你敲 app.upload. 时提示字段和默认值。自定义 starter 若不生成元数据,使用者的开发体验会明显下降。
七、内嵌容器与 Web 组件
7.1 容器是可替换的
spring-boot-starter-web 默认带 Tomcat。想换 Jetty:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
内嵌容器的本质是:ServletWebServerFactory 在 refresh 阶段创建容器并启动,然后把已经初始化好的 DispatcherServlet 注册进去。所以应用「启动完就能访问」——不需要外部容器部署。
需要调优时,实现 WebServerFactoryCustomizer 比写 XML 更现代:
@Component
public class TomcatTuning implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector ->
connector.setProperty("relaxedQueryChars", "[]{}"));
}
}
常用调优项在 server.tomcat.* 下:max-connections、threads.max、accept-count、max-swallow-size。HTTP/2 与 TLS 则通过 server.ssl.* 与 server.http2.enabled 控制。
Spring Boot 3.2 之后还可以开启虚拟线程:
spring.threads.virtual.enabled=true
开启后 Tomcat 的请求处理会自动落到虚拟线程上,对 I/O 密集型接口收益明显,但对 CPU 密集型无益,且需留意 ThreadLocal 与 synchronized 的 pinning 问题。
7.2 Filter、Interceptor、@ControllerAdvice 的分工
三个东西都能「拦截请求」,但层级完全不同:
| 组件 | 所属层 | 能拿到什么 | 典型用途 |
|---|---|---|---|
Filter |
Servlet 规范 | 原始 ServletRequest / Response |
编码、跨域、请求体包装、链路 ID |
HandlerInterceptor |
Spring MVC | HandlerMethod、ModelAndView |
权限校验、日志、耗时统计 |
@ControllerAdvice |
Spring MVC | 异常对象、绑定结果 | 全局异常、统一响应、数据预处理 |
关键区别:Filter 在 DispatcherServlet 之前执行,拿不到 Handler 信息;Interceptor 在 DispatcherServlet 内部、Handler 匹配之后执行。所以「需要知道调用的是哪个 Controller 方法」的逻辑只能放在 Interceptor 里。
Filter 的注册与顺序控制:
@Bean
public FilterRegistrationBean<TraceFilter> traceFilter() {
FilterRegistrationBean<TraceFilter> bean = new FilterRegistrationBean<>(new TraceFilter());
bean.addUrlPatterns("/*");
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
直接给 Filter 加 @Component 也能生效,但顺序不可控(默认 Ordered.LOWEST_PRECEDENCE),生产环境建议统一用 FilterRegistrationBean 显式声明顺序。
八、Actuator:生产环境的眼睛
引入 spring-boot-starter-actuator 后,默认只暴露 /actuator/health,其余端点需显式开启:
management:
endpoints:
web:
exposure:
include: health,info,metrics,loggers
endpoint:
health:
show-details: when-authorized
probes:
enabled: true # 开启 liveness / readiness 探针
核心端点:
| 端点 | 作用 | 默认暴露 |
|---|---|---|
/health |
健康状态聚合 | 是 |
/info |
应用元信息 | 是 |
/metrics |
Micrometer 指标 | 否 |
/loggers |
动态调整日志级别 | 否 |
/beans |
容器内所有 Bean | 否 |
/mappings |
所有请求映射 | 否 |
/threaddump |
线程栈 | 否 |
/heapdump |
堆转储 | 否 |
/shutdown |
优雅停机 | 否(且默认禁用) |
两个必须注意的安全点:
/actuator绝不能直接暴露在公网。/env、/heapdump、/beans泄露的信息足以支撑一次完整攻击。生产环境要么走内网端口(management.server.port独立端口),要么接 Spring Security 做鉴权。/shutdown默认关闭是有原因的。需要它时配合 Kubernetes 的preStop钩子使用,别图省事直接打开。
指标层面,Actuator 通过 Micrometer 做门面,只需引入对应 registry(Prometheus、InfluxDB 等)即可上报:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
业务自定义指标:
@Service
public class OrderService {
private final Counter orderCounter;
public OrderService(MeterRegistry registry) {
this.orderCounter = Counter.builder("order.created")
.description("下单数量")
.tag("channel", "app")
.register(registry);
}
public void create() {
orderCounter.increment();
}
}
九、日志:默认 Logback,但被 Spring 改造过
Spring Boot 默认使用 SLF4J + Logback,并提供 logback-spring.xml 这个带 Spring 扩展的配置文件。它与原生 logback.xml 的区别在于支持:
<springProfile name="dev">
<root level="DEBUG"/>
</springProfile>
<springProperty scope="context" name="appName"
source="spring.application.name" defaultValue="unknown"/>
<springProfile> 让你可以用同一份日志配置区分环境,<springProperty> 则把 Spring 配置注入日志变量——这两点是原生 Logback 做不到的。
换 Log4j2 需要先排除默认实现:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
运行期改级别不用重启,/actuator/loggers 端点直接支持:
curl -X POST http://localhost:8080/actuator/loggers/com.example \
-H "Content-Type: application/json" \
-d '{"configuredLevel":"DEBUG"}'
十、扩展点清单:Spring Boot 留给你的接口
除了前面的事件机制,还有几类扩展点值得放在手边:
| 扩展点 | 触发时机 | 典型用途 |
|---|---|---|
BeanFactoryPostProcessor |
Bean 定义加载后、实例化前 | 修改 BeanDefinition |
BeanPostProcessor |
每个 Bean 初始化前后 | 代理增强、注解处理 |
@PostConstruct / InitializingBean |
Bean 属性注入完成后 | 单 Bean 初始化 |
SmartInitializingSingleton |
所有单例创建完成 | 依赖全量 Bean 的初始化 |
ContextRefreshedEvent |
容器刷新完成 | 缓存预热 |
ApplicationRunner / CommandLineRunner |
启动末尾、ready 之前 | 一次性任务 |
SmartLifecycle |
容器启停阶段 | 长连接、消息消费的启停 |
GracefulShutdown |
停机时 | 等待在途请求完成 |
优雅停机是 3.x 的重点能力,开启方式:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
开启后,停机时会先拒绝新请求,再等待正在处理的请求完成(最多 30 秒),最后关闭容器。配合 Kubernetes 的 preStop 睡眠,可以基本消除滚动发布时的 502。
十一、启动时序速查
把前面的内容压缩成一图,方便对照排查问题:
main()
└─ SpringApplication.run()
├─ 创建 BootstrapContext
├─ 发布 StartingEvent
├─ 准备 Environment(加载 application.yml / 激活 profile)
├─ 发布 EnvironmentPreparedEvent
├─ 创建 ApplicationContext(按 WebApplicationType 推断)
├─ 加载主配置类 + 自动配置类(AutoConfiguration.imports)
├─ 条件装配过滤 → 得到最终生效的配置类清单
├─ 发布 ContextInitializedEvent / PreparedEvent
├─ refresh()
│ ├─ BeanFactory 后置处理
│ ├─ 实例化单例 Bean(含 BeanPostProcessor 增强)
│ ├─ 内嵌容器启动,注册 DispatcherServlet
│ └─ 发布 ContextRefreshedEvent
├─ 发布 StartedEvent + LivenessState.CORRECT
├─ 执行 ApplicationRunner / CommandLineRunner
└─ 发布 ReadyEvent + ReadinessState.ACCEPTING_TRAFFIC
排查启动问题时的对应关系:
- Bean 没被创建 → 检查自动配置是否生效(
/actuator/beans、启动加--debug看条件评估报告) - 配置不生效 → 检查优先级链,用
/actuator/env看最终值和来源 - 启动慢 → 看
StartedEvent与ReadyEvent的时间差,Runner 往往是元凶 - 容器起不来 → 检查
WebApplicationType推断结果与端口占用
--debug 是排查自动配置最有效的开关,它会在启动日志里输出 CONDITIONS EVALUATION REPORT,明确列出每个自动配置类「匹配 / 未匹配」以及未匹配的具体原因。
十二、常见坑与最佳实践
12.1 五个高频坑
- 主类放在默认包。启动时报「扫描到大量无关 Bean」或直接失败。主类必须位于有 package 声明的根包。
- 3.x 升级后自定义 starter 失效。
spring.factories里的EnableAutoConfiguration不再被读取,必须迁移到AutoConfiguration.imports。 - 循环依赖。Spring Boot 2.6 起默认禁止循环依赖,3.x 沿用。遇到就重构,而不是去开
spring.main.allow-circular-references。 @ConfigurationProperties没生效。忘了@EnableConfigurationProperties或@ConfigurationPropertiesScan,属性类根本没人注册。- Actuator 全量暴露。
exposure.include=*图方便,结果/env把数据库密码以明文吐了出来。
12.2 值得坚持的几条
- 配置类统一加
proxyBeanMethods = false。Spring Boot 自己的自动配置都这么写,能减少 CGLIB 代理开销,也避免方法间调用产生意外的单例语义。 @Value只用于零星的一两个值,成组的配置一律用@ConfigurationProperties。- 自定义自动配置类用
@AutoConfiguration,并配合@AutoConfigureAfter/@AutoConfigureBefore明确顺序,别依赖 classpath 顺序。 - 排序一律显式声明:Filter、AOP、Listener 都存在隐式顺序,生产代码里不要赌。
- 对外接口的默认实现永远加
@ConditionalOnMissingBean,把覆盖权交给使用者。 - 把
/actuator/health与/actuator/prometheus接入探针和监控,这是区分「能跑」和「可运维」的分界线。
十三、结语
Spring Boot 的核心其实只有一句话:把「根据 classpath 和配置推断出该装配什么」这件事做到了极致。
SpringApplication负责编排启动顺序;@EnableAutoConfiguration+AutoConfiguration.imports负责找到候选配置;- 条件注解负责在运行时筛掉不合适的候选;
- Starter 负责把依赖和版本收敛成一个坐标;
- 配置体系负责把外部输入绑定成强类型对象;
- Actuator 与扩展点负责让这套体系在生产环境可观测、可干预。
理解了这条链路,再看那些「加个依赖就好了」的魔法,就不再是魔法了。遇到问题时,你会知道该去 /actuator/env 看优先级,该开 --debug 看条件报告,该怀疑 Runner 拖慢了就绪时间——而不是凭感觉改配置。
参考:Spring Boot 官方文档(Reference Guide, 3.4.x)· Spring Framework 6.x 文档
- 点赞
- 收藏
- 关注作者
评论(0)