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,自动配置早就读完配置了;如果你在 BeanFactoryPostProcessor 里 getBean(),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 请求链路Filter、Interceptor、ArgumentResolvertraceId、鉴权、当前用户注入每个 Controller 重复写
监控诊断HealthIndicator、MeterBinder、FailureAnalyzer健康检查、指标、友好错误执行重查询

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
BeanFactoryPostProcessor 里 getBean()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 没创建、扩展点选择问题,都不会只停留在背答案。