Skip to content

Spring Boot 自动配置与扩展点全过程

很多人学 Spring Boot 只记住一句“约定优于配置”,但真正做项目、排查问题、回答面试时,这句话远远不够。你必须知道:

  1. 自动配置类是怎么被发现的。
  2. 条件注解是怎么决定 Bean 是否创建的。
  3. Starter 为什么引入后能开箱即用。
  4. 用户自定义 Bean 为什么能覆盖默认 Bean。
  5. 各种扩展点分别插在启动生命周期的哪一步。
  6. 项目中到底应该用哪个扩展点,而不是乱用。

这一页把自动配置、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 的启动不是一步完成,而是一层层推进。

mermaid
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,自动配置早就读完配置了;如果你在 BeanFactoryPostProcessorgetBean(),Bean 可能会过早创建,生命周期就乱了。

第一步:@SpringBootApplication 做了什么

@SpringBootApplication 不是一个单纯的“启动按钮”,它是组合注解。

mermaid
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”,而是因为:

  1. 你引入了 spring-boot-starter-web
  2. classpath 中出现了 Spring MVC、Jackson、Tomcat。
  3. Web 相关自动配置类进入候选列表。
  4. 条件注解判断成立。
  5. 默认的 Servlet、MVC、JSON、内嵌容器 Bean 被注册。

第二步:自动配置候选类从哪里来

自动配置不是在启动时全盘扫描所有 jar 包。它读取的是约定位置的清单。

Boot 2 常见方式

文件路径:

text
META-INF/spring.factories

内容示例:

properties
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 推荐方式

文件路径:

text
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

内容示例:

text
com.example.collect.CollectClientAutoConfiguration
com.example.collect.CollectMetricsAutoConfiguration

两者区别

对比项Boot 2 spring.factoriesBoot 3 AutoConfiguration.imports
声明方式key-value每行一个配置类
可读性自动配置多时很长更清晰
常见环境JDK 8、Boot 2 老项目JDK 17+、Boot 3 新项目
面试重点能看懂老项目机制能说明新机制为什么更直接

第三步:AutoConfigurationImportSelector 到底做了什么

@EnableAutoConfiguration 内部会通过 @Import 引入选择器。这个选择器不是直接创建 Bean,而是返回一批自动配置类名,让 Spring 继续解析它们。

mermaid
flowchart TD
    A["@EnableAutoConfiguration"] --> B["AutoConfigurationImportSelector"]
    B --> C["读取自动配置清单"]
    C --> D["加载候选类名"]
    D --> E["处理 exclude"]
    E --> F["去重"]
    F --> G["按顺序排序"]
    G --> H["返回给 Spring 解析"]

关键理解:

  1. AutoConfigurationImportSelector 做的是“选择配置类”,不是“创建业务对象”。
  2. 自动配置类最终仍然要被 Spring 当作配置类解析。
  3. 自动配置类里的 @Bean 方法最终会变成 BeanDefinition。
  4. Bean 的实例化仍然发生在 ApplicationContext.refresh() 阶段。

第四步:条件注解为什么是自动配置核心

自动配置类进入候选列表,并不代表一定生效。真正决定是否创建默认 Bean 的,是条件注解。

mermaid
flowchart TD
    A["候选自动配置类"] --> B{"类路径是否有依赖"}
    B -- "否" --> X["跳过"]
    B -- "是" --> C{"配置开关是否开启"}
    C -- "否" --> X
    C -- "是" --> D{"容器中是否已有用户 Bean"}
    D -- "有" --> E["默认 Bean 让位"]
    D -- "没有" --> F["注册默认 Bean"]
注解判断什么商业项目常见用法
@ConditionalOnClassclasspath 中存在某个类引入 Redis 依赖才配置 RedisTemplate
@ConditionalOnMissingClassclasspath 中不存在某个类某实现不存在时启用备用实现
@ConditionalOnBean容器中存在某个 Bean有 DataSource 后再创建 JdbcTemplate
@ConditionalOnMissingBean容器中不存在某个 Bean用户没自定义时提供默认客户端
@ConditionalOnProperty配置项符合条件enabled=false 关闭某个 Starter
@ConditionalOnWebApplication当前是 Web 应用只在 Web 服务中注册 MVC 组件
@ConditionalOnResource存在某资源根据模板、证书、配置文件启用能力

为什么 @ConditionalOnMissingBean 特别重要

假设自动配置强行创建一个 CollectClient,业务项目就很难替换它。正确写法是:

java
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());
}

业务项目自定义:

java
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 标准流程。

mermaid
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 覆盖默认 BeanBeanDefinition 条件判断和 IOC 容器
AOP、事务、监控增强BeanPostProcessor 创建代理
配置绑定到对象Environment、Binder、类型转换
启动后执行 Runner容器刷新完成后的回调

第六步:Starter 为什么能开箱即用

Starter 不是一个神秘包,它通常由两部分组成:

模块职责
xxx-spring-boot-starter聚合依赖,让业务项目只引入一个入口
xxx-spring-boot-autoconfigure放自动配置类、配置属性类、条件判断、默认 Bean

工作链路:

mermaid
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”,而是:

  1. 依赖边界清楚。
  2. 自动配置类能被 Spring Boot 发现。
  3. 条件注解写得合理。
  4. 配置属性可读、可校验。
  5. 默认 Bean 可被用户覆盖。
  6. 失败时有清晰排查信息。

自定义采集客户端 Starter Demo

下面以医疗数据采集平台里的“医院接口采集客户端”为例,写一个最小但完整的 Starter 思路。

1. 配置属性类

java
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. 客户端类

java
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. 自动配置类

java
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 注册

text
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
text
com.example.collect.CollectClientAutoConfiguration

5. Boot 2 注册

text
META-INF/spring.factories
properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.collect.CollectClientAutoConfiguration

6. 业务项目配置

yaml
collect:
  client:
    enabled: true
    base-url: https://hospital.example.com
    connect-timeout-ms: 2000
    read-timeout-ms: 8000

7. 业务项目使用

java
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 的执行过程是:

  1. 业务项目引入 Starter。
  2. CollectClient 进入 classpath。
  3. Boot 读取自动配置声明。
  4. @ConditionalOnClass 判断通过。
  5. collect.client.enabled 没配置或为 true,判断通过。
  6. CollectClientProperties 绑定 application.yml
  7. 容器里没有用户自定义 CollectClient,默认 Bean 创建。
  8. Controller 构造方法注入 CollectClient

扩展点全景:先看生命周期

扩展点必须按生命周期理解,不能靠背名字。

mermaid
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 请求链路FilterInterceptorArgumentResolvertraceId、鉴权、当前用户注入每个 Controller 重复写
监控诊断HealthIndicatorMeterBinderFailureAnalyzer健康检查、指标、友好错误执行重查询

EnvironmentPostProcessor:配置读取前的扩展

它在 Bean 创建之前执行,适合处理“后续自动配置要读取的配置”。

mermaid
flowchart TD
    A["读取命令行和环境变量"] --> B["创建 Environment"]
    B --> C["执行 EnvironmentPostProcessor"]
    C --> D["加入解密后配置或默认配置"]
    D --> E["@ConfigurationProperties 绑定"]
    E --> F["自动配置条件判断"]

Demo:给采集线程池补默认配置。

java
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

错误用法:

java
// 错误思路:此时业务 Bean 还没创建
// hospitalApiClient.ping();

这里不要做远程调用,也不要拿业务 Bean。否则启动早期就会被外部系统拖慢,甚至整个应用起不来。

BeanDefinitionRegistryPostProcessor:动态注册 Bean 定义

它比普通 BeanFactoryPostProcessor 更早,能向容器注册新的 BeanDefinition。典型场景是框架根据注解、配置、接口动态注册一批客户端或 Mapper。

mermaid
flowchart TD
    A["扫描配置或接口"] --> B["构造 BeanDefinition"]
    B --> C["注册到 BeanDefinitionRegistry"]
    C --> D["refresh 后续统一创建 Bean"]

Demo:根据配置动态注册一个 CollectClient BeanDefinition。

java
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。

mermaid
flowchart TD
    A["BeanDefinition 已加载"] --> B["BeanFactoryPostProcessor"]
    B --> C["修改属性、作用域、初始化参数"]
    C --> D["后续按新定义创建 Bean"]

常见用法:

用法说明
修改属性值给某些基础设施 Bean 增加默认属性
校验定义启动时检查关键 Bean 是否存在
扩展框架MyBatis、配置类增强等框架内部会用

不要这么做:

java
// 不推荐:可能导致 Bean 过早实例化
// Object service = beanFactory.getBean("collectService");

因为这时很多后置处理器还没准备好,过早创建出来的 Bean 可能没有 AOP、事务、校验等增强。

BeanPostProcessor:Bean 初始化前后的增强

BeanPostProcessor 是理解 AOP、事务代理、自动增强的关键。

mermaid
flowchart TD
    A["实例化对象"] --> B["属性填充"]
    B --> C["初始化前 postProcessBeforeInitialization"]
    C --> D["初始化方法"]
    D --> E["初始化后 postProcessAfterInitialization"]
    E --> F["可能返回代理对象"]

Demo:给带 @CollectComponent 的 Bean 做启动校验。

java
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 在容器启动完成后执行。它适合做“服务可用前后需要完成的轻量准备”。

mermaid
flowchart TD
    A["refresh 完成"] --> B["WebServer 启动"]
    B --> C["执行 ApplicationRunner"]
    C --> D["应用进入可服务状态"]

Demo:预热字典缓存。

java
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();
    }
}

注意:

  1. Runner 抛异常,应用可能启动失败。
  2. Runner 里不要做无限循环任务。
  3. 慢任务要异步化,并暴露“预热中”的状态。
  4. 定时任务不要强塞进 Runner,应该用定时任务框架。

Web 请求链路扩展点

Web 扩展点要按请求经过的层次理解。

mermaid
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 响应"]
扩展点层次适合场景
FilterServlet 层traceId、IP 黑名单、请求体包装、基础鉴权
HandlerInterceptorSpring MVC Handler 前后权限、审计、接口日志、租户上下文
HandlerMethodArgumentResolverController 参数解析当前用户、当前租户、分页对象
HandlerMethodReturnValueHandlerController 返回值处理统一包装、特殊响应类型
HttpMessageConverter请求体和响应体转换JSON、自定义格式、加密响应
WebMvcConfigurerMVC 扩展入口注册拦截器、参数解析器、格式化器、跨域

当前登录用户注入 Demo

注解:

java
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 {
}

用户对象:

java
public class LoginUser {
    private final String userId;

    public LoginUser(String userId) {
        this.userId = userId;
    }

    public String getUserId() {
        return userId;
    }
}

解析器:

java
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);
    }
}

注册:

java
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:

java
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 与生产扩展

商业项目不能只会写接口,还要能被监控、能排查、能被运维系统理解。

健康检查

java
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 探针或监控系统频繁访问时,会反过来压垮服务。

指标

java
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 给接入方清楚提示。

java
public class CollectClientConfigException extends RuntimeException {
    public CollectClientConfigException(String message) {
        super(message);
    }
}
java
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 常见声明:

properties
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.collect.CollectClientFailureAnalyzer

商业项目怎么组合这些能力

以医疗数据采集与资产平台为例,一个成熟的公共采集 Starter 可以这样设计:

能力使用的机制为什么
医院接口客户端自动配置 + 条件注解多服务共用,默认装配
医院接口地址和超时@ConfigurationProperties配置可外置、可校验
配置解密EnvironmentPostProcessor自动配置读取前必须解密
当前租户注入HandlerMethodArgumentResolverController 不重复解析
traceIdFilter请求入口统一生成
接口审计Interceptor能拿到 handler 和用户上下文
采集成功失败指标Micrometer用于监控和告警
医院接口健康检查HealthIndicator用于探针和依赖可用性判断
缺配置友好提示FailureAnalyzer接入方能快速定位问题
启动后字典预热ApplicationRunner避免首次请求慢

常见错误和后果

错误做法后果正确做法
自动配置类强行创建 Bean业务无法替换默认实现使用 @ConditionalOnMissingBean
Starter 里写具体业务规则公共组件污染业务系统Starter 只做基础设施能力
EnvironmentPostProcessor 访问业务 BeanBean 还没创建,启动异常只处理 Environment
BeanFactoryPostProcessorgetBean()Bean 过早实例化,AOP 可能失效只改 BeanDefinition
Runner 做长时间任务应用启动慢或启动失败异步化或交给任务系统
健康检查做重 SQL探针拖垮服务轻量检查、加超时
只看 Bean 不看 conditions排查靠猜debug=true 或看 /actuator/conditions
Boot 2 项目照抄 Boot 3 注册文件自动配置不生效按版本选择声明方式

自动配置没生效怎么排查

排查不要靠猜,按顺序走。

mermaid
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. 开启条件报告

yaml
debug: true

重点看:

区域含义
Positive matches条件匹配,配置生效
Negative matches条件不匹配,配置未生效
Exclusions被排除的自动配置
Unconditional classes无条件自动配置类

2. 使用 Actuator conditions

yaml
management:
  endpoints:
    web:
      exposure:
        include: conditions,health,info

访问:

bash
curl http://127.0.0.1:8080/actuator/conditions

3. 常见定位点

现象重点检查
自动配置类完全没出现自动配置声明文件路径、类名、jar 是否真的依赖进来
条件不匹配是否缺依赖、配置项写错、不是 Web 环境
默认 Bean 不创建是否已有同类型 Bean,@ConditionalOnMissingBean 是否让位
配置绑定失败属性名、类型、Profile、配置中心覆盖
本地能启动线上不能环境变量、Profile、配置中心、JDK/Boot 版本
Boot 3 有效 Boot 2 无效是否只写了 AutoConfiguration.imports

面试标准回答

自动配置完整流程

text
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 为什么能开箱即用

text
Starter 本质是依赖聚合和自动配置入口。starter 模块负责让业务项目方便地引入一组场景依赖,autoconfigure 模块负责提供自动配置类、配置属性类、条件注解和默认 Bean。业务项目引入 Starter 后,相关类进入 classpath,Spring Boot 读取自动配置声明文件,条件满足就创建默认 Bean,所以看起来像开箱即用。成熟 Starter 还要支持配置开关、用户 Bean 覆盖、健康检查、指标和失败诊断。

Spring Boot 有哪些扩展点,怎么选

text
Spring Boot 扩展点要按生命周期选择。启动早期改配置用 EnvironmentPostProcessor;容器创建后 refresh 前用 ApplicationContextInitializer;动态注册 BeanDefinition 用 BeanDefinitionRegistryPostProcessor;修改 BeanDefinition 用 BeanFactoryPostProcessor;Bean 初始化前后增强用 BeanPostProcessor;启动完成后做轻量预热用 ApplicationRunner 或 CommandLineRunner;Web 请求链路用 Filter、Interceptor、ArgumentResolver、MessageConverter;生产监控用 HealthIndicator、MeterBinder;启动失败提示用 FailureAnalyzer。核心原则是先判断要干预哪个阶段,再选最小合适扩展点。

关联知识点

本章小结

Spring Boot 的核心不是“少写配置”这么简单,而是把通用能力沉淀为一套可发现、可判断、可让位、可扩展、可排查的装配体系。自动配置负责默认装配,Starter 负责传播依赖和入口,条件注解负责是否生效,IOC 负责真正创建 Bean,扩展点负责在不同生命周期插入自定义逻辑。把这条链路打通后,你再看任何 Starter、自动配置失效、启动慢、Bean 没创建、扩展点选择问题,都不会只停留在背答案。