Spring Boot 自动配置与扩展点全过程
很多人学 Spring Boot 只记住一句“约定优于配置”,但真正做项目、排查问题、回答面试时,这句话远远不够。你必须知道:
- 自动配置类是怎么被发现的。
- 条件注解是怎么决定 Bean 是否创建的。
- Starter 为什么引入后能开箱即用。
- 用户自定义 Bean 为什么能覆盖默认 Bean。
- 各种扩展点分别插在启动生命周期的哪一步。
- 项目中到底应该用哪个扩展点,而不是乱用。
这一页把自动配置、Starter、生命周期、扩展点、排查方法串成一条完整链路。面试页只负责背标准回答,本页负责让你真正理解为什么。
这一页解决哪些问题
| 问题 | 学完后应该能说清 |
|---|---|
| Spring Boot 自动配置到底是什么 | 候选配置类 + 条件判断 + BeanDefinition + IOC 创建 |
@SpringBootApplication 为什么能启动项目 | 它组合了配置类、组件扫描、自动配置三个能力 |
| Boot 2 和 Boot 3 自动配置文件有什么区别 | Boot 2.6及更早常见 spring.factories;Boot 2.7已支持imports;Boot 3使用 AutoConfiguration.imports |
| Starter 为什么能开箱即用 | Starter 负责依赖入口,autoconfigure 负责默认装配,条件注解负责是否生效 |
| 扩展点怎么选 | 先判断要干预启动前、Bean 定义、Bean 初始化、启动完成、Web 请求还是监控诊断 |
| 自动配置没生效怎么排查 | 看 conditions 报告、classpath、配置项、exclude、用户 Bean、Bean 创建异常 |
总体链路
先看一张不要画太宽的总图。Spring Boot 的启动不是一步完成,而是一层层推进。
flowchart TD
A["SpringApplication.run"] --> B["准备 Environment"]
B --> C["创建 ApplicationContext"]
C --> D["解析启动类"]
D --> E["@EnableAutoConfiguration"]
E --> F["读取自动配置候选类"]
F --> G["条件注解筛选"]
G --> H["注册 BeanDefinition"]
H --> I["refresh 创建 Bean"]
I --> J["启动 WebServer"]
J --> K["发布启动完成事件"]
K --> L["执行 Runner"]这张图里每个节点都能扩展,但扩展点不同。如果你把配置解密放到 Runner,自动配置早就读完配置了;如果你在 BeanFactoryPostProcessor 里 getBean(),Bean 可能会过早创建,生命周期就乱了。
第一步:@SpringBootApplication 做了什么
@SpringBootApplication 不是一个单纯的“启动按钮”,它是组合注解。
flowchart TD
A["@SpringBootApplication"] --> B["@SpringBootConfiguration"]
A --> C["@ComponentScan"]
A --> D["@EnableAutoConfiguration"]
B --> E["把启动类当配置类"]
C --> F["扫描 Controller、Service、Component"]
D --> G["导入自动配置候选类"]| 组成 | 作用 | 如果没有会怎样 |
|---|---|---|
@SpringBootConfiguration | 本质上是配置类,启动类可以声明 @Bean | 启动类不再作为配置类参与 Spring 解析 |
@ComponentScan | 扫描启动类所在包及子包 | Controller、Service、Component 不会自动进容器 |
@EnableAutoConfiguration | 开启自动配置导入 | Web、数据源、Redis、Jackson、Tomcat 等默认能力不会自动装配 |
所以,一个最小 Web 项目能启动,不是因为 Spring Boot “猜到你要 Web”,而是因为:
- 你引入了
spring-boot-starter-web。 - classpath 中出现了 Spring MVC、Jackson、Tomcat。
- Web 相关自动配置类进入候选列表。
- 条件注解判断成立。
- 默认的 Servlet、MVC、JSON、内嵌容器 Bean 被注册。
第二步:自动配置候选类从哪里来
自动配置不是在启动时全盘扫描所有 jar 包。它读取的是约定位置的清单。
Boot 2 常见方式
文件路径:
META-INF/spring.factories内容示例:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.collect.CollectClientAutoConfiguration,\
com.example.collect.CollectMetricsAutoConfiguration很多老项目运行在 JDK 8、Spring Boot 2.x,这种写法非常常见。不能因为现在新项目常用 Boot 3,就把 spring.factories 当成错误。
Boot 3 推荐方式
文件路径:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容示例:
com.example.collect.CollectClientAutoConfiguration
com.example.collect.CollectMetricsAutoConfiguration两者区别
| 对比项 | Boot 2 spring.factories | Boot 3 AutoConfiguration.imports |
|---|---|---|
| 声明方式 | key-value | 每行一个配置类 |
| 可读性 | 自动配置多时很长 | 更清晰 |
| 常见环境 | JDK 8、Boot 2 老项目 | JDK 17+、Boot 3 新项目 |
| 面试重点 | 能看懂老项目机制 | 能说明新机制为什么更直接 |
第三步:AutoConfigurationImportSelector 到底做了什么
@EnableAutoConfiguration 内部会通过 @Import 引入选择器。这个选择器不是直接创建 Bean,而是返回一批自动配置类名,让 Spring 继续解析它们。
flowchart TD
A["@EnableAutoConfiguration"] --> B["AutoConfigurationImportSelector"]
B --> C["读取自动配置清单"]
C --> D["加载候选类名"]
D --> E["处理 exclude"]
E --> F["去重"]
F --> G["按顺序排序"]
G --> H["返回给 Spring 解析"]关键理解:
AutoConfigurationImportSelector做的是“选择配置类”,不是“创建业务对象”。- 自动配置类最终仍然要被 Spring 当作配置类解析。
- 自动配置类里的
@Bean方法最终会变成 BeanDefinition。 - Bean 的实例化仍然发生在
ApplicationContext.refresh()阶段。
第四步:条件注解为什么是自动配置核心
自动配置类进入候选列表,并不代表一定生效。真正决定是否创建默认 Bean 的,是条件注解。
flowchart TD
A["候选自动配置类"] --> B{"类路径是否有依赖"}
B -- "否" --> X["跳过"]
B -- "是" --> C{"配置开关是否开启"}
C -- "否" --> X
C -- "是" --> D{"容器中是否已有用户 Bean"}
D -- "有" --> E["默认 Bean 让位"]
D -- "没有" --> F["注册默认 Bean"]| 注解 | 判断什么 | 商业项目常见用法 |
|---|---|---|
@ConditionalOnClass | classpath 中存在某个类 | 引入 Redis 依赖才配置 RedisTemplate |
@ConditionalOnMissingClass | classpath 中不存在某个类 | 某实现不存在时启用备用实现 |
@ConditionalOnBean | 容器中存在某个 Bean | 有 DataSource 后再创建 JdbcTemplate |
@ConditionalOnMissingBean | 容器中不存在某个 Bean | 用户没自定义时提供默认客户端 |
@ConditionalOnProperty | 配置项符合条件 | 用 enabled=false 关闭某个 Starter |
@ConditionalOnWebApplication | 当前是 Web 应用 | 只在 Web 服务中注册 MVC 组件 |
@ConditionalOnResource | 存在某资源 | 根据模板、证书、配置文件启用能力 |
为什么 @ConditionalOnMissingBean 特别重要
假设自动配置强行创建一个 CollectClient,业务项目就很难替换它。正确写法是:
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@Bean
@ConditionalOnMissingBean
public CollectClient collectClient(CollectClientProperties properties) {
return new CollectClient(properties.getBaseUrl(), properties.getTimeout());
}业务项目自定义:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CustomCollectClientConfig {
@Bean
public CollectClient collectClient() {
return new CollectClient("https://custom.example.com", 1000);
}
}这时默认 Bean 不会创建。这个原则非常重要:
框架提供默认值,业务项目保留接管权。
如果没有这个原则,公共 Starter 会变成“强侵入框架”,一引入就把业务自己的实现挤掉。
第五步:自动配置和 IOC 的关系
自动配置不是绕过 Spring IOC 的特殊魔法。它只是把一批配置类自动导入,后面仍然走 IOC 标准流程。
flowchart TD
A["自动配置类"] --> B["@Bean 方法"]
B --> C["解析成 BeanDefinition"]
C --> D["放入 BeanDefinitionRegistry"]
D --> E["BeanFactoryPostProcessor 可修改定义"]
E --> F["实例化 Bean"]
F --> G["属性填充"]
G --> H["BeanPostProcessor 初始化前后增强"]
H --> I["单例 Bean 可用"]这也是为什么学 Spring Boot 不能跳过 Spring:
| Spring Boot 现象 | 背后的 Spring 原理 |
|---|---|
| 自动配置注册默认 Bean | 配置类解析、BeanDefinition |
| 用户 Bean 覆盖默认 Bean | BeanDefinition 条件判断和 IOC 容器 |
| AOP、事务、监控增强 | BeanPostProcessor 创建代理 |
| 配置绑定到对象 | Environment、Binder、类型转换 |
| 启动后执行 Runner | 容器刷新完成后的回调 |
第六步:Starter 为什么能开箱即用
Starter 不是一个神秘包,它通常由两部分组成:
| 模块 | 职责 |
|---|---|
xxx-spring-boot-starter | 聚合依赖,让业务项目只引入一个入口 |
xxx-spring-boot-autoconfigure | 放自动配置类、配置属性类、条件判断、默认 Bean |
工作链路:
flowchart TD
A["业务项目引入 Starter"] --> B["依赖进入 classpath"]
B --> C["Boot 读取自动配置声明"]
C --> D["加载 AutoConfiguration"]
D --> E{"条件是否满足"}
E -- "否" --> F["跳过"]
E -- "是" --> G["绑定配置属性"]
G --> H["注册默认 Bean"]
H --> I["业务代码直接注入使用"]所以 Starter 成功的关键不是“把代码打成 jar”,而是:
- 依赖边界清楚。
- 自动配置类能被 Spring Boot 发现。
- 条件注解写得合理。
- 配置属性可读、可校验。
- 默认 Bean 可被用户覆盖。
- 失败时有清晰排查信息。
自定义采集客户端 Starter Demo
下面以医疗数据采集平台里的“医院接口采集客户端”为例,写一个最小但完整的 Starter 思路。
1. 配置属性类
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "collect.client")
public class CollectClientProperties {
private boolean enabled = true;
private String baseUrl;
private int connectTimeoutMs = 3000;
private int readTimeoutMs = 5000;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public int getConnectTimeoutMs() {
return connectTimeoutMs;
}
public void setConnectTimeoutMs(int connectTimeoutMs) {
this.connectTimeoutMs = connectTimeoutMs;
}
public int getReadTimeoutMs() {
return readTimeoutMs;
}
public void setReadTimeoutMs(int readTimeoutMs) {
this.readTimeoutMs = readTimeoutMs;
}
}2. 客户端类
public class CollectClient {
private final String baseUrl;
private final int connectTimeoutMs;
private final int readTimeoutMs;
public CollectClient(String baseUrl, int connectTimeoutMs, int readTimeoutMs) {
this.baseUrl = baseUrl;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
}
public String fetchPatient(String patientId) {
return "GET " + baseUrl + "/patients/" + patientId
+ ", connectTimeoutMs=" + connectTimeoutMs
+ ", readTimeoutMs=" + readTimeoutMs;
}
}3. 自动配置类
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnClass(CollectClient.class)
@EnableConfigurationProperties(CollectClientProperties.class)
@ConditionalOnProperty(prefix = "collect.client", name = "enabled",
havingValue = "true", matchIfMissing = true)
public class CollectClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CollectClient collectClient(CollectClientProperties properties) {
if (properties.getBaseUrl() == null || properties.getBaseUrl().isEmpty()) {
throw new CollectClientConfigException("collect.client.base-url 不能为空");
}
return new CollectClient(
properties.getBaseUrl(),
properties.getConnectTimeoutMs(),
properties.getReadTimeoutMs()
);
}
}4. Boot 3 注册
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importscom.example.collect.CollectClientAutoConfiguration5. Boot 2 注册
META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.collect.CollectClientAutoConfiguration6. 业务项目配置
collect:
client:
enabled: true
base-url: https://hospital.example.com
connect-timeout-ms: 2000
read-timeout-ms: 80007. 业务项目使用
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PatientCollectController {
private final CollectClient collectClient;
public PatientCollectController(CollectClient collectClient) {
this.collectClient = collectClient;
}
@GetMapping("/collect/patient")
public String collect(@RequestParam String patientId) {
return collectClient.fetchPatient(patientId);
}
}这个 Demo 的执行过程是:
- 业务项目引入 Starter。
CollectClient进入 classpath。- Boot 读取自动配置声明。
@ConditionalOnClass判断通过。collect.client.enabled没配置或为true,判断通过。CollectClientProperties绑定application.yml。- 容器里没有用户自定义
CollectClient,默认 Bean 创建。 - Controller 构造方法注入
CollectClient。
扩展点全景:先看生命周期
扩展点必须按生命周期理解,不能靠背名字。
flowchart TD
A["准备 Environment"] --> B["EnvironmentPostProcessor"]
B --> C["创建 ApplicationContext"]
C --> D["ApplicationContextInitializer"]
D --> E["加载 BeanDefinition"]
E --> F["BeanDefinitionRegistryPostProcessor"]
F --> G["BeanFactoryPostProcessor"]
G --> H["创建和初始化 Bean"]
H --> I["BeanPostProcessor"]
I --> J["启动 WebServer"]
J --> K["ApplicationListener"]
K --> L["ApplicationRunner / CommandLineRunner"]| 阶段 | 扩展点 | 最适合做什么 | 不适合做什么 |
|---|---|---|---|
| Environment 准备 | EnvironmentPostProcessor | 配置解密、加载默认属性、插入属性源 | 注入业务 Bean |
| Context 创建后 | ApplicationContextInitializer | 修改容器、注册早期对象 | 执行业务逻辑 |
| BeanDefinition 阶段 | BeanDefinitionRegistryPostProcessor | 动态注册 BeanDefinition | 创建真实业务对象 |
| BeanFactory 阶段 | BeanFactoryPostProcessor | 修改 BeanDefinition 属性 | 调用 getBean() |
| Bean 初始化阶段 | BeanPostProcessor | 包装 Bean、生成代理、解析注解 | 做全局慢任务 |
| 事件阶段 | ApplicationListener | 监听启动、失败、就绪事件 | 替代所有业务流程 |
| 启动完成后 | ApplicationRunner | 缓存预热、启动检查 | 不可控长任务 |
| Web 请求链路 | Filter、Interceptor、ArgumentResolver | traceId、鉴权、当前用户注入 | 每个 Controller 重复写 |
| 监控诊断 | HealthIndicator、MeterBinder、FailureAnalyzer | 健康检查、指标、友好错误 | 执行重查询 |
EnvironmentPostProcessor:配置读取前的扩展
它在 Bean 创建之前执行,适合处理“后续自动配置要读取的配置”。
flowchart TD
A["读取命令行和环境变量"] --> B["创建 Environment"]
B --> C["执行 EnvironmentPostProcessor"]
C --> D["加入解密后配置或默认配置"]
D --> E["@ConfigurationProperties 绑定"]
E --> F["自动配置条件判断"]Demo:给采集线程池补默认配置。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.HashMap;
import java.util.Map;
public class CollectEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
Map<String, Object> defaults = new HashMap<String, Object>();
defaults.put("collect.thread.core-size", "8");
defaults.put("collect.thread.queue-capacity", "1000");
environment.getPropertySources().addLast(
new MapPropertySource("collectDefaultProperties", defaults)
);
}
}为什么用 addLast:默认值优先级应该低于用户配置。如果用 addFirst,可能把线上显式配置覆盖掉。
适用场景:
| 场景 | 为什么 |
|---|---|
| 配置解密 | 自动配置和属性绑定之前必须看到明文 |
| 默认属性 | 给 Starter 一个兜底值,但不覆盖用户配置 |
| 加载外部配置 | 把外部配置源接入 Spring Environment |
错误用法:
// 错误思路:此时业务 Bean 还没创建
// hospitalApiClient.ping();这里不要做远程调用,也不要拿业务 Bean。否则启动早期就会被外部系统拖慢,甚至整个应用起不来。
BeanDefinitionRegistryPostProcessor:动态注册 Bean 定义
它比普通 BeanFactoryPostProcessor 更早,能向容器注册新的 BeanDefinition。典型场景是框架根据注解、配置、接口动态注册一批客户端或 Mapper。
flowchart TD
A["扫描配置或接口"] --> B["构造 BeanDefinition"]
B --> C["注册到 BeanDefinitionRegistry"]
C --> D["refresh 后续统一创建 Bean"]Demo:根据配置动态注册一个 CollectClient BeanDefinition。
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
public class CollectClientRegistryPostProcessor
implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
BeanDefinition beanDefinition = BeanDefinitionBuilder
.genericBeanDefinition(CollectClient.class)
.addConstructorArgValue("https://hospital.example.com")
.addConstructorArgValue(3000)
.addConstructorArgValue(5000)
.getBeanDefinition();
registry.registerBeanDefinition("hospitalCollectClient", beanDefinition);
}
}注意:这里只是注册“定义”,不是马上创建对象。这样才能让 Bean 后置处理器、依赖注入、生命周期回调都正常生效。
BeanFactoryPostProcessor:修改 Bean 定义
它在 Bean 实例化前执行,适合修改已经存在的 BeanDefinition。
flowchart TD
A["BeanDefinition 已加载"] --> B["BeanFactoryPostProcessor"]
B --> C["修改属性、作用域、初始化参数"]
C --> D["后续按新定义创建 Bean"]常见用法:
| 用法 | 说明 |
|---|---|
| 修改属性值 | 给某些基础设施 Bean 增加默认属性 |
| 校验定义 | 启动时检查关键 Bean 是否存在 |
| 扩展框架 | MyBatis、配置类增强等框架内部会用 |
不要这么做:
// 不推荐:可能导致 Bean 过早实例化
// Object service = beanFactory.getBean("collectService");因为这时很多后置处理器还没准备好,过早创建出来的 Bean 可能没有 AOP、事务、校验等增强。
BeanPostProcessor:Bean 初始化前后的增强
BeanPostProcessor 是理解 AOP、事务代理、自动增强的关键。
flowchart TD
A["实例化对象"] --> B["属性填充"]
B --> C["初始化前 postProcessBeforeInitialization"]
C --> D["初始化方法"]
D --> E["初始化后 postProcessAfterInitialization"]
E --> F["可能返回代理对象"]Demo:给带 @CollectComponent 的 Bean 做启动校验。
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class CollectBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean.getClass().isAnnotationPresent(CollectComponent.class)) {
System.out.println("检查采集组件配置:" + beanName);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}如果要返回代理对象,通常在初始化后返回。Spring AOP、事务增强等能力,本质上也离不开类似时机。
ApplicationRunner:启动完成后的业务准备
Runner 在容器启动完成后执行。它适合做“服务可用前后需要完成的轻量准备”。
flowchart TD
A["refresh 完成"] --> B["WebServer 启动"]
B --> C["执行 ApplicationRunner"]
C --> D["应用进入可服务状态"]Demo:预热字典缓存。
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(1)
public class DictionaryWarmupRunner implements ApplicationRunner {
private final DictionaryService dictionaryService;
public DictionaryWarmupRunner(DictionaryService dictionaryService) {
this.dictionaryService = dictionaryService;
}
@Override
public void run(ApplicationArguments args) {
dictionaryService.warmup();
}
}注意:
- Runner 抛异常,应用可能启动失败。
- Runner 里不要做无限循环任务。
- 慢任务要异步化,并暴露“预热中”的状态。
- 定时任务不要强塞进 Runner,应该用定时任务框架。
Web 请求链路扩展点
Web 扩展点要按请求经过的层次理解。
flowchart TD
A["HTTP 请求"] --> B["Filter"]
B --> C["DispatcherServlet"]
C --> D["HandlerMapping"]
D --> E["HandlerInterceptor"]
E --> F["ArgumentResolver"]
F --> G["Controller"]
G --> H["ReturnValueHandler"]
H --> I["HttpMessageConverter"]
I --> J["HTTP 响应"]| 扩展点 | 层次 | 适合场景 |
|---|---|---|
Filter | Servlet 层 | traceId、IP 黑名单、请求体包装、基础鉴权 |
HandlerInterceptor | Spring MVC Handler 前后 | 权限、审计、接口日志、租户上下文 |
HandlerMethodArgumentResolver | Controller 参数解析 | 当前用户、当前租户、分页对象 |
HandlerMethodReturnValueHandler | Controller 返回值处理 | 统一包装、特殊响应类型 |
HttpMessageConverter | 请求体和响应体转换 | JSON、自定义格式、加密响应 |
WebMvcConfigurer | MVC 扩展入口 | 注册拦截器、参数解析器、格式化器、跨域 |
当前登录用户注入 Demo
注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}用户对象:
public class LoginUser {
private final String userId;
public LoginUser(String userId) {
this.userId = userId;
}
public String getUserId() {
return userId;
}
}解析器:
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class)
&& parameter.getParameterType().equals(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
String userId = webRequest.getHeader("X-User-Id");
return new LoginUser(userId);
}
}注册:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver());
}
}Controller:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/me")
public String me(@CurrentUser LoginUser user) {
return user.getUserId();
}
}如果不用这个扩展点,每个 Controller 都要手动解析 Header 或 Token,代码重复,而且权限上下文容易不一致。
Actuator 与生产扩展
商业项目不能只会写接口,还要能被监控、能排查、能被运维系统理解。
健康检查
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class HospitalApiHealthIndicator implements HealthIndicator {
private final HospitalApiClient hospitalApiClient;
public HospitalApiHealthIndicator(HospitalApiClient hospitalApiClient) {
this.hospitalApiClient = hospitalApiClient;
}
@Override
public Health health() {
try {
boolean ok = hospitalApiClient.ping();
if (ok) {
return Health.up().withDetail("hospitalApi", "ok").build();
}
return Health.down().withDetail("hospitalApi", "unavailable").build();
} catch (Exception ex) {
return Health.down(ex).build();
}
}
}健康检查要轻量、带超时,不能把全量业务查询放进去。否则 K8s 探针或监控系统频繁访问时,会反过来压垮服务。
指标
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class CollectMetrics {
private final Counter successCounter;
private final Counter failedCounter;
public CollectMetrics(MeterRegistry registry) {
this.successCounter = Counter.builder("collect_success_total")
.description("采集成功次数")
.register(registry);
this.failedCounter = Counter.builder("collect_failed_total")
.description("采集失败次数")
.register(registry);
}
public void success() {
successCounter.increment();
}
public void failed() {
failedCounter.increment();
}
}日志适合查单个请求,指标适合看整体趋势。比如“采集失败率突然升高”,用指标比翻日志更快。
失败分析器
Starter 如果缺配置,不能只抛空指针。可以用 FailureAnalyzer 给接入方清楚提示。
public class CollectClientConfigException extends RuntimeException {
public CollectClientConfigException(String message) {
super(message);
}
}import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
public class CollectClientFailureAnalyzer
extends AbstractFailureAnalyzer<CollectClientConfigException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure,
CollectClientConfigException cause) {
return new FailureAnalysis(
"采集客户端配置不完整:" + cause.getMessage(),
"请在 application.yml 中配置 collect.client.base-url",
cause
);
}
}Boot 2 常见声明:
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.collect.CollectClientFailureAnalyzer商业项目怎么组合这些能力
以医疗数据采集与资产平台为例,一个成熟的公共采集 Starter 可以这样设计:
| 能力 | 使用的机制 | 为什么 |
|---|---|---|
| 医院接口客户端 | 自动配置 + 条件注解 | 多服务共用,默认装配 |
| 医院接口地址和超时 | @ConfigurationProperties | 配置可外置、可校验 |
| 配置解密 | EnvironmentPostProcessor | 自动配置读取前必须解密 |
| 当前租户注入 | HandlerMethodArgumentResolver | Controller 不重复解析 |
| traceId | Filter | 请求入口统一生成 |
| 接口审计 | Interceptor | 能拿到 handler 和用户上下文 |
| 采集成功失败指标 | Micrometer | 用于监控和告警 |
| 医院接口健康检查 | HealthIndicator | 用于探针和依赖可用性判断 |
| 缺配置友好提示 | FailureAnalyzer | 接入方能快速定位问题 |
| 启动后字典预热 | ApplicationRunner | 避免首次请求慢 |
常见错误和后果
| 错误做法 | 后果 | 正确做法 |
|---|---|---|
| 自动配置类强行创建 Bean | 业务无法替换默认实现 | 使用 @ConditionalOnMissingBean |
| Starter 里写具体业务规则 | 公共组件污染业务系统 | Starter 只做基础设施能力 |
EnvironmentPostProcessor 访问业务 Bean | Bean 还没创建,启动异常 | 只处理 Environment |
BeanFactoryPostProcessor 里 getBean() | Bean 过早实例化,AOP 可能失效 | 只改 BeanDefinition |
| Runner 做长时间任务 | 应用启动慢或启动失败 | 异步化或交给任务系统 |
| 健康检查做重 SQL | 探针拖垮服务 | 轻量检查、加超时 |
| 只看 Bean 不看 conditions | 排查靠猜 | 开 debug=true 或看 /actuator/conditions |
| Boot 2 项目照抄 Boot 3 注册文件 | 自动配置不生效 | 按版本选择声明方式 |
自动配置没生效怎么排查
排查不要靠猜,按顺序走。
flowchart TD
A["发现 Bean 没有自动配置"] --> B["打开 debug 或 actuator conditions"]
B --> C{"自动配置类在候选列表吗"}
C -- "否" --> D["检查 imports / spring.factories / jar 是否引入"]
C -- "是" --> E{"是否被 exclude"}
E -- "是" --> F["检查启动类和配置项 exclude"]
E -- "否" --> G{"条件是否匹配"}
G -- "否" --> H["检查 classpath、配置开关、Web 环境"]
G -- "是" --> I{"是否已有用户 Bean"}
I -- "是" --> J["默认 Bean 让位,检查 @ConditionalOnMissingBean"]
I -- "否" --> K["看 Bean 创建异常和绑定错误"]1. 开启条件报告
debug: true重点看:
| 区域 | 含义 |
|---|---|
| Positive matches | 条件匹配,配置生效 |
| Negative matches | 条件不匹配,配置未生效 |
| Exclusions | 被排除的自动配置 |
| Unconditional classes | 无条件自动配置类 |
2. 使用 Actuator conditions
management:
endpoints:
web:
exposure:
include: conditions,health,info访问:
curl http://127.0.0.1:8080/actuator/conditions3. 常见定位点
| 现象 | 重点检查 |
|---|---|
| 自动配置类完全没出现 | 自动配置声明文件路径、类名、jar 是否真的依赖进来 |
| 条件不匹配 | 是否缺依赖、配置项写错、不是 Web 环境 |
| 默认 Bean 不创建 | 是否已有同类型 Bean,@ConditionalOnMissingBean 是否让位 |
| 配置绑定失败 | 属性名、类型、Profile、配置中心覆盖 |
| 本地能启动线上不能 | 环境变量、Profile、配置中心、JDK/Boot 版本 |
| Boot 3 有效 Boot 2 无效 | 是否只写了 AutoConfiguration.imports |
面试标准回答
自动配置完整流程
Spring Boot 自动配置的核心是 @EnableAutoConfiguration。它通过 @Import 导入 AutoConfigurationImportSelector,启动时读取自动配置候选类。Spring Boot 2.6及更早常见读取 META-INF/spring.factories;Boot 2.7已支持并推荐AutoConfiguration.imports;Boot 3使用该imports机制。读取到候选类后,会处理 exclude、去重、排序,然后把这些配置类交给 Spring 解析。
候选配置类不会全部生效,而是通过 @ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnWebApplication 等条件注解决定是否注册默认 Bean。自动配置类里的 @Bean 最终仍然会被解析成 BeanDefinition,并在 ApplicationContext.refresh 阶段由 IOC 容器创建。很多默认 Bean 使用 @ConditionalOnMissingBean,所以业务项目自定义 Bean 后,默认 Bean 会让位。Starter 为什么能开箱即用
Starter 本质是依赖聚合和自动配置入口。starter 模块负责让业务项目方便地引入一组场景依赖,autoconfigure 模块负责提供自动配置类、配置属性类、条件注解和默认 Bean。业务项目引入 Starter 后,相关类进入 classpath,Spring Boot 读取自动配置声明文件,条件满足就创建默认 Bean,所以看起来像开箱即用。成熟 Starter 还要支持配置开关、用户 Bean 覆盖、健康检查、指标和失败诊断。Spring Boot 有哪些扩展点,怎么选
Spring Boot 扩展点要按生命周期选择。启动早期改配置用 EnvironmentPostProcessor;容器创建后 refresh 前用 ApplicationContextInitializer;动态注册 BeanDefinition 用 BeanDefinitionRegistryPostProcessor;修改 BeanDefinition 用 BeanFactoryPostProcessor;Bean 初始化前后增强用 BeanPostProcessor;启动完成后做轻量预热用 ApplicationRunner 或 CommandLineRunner;Web 请求链路用 Filter、Interceptor、ArgumentResolver、MessageConverter;生产监控用 HealthIndicator、MeterBinder;启动失败提示用 FailureAnalyzer。核心原则是先判断要干预哪个阶段,再选最小合适扩展点。关联知识点
- Spring Boot 自动配置原理:看自动配置基础链路。
- Spring Boot Starter:看 Starter 的模块拆分和工程写法。
- Spring Boot 扩展点:看各扩展点的基础用法。
- Spring Boot 启动原理:看
SpringApplication.run和refresh()。 - Spring IOC:理解 BeanDefinition 和容器创建。
- Spring 核心扩展点:理解底层扩展点。
本章小结
Spring Boot 的核心不是“少写配置”这么简单,而是把通用能力沉淀为一套可发现、可判断、可让位、可扩展、可排查的装配体系。自动配置负责默认装配,Starter 负责传播依赖和入口,条件注解负责是否生效,IOC 负责真正创建 Bean,扩展点负责在不同生命周期插入自定义逻辑。把这条链路打通后,你再看任何 Starter、自动配置失效、启动慢、Bean 没创建、扩展点选择问题,都不会只停留在背答案。
