为什么加了 starter 就能用
很多人第一次用 Spring Boot 时会有两个疑问:项目里只加了 spring-boot-starter-web 一个依赖,为什么就能直接注入 ObjectMapper、DataSource,甚至 ServletWebServerFactory?自己写一个同类型的 @Bean 之后,原来那个 Bean 为什么就"让位"了?反过来,把某个 starter 排除掉,启动又立刻报 NoSuchBeanDefinitionException。
这几个现象背后都是同一套机制:自动配置。它并不神秘,本质是"提前写好一批配置类,启动时按条件筛选,满足条件才注册 Bean"。把这套流程弄清楚,"这个 Bean 从哪来""我的配置为什么不生效"这类问题就不用再靠猜。
自动配置从哪里开始
@SpringBootApplication 是一个组合注解,拆开看主要是三个:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
// 省略 excludeFilters 等属性
}
真正负责自动配置的是 @EnableAutoConfiguration,它通过 @Import(AutoConfigurationImportSelector.class) 把候选配置类收集进来。收集的来源随版本变过:
| Spring Boot 版本 | 候选配置来源 |
|---|---|
| 2.6 及以前 | 各 jar 的 META-INF/spring.factories 中 EnableAutoConfiguration 键 |
| 2.7 | 新增 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,两种都认 |
| 3.0 及以后 | 只认新的 .imports 文件,spring.factories 不再用于自动配置 |
所以从 2.7 升级到 3.0 时,如果公司内部 starter 还在用 spring.factories 注册自动配置,升级后会出现"依赖在、Bean 没了"的情况,这是很典型的一次断点。
收集到的候选类动辄上百个,但它们不会全部生效。每个自动配置类头上都挂着一串条件注解,启动时逐个判断,只有条件成立才注册里面的 @Bean。
条件注解决定装不装
常见条件注解可以按用途记:
| 注解 | 作用 | 典型场景 |
|---|---|---|
@ConditionalOnClass |
类路径存在指定类 | 有 DataSource 才装配数据源 |
@ConditionalOnMissingBean |
容器里没有同类型 Bean 才生效 | 给用户留出覆盖入口 |
@ConditionalOnProperty |
配置项为指定值才生效 | xxx.enabled=true |
@ConditionalOnWebApplication |
当前是 Web 应用 | 只在 Servlet 环境装配 |
@ConditionalOnBean |
容器里已有某 Bean | 依赖其他自动配置的产物 |
这也是"自定义 Bean 优先"的原因:自动配置类里大量使用 @ConditionalOnMissingBean,你只要在能被扫描到的配置类里声明同类型 Bean,自动配置那一段就会自动跳过。
除此之外,自动配置类之间还有先后关系,常用 @AutoConfigureBefore、@AutoConfigureAfter 标注。比如 Web 相关配置要等 Servlet 容器就绪,数据源相关配置要排在前面的基础设施之后。顺序错了,条件判断时"依赖的 Bean 还没注册",配置就悄悄不生效。
排查:Bean 到底从哪来
出问题时不要先改代码,先让 Spring 把判断结果打出来。在 application.yml 里加一行:
debug: true
或者在启动命令上加 --debug,控制台会输出 Condition Evaluation Report,里面分成 Positive matches(生效的自动配置)和 Negative matches(没生效的以及原因)。如果引了 actuator,还可以直接访问:
curl http://localhost:8080/actuator/conditions
想确认某个自动配置是否被排除,看 Negative matches 里的 Exclusions 部分即可。定位到具体类以后,再回头看它的条件注解,一般就能解释"为什么没装"。
常见坑
1. 自定义 Bean 没生效
先确认这个 @Configuration 类是否在启动类所在包或子包下。@ComponentScan 默认只扫启动类所在包,多模块项目里把配置类放到 com.example.common、启动类却在 com.example.app,就会出现"代码写了但没注册"。
2. 排除自动配置后忘了补 Bean
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
排除只是"不装配",不是"帮你配好"。排除数据源自动配置后,如果代码里还有 JdbcTemplate 依赖,启动照样失败,需要自己提供 DataSource。
3. 升级 Boot 3 后内部 starter 失效
前面说过,3.0 只认 AutoConfiguration.imports。内部 starter 要么补文件,要么升级依赖版本,不能只改 spring-boot.version 就上线。
4. 多个 BOM 版本打架
父 POM 引了 Spring Boot 2.7 的 BOM,子模块又单独指定了 3.0 的 starter 版本,编译能过,启动时报 NoSuchMethodError 或条件判断异常。统一由父 POM 管理版本,子模块不要写版本号。
5. 条件"看起来对但没生效"
@ConditionalOnProperty 的 havingValue 区分大小写,matchIfMissing 默认是 false。配置写成 enabled: TRUE、代码里写 havingValue = "true",条件就不成立。这种问题只能靠 --debug 的报告发现。
小结
自动配置可以概括成三步:启动时收集候选配置类,按条件注解过滤,把通过的配置类里的 Bean 注册进容器。排查时按"候选有没有被收集、条件是否满足、用户 Bean 是否覆盖"的顺序看,基本不会跑偏。
评论