Skip to content

Spring Boot 扩展点

Spring Boot 扩展点解决的是:不改框架源码,也不把逻辑硬塞进业务代码,就能在合适的生命周期阶段插入自己的逻辑。

扩展点不是越多越好。真正要学会的是:

先判断你要干预的是“启动前配置”“Bean 定义”“Bean 初始化”“应用启动完成”“Web 请求链路”“健康检查”“失败诊断”中的哪一段,再选择对应扩展点。

学习目标

学完本页,你应该能回答:

  1. Spring Boot 启动过程有哪些阶段可以扩展。
  2. EnvironmentPostProcessorApplicationContextInitializerApplicationRunner 有什么区别。
  3. 什么场景用 BeanPostProcessor,什么场景用 ApplicationListener
  4. Web 项目里拦截器、过滤器、参数解析器、返回值处理器分别在哪一层生效。
  5. Actuator 健康检查和指标怎么扩展。
  6. 自定义 Starter 通常要用哪些扩展点。
  7. 面试时如何讲“我不只会用注解,还懂框架扩展链路”。

本页按扩展点逐个讲用法。如果你想先建立“自动配置什么时候发生、扩展点插在哪一步、为什么选错会出问题”的全链路认知,先看 Spring Boot 自动配置与扩展点全过程

扩展点全景图

mermaid
flowchart TD
    A["Spring Boot 扩展点"] --> B["启动早期"]
    A --> C["容器阶段"]
    A --> D["启动完成"]
    A --> E["Web 请求链路"]
    A --> F["运维监控"]
    A --> G["失败诊断"]
    B --> B1["EnvironmentPostProcessor"]
    B --> B2["ApplicationContextInitializer"]
    C --> C1["BeanFactoryPostProcessor"]
    C --> C2["BeanPostProcessor"]
    D --> D1["ApplicationRunner"]
    D --> D2["CommandLineRunner"]
    E --> E1["Filter / Interceptor"]
    E --> E2["WebMvcConfigurer"]
    F --> F1["HealthIndicator / MeterBinder"]
    G --> G1["FailureAnalyzer"]

按启动阶段理解扩展点

mermaid
flowchart TD
    A["启动 SpringApplication"] --> B["准备 Environment"]
    B --> C["创建 ApplicationContext"]
    C --> D["加载 BeanDefinition"]
    D --> E["实例化和初始化 Bean"]
    E --> F["启动 WebServer"]
    F --> G["发布启动完成事件"]
    G --> H["执行 Runner"]
阶段常用扩展点适合做什么
准备 EnvironmentEnvironmentPostProcessor加载外部配置、解密配置、插入属性源
创建容器后刷新前ApplicationContextInitializer修改容器属性、注册早期组件
BeanDefinition 阶段BeanFactoryPostProcessor修改 BeanDefinition
Bean 初始化阶段BeanPostProcessorBean 初始化前后增强
事件阶段ApplicationListener监听启动、刷新、失败等事件
启动完成后ApplicationRunnerCommandLineRunner预热缓存、校验依赖、启动任务
Web 请求链路FilterHandlerInterceptor日志、鉴权、traceId、限流
MVC 定制WebMvcConfigurer参数解析、跨域、格式化、拦截器
监控HealthIndicatorMeterBinder健康检查、自定义指标
失败诊断FailureAnalyzer把启动异常翻译成人能看懂的提示

EnvironmentPostProcessor

EnvironmentPostProcessor 在 Bean 创建之前执行,适合修改配置环境。比如从自定义位置加载配置、对敏感配置解密、加入默认属性。

流程:

mermaid
flowchart TD
    A["SpringApplication 启动"] --> B["创建 Environment"]
    B --> C["执行 EnvironmentPostProcessor"]
    C --> D["加入或修改 PropertySource"]
    D --> E["后续自动配置读取配置"]

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 CollectDefaultEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
                                       SpringApplication application) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("collect.thread.core-size", "8");
        map.put("collect.thread.queue-capacity", "1000");
        environment.getPropertySources().addLast(
                new MapPropertySource("collectDefaultProperties", map)
        );
    }
}

声明文件,Boot 2 常见:

properties
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.collect.CollectDefaultEnvironmentPostProcessor

适合场景:

场景原因
配置解密自动配置读取前就要解密
加载外部配置让后续 @ConfigurationProperties 能绑定
设置默认值不影响用户显式配置

常见坑:

后果
在这里访问业务 Bean此时 Bean 还没创建
做慢网络调用启动变慢甚至失败
属性源优先级放错用户配置可能被默认值覆盖

ApplicationContextInitializer

ApplicationContextInitializerApplicationContext 创建后、refresh() 之前执行。此时容器对象有了,但 Bean 还没有正式创建。

mermaid
flowchart TD
    A["创建 ApplicationContext"] --> B["执行 Initializer"]
    B --> C["修改容器或注册早期对象"]
    C --> D["refresh 创建 Bean"]

Demo:设置一个启动标记。

java
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;

public class CollectContextInitializer
        implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        applicationContext.getBeanFactory()
                .registerSingleton("collectStartupMarker", "collect-context-ready");
    }
}

注册方式:

java
SpringApplication application = new SpringApplication(DemoApplication.class);
application.addInitializers(new CollectContextInitializer());
application.run(args);

EnvironmentPostProcessor 区别:

对比EnvironmentPostProcessorApplicationContextInitializer
执行时机Environment 准备阶段Context 创建后、refresh 前
主要对象配置环境Spring 容器
典型用途配置解密、属性源修改容器、注册早期对象

ApplicationListener

Spring Boot 启动过程中会发布很多事件,例如环境准备完成、容器准备完成、启动完成、启动失败。

mermaid
flowchart TD
    A["启动过程"] --> B["发布事件"]
    B --> C["ApplicationListener 接收"]
    C --> D["执行监听逻辑"]

Demo:监听应用启动完成。

java
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class CollectReadyListener
        implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        System.out.println("采集服务启动完成,可以接收任务");
    }
}

常见事件:

事件含义适合做什么
ApplicationStartingEvent启动刚开始很少直接使用
ApplicationEnvironmentPreparedEventEnvironment 准备好读取配置、日志准备
ApplicationPreparedEventContext 准备好观察 BeanDefinition
ApplicationReadyEvent应用可服务缓存预热、注册状态
ApplicationFailedEvent启动失败告警、诊断日志

注意:监听启动完成后做预热可以,但不要长时间阻塞主线程。如果预热很慢,要异步执行并做好状态标记。

ApplicationRunner 和 CommandLineRunner

这两个扩展点都在容器启动完成后执行,适合做启动后任务。

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

区别:

扩展点参数形式适合
ApplicationRunnerApplicationArguments需要解析命令行 option
CommandLineRunnerString[] args简单拿原始参数

Demo:启动后预热字典缓存。

java
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
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. 多个 Runner 可以用 @Order 控制顺序。
  4. 如果只是定时任务,不要强行放 Runner。

BeanFactoryPostProcessor

BeanFactoryPostProcessor 在 Bean 实例化之前执行,能修改 BeanDefinition。

这是 Spring 核心扩展点,Spring Boot 中也经常间接受益于它。

mermaid
flowchart TD
    A["BeanDefinition 已注册"] --> B["BeanFactoryPostProcessor"]
    B --> C["修改 BeanDefinition"]
    C --> D["后续按新定义创建 Bean"]

Demo:检查所有 Service Bean 命名。

java
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

@Component
public class ServiceBeanNameCheckPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
            throws BeansException {
        String[] names = beanFactory.getBeanDefinitionNames();
        for (String name : names) {
            if (name.endsWith("Service")) {
                System.out.println("service bean = " + name);
            }
        }
    }
}

不要在这里提前 getBean(),因为这会导致 Bean 过早实例化,破坏正常生命周期。

BeanDefinitionRegistryPostProcessor

BeanDefinitionRegistryPostProcessorBeanFactoryPostProcessor 更早一点。它能在 BeanDefinition 注册阶段继续往容器里注册新的 BeanDefinition。

一句话区分:

扩展点主要能力典型用途
BeanDefinitionRegistryPostProcessor新增或删除 BeanDefinition扫描 Mapper、注册代理 Bean、批量注册客户端
BeanFactoryPostProcessor修改已有 BeanDefinition调整属性、校验定义、修改 scope

为什么它重要?

MyBatis Mapper、Feign Client、Dubbo Reference 这类接口本身不能直接 new 出业务实现,框架需要扫描接口,然后为接口注册一个代理对象的 BeanDefinition。这个动作必须发生在普通 Bean 创建之前,否则业务 Service 注入接口时就找不到 Bean。

mermaid
flowchart TD
    A["扫描接口或配置"] --> B["构造 BeanDefinition"]
    B --> C["注册到 BeanDefinitionRegistry"]
    C --> D["业务 Bean 依赖该定义"]
    D --> E["refresh 阶段创建代理对象"]

Demo:注册一个采集客户端 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;
import org.springframework.stereotype.Component;

@Component
public class CollectClientRegistryPostProcessor
        implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
            throws BeansException {
        BeanDefinition definition = BeanDefinitionBuilder
                .genericBeanDefinition(CollectClient.class)
                .addConstructorArgValue("https://hospital.example.com")
                .getBeanDefinition();
        registry.registerBeanDefinition("collectClient", definition);
    }
}

常见坑:

后果正确做法
在这里读取业务 Bean此时 Bean 还没创建只处理定义,不处理对象
注册 Bean 名重复覆盖或启动失败注册前先判断是否存在
把业务逻辑放进注册器启动过程变复杂只做框架基础设施注册

ImportBeanDefinitionRegistrar

ImportBeanDefinitionRegistrar 也是注册 BeanDefinition 的扩展点,但它通常和 @Import、自定义 @EnableXxx 注解一起使用。

这类扩展的典型风格是:

java
@EnableCollectClient
@SpringBootApplication
public class DemoApplication {
}

然后 @EnableCollectClient 负责导入注册器。

java
import org.springframework.context.annotation.Import;

@Import(CollectClientRegistrar.class)
public @interface EnableCollectClient {
}

注册器代码:

java
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class CollectClientRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
                                        BeanDefinitionRegistry registry) {
        if (!registry.containsBeanDefinition("collectClient")) {
            registry.registerBeanDefinition(
                    "collectClient",
                    BeanDefinitionBuilder
                            .genericBeanDefinition(CollectClient.class)
                            .getBeanDefinition()
            );
        }
    }
}

它和自动配置的区别:

对比ImportBeanDefinitionRegistrarSpring Boot 自动配置
触发方式用户显式加 @EnableXxx 或被配置类导入Boot 启动时读取自动配置清单
主要用途精确注册某类基础设施 Bean按条件提供默认 Bean
常见例子Mapper 扫描、Feign Client、@EnableScheduling 类思路Web、Redis、DataSource、Actuator
面试重点编程式注册 BeanDefinition候选配置类 + 条件注解

如果是业务项目里普通配置,不要为了“高级”滥用这个扩展点。只有当你需要根据注解元数据、接口扫描结果、动态规则注册 BeanDefinition 时,它才合适。

BeanPostProcessor

BeanPostProcessor 在 Bean 初始化前后执行,适合给 Bean 做增强、包装、校验。

mermaid
flowchart TD
    A["实例化 Bean"] --> B["属性填充"]
    B --> C["初始化前 BeanPostProcessor"]
    C --> D["初始化方法"]
    D --> E["初始化后 BeanPostProcessor"]
    E --> F["Bean 可用"]

Demo:给带自定义注解的 Bean 打印初始化日志。

java
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class CollectBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean.getClass().isAnnotationPresent(CollectComponent.class)) {
            System.out.println("before init collect bean: " + beanName);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        return bean;
    }
}

常见用途:

用途说明
AOP 代理创建初始化后返回代理对象
注解解析扫描 Bean 上的自定义注解
客户端增强给 RPC、HTTP 客户端包装监控逻辑
参数校验启动期检查 Bean 配置是否完整

Web 层扩展点

Spring Boot Web 项目常见请求链路:

mermaid
flowchart TD
    A["HTTP 请求"] --> B["Filter"]
    B --> C["DispatcherServlet"]
    C --> D["HandlerInterceptor"]
    D --> E["Controller"]
    E --> F["返回值处理"]
    F --> G["JSON 响应"]

Filter

Filter 是 Servlet 规范层面的扩展,进入 Spring MVC 前就会执行。

适合:

场景说明
traceId请求一进来就生成
安全基础过滤Token 预处理、IP 黑名单
请求体包装多次读取 body
跨框架通用逻辑不依赖 Spring MVC

Demo:

java
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.UUID;

@Component
public class TraceIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, jakarta.servlet.ServletException {
        String traceId = UUID.randomUUID().toString();
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("traceId");
        }
    }
}

HandlerInterceptor

Interceptor 是 Spring MVC 层面的扩展,能拿到 Handler 信息。

Demo:

java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            response.setStatus(401);
            return false;
        }
        return true;
    }
}

注册:

java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final AuthInterceptor authInterceptor;

    public WebConfig(AuthInterceptor authInterceptor) {
        this.authInterceptor = authInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**");
    }
}

Filter 和 Interceptor 区别:

对比FilterInterceptor
所属体系ServletSpring MVC
执行时机DispatcherServlet 前HandlerMapping 后、Controller 前后
是否知道 Controller不知道知道 handler
适合traceId、底层过滤权限、审计、接口日志

WebMvcConfigurer

WebMvcConfigurer 是 Spring MVC 常用扩展入口。

常见方法:

方法用途
addInterceptors注册拦截器
addCorsMappings跨域
addFormatters类型转换和格式化
addArgumentResolvers自定义参数解析
addResourceHandlers静态资源映射
configureMessageConverters消息转换器

自定义参数解析 Demo:

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

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) {
        String userId = webRequest.getHeader("X-User-Id");
        return new LoginUser(userId);
    }
}

注册:

java
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(
            java.util.List<org.springframework.web.method.support.HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserArgumentResolver());
    }
}

Controller 使用:

java
@GetMapping("/me")
public LoginUser me(@CurrentUser LoginUser user) {
    return user;
}

这种扩展适合商业系统中的“当前登录用户”“当前租户”“数据权限上下文”等通用参数。

配置绑定扩展点

Spring Boot 推荐用 @ConfigurationProperties 绑定配置。

java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "collect.retry")
public class CollectRetryProperties {
    private int maxTimes = 3;
    private long intervalMs = 1000;

    public int getMaxTimes() {
        return maxTimes;
    }

    public void setMaxTimes(int maxTimes) {
        this.maxTimes = maxTimes;
    }

    public long getIntervalMs() {
        return intervalMs;
    }

    public void setIntervalMs(long intervalMs) {
        this.intervalMs = intervalMs;
    }
}

自动配置中启用:

java
@EnableConfigurationProperties(CollectRetryProperties.class)
public class CollectRetryAutoConfiguration {
}

扩展点意义:

  1. 配置从代码中分离。
  2. 支持类型转换。
  3. 支持嵌套对象。
  4. 支持校验。
  5. 方便生成配置提示元数据。

Actuator 健康检查扩展

自定义健康检查用于告诉监控系统:当前服务依赖的外部组件是否正常。

Demo:检查医院接口是否可用。

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

访问:

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

注意:健康检查不能做太重。否则监控探针本身会拖垮服务。

Micrometer 指标扩展

自定义指标用于观察业务运行情况,例如采集成功数、失败数、耗时。

Demo:

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

商业项目里,指标比日志更适合回答“现在整体是否异常”。日志适合查单个请求,指标适合看趋势和告警。

FailureAnalyzer

FailureAnalyzer 用于把启动异常转换成更可读的错误说明。

比如自定义 Starter 要求配置 collect.client.base-url,如果没配置,直接抛空指针很不友好。可以写失败分析器。

自定义异常:

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

声明:

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

适合写 Starter 时使用,让接入方看到清楚的错误提示。

自定义 Starter 常用扩展点组合

一个成熟 Starter 往往不是只有自动配置类,而是多个扩展点组合。

mermaid
flowchart TD
    A["自定义 Starter"] --> B["AutoConfiguration"]
    A --> C["ConfigurationProperties"]
    A --> D["Conditional 注解"]
    A --> E["HealthIndicator"]
    A --> F["MeterBinder 或 MeterRegistry"]
    A --> G["FailureAnalyzer"]
    A --> H["EnvironmentPostProcessor"]

采集客户端 Starter 可以这样设计:

能力扩展点
默认创建客户端@AutoConfiguration + @Bean
配置绑定@ConfigurationProperties
用户关闭功能@ConditionalOnProperty
用户自定义覆盖@ConditionalOnMissingBean
外部接口健康检查HealthIndicator
调用次数和耗时Micrometer 指标
缺配置提示FailureAnalyzer
默认配置值EnvironmentPostProcessor

扩展点怎么选

需求推荐扩展点不推荐
启动前解密配置EnvironmentPostProcessor在业务 Service 里解密
修改 BeanDefinitionBeanFactoryPostProcessorBean 创建后再改定义
给 Bean 包装增强BeanPostProcessor到处手写包装代码
启动后预热缓存ApplicationRunnerController 首次请求时懒加载
记录所有请求 traceIdFilter每个 Controller 手动写
根据注解获取当前用户HandlerMethodArgumentResolver每个接口手动解析 header
暴露依赖健康状态HealthIndicator靠日志猜
自定义启动错误提示FailureAnalyzer抛晦涩异常

扩展点选错为什么会出问题

扩展点最难的不是写接口,而是选时机。Spring Boot 启动过程像一条流水线,越早的扩展点能改的东西越底层,但能使用的对象越少;越晚的扩展点能拿到完整 Bean,但很多配置和条件判断已经结束。

mermaid
flowchart TD
    A["Environment 准备"] --> B["配置绑定和条件判断"]
    B --> C["BeanDefinition 注册"]
    C --> D["Bean 实例化和初始化"]
    D --> E["WebServer 启动"]
    E --> F["应用 Ready"]
    F --> G["接收业务请求"]
你想做的事必须发生在选晚了会怎样
配置解密自动配置条件判断之前条件已经按密文或空值判断,默认 Bean 不生效
动态注册客户端代理BeanDefinition 注册阶段容器刷新后再注册,依赖注入已经错过
包装某类 BeanBean 初始化前后Bean 已经注入到别处,增强不完整
启动后预热缓存应用 Ready 前后首次请求变慢,但如果过早做又可能依赖未就绪
当前用户注入 Controller 参数MVC 参数解析阶段每个 Controller 重复解析,逻辑不一致
输出友好启动失败原因启动失败诊断阶段只能看到底层空指针或连接异常

错误案例:配置解密放到 Runner

假设数据库密码在配置文件中是密文:

yaml
collect:
  client:
    base-url: ENC(xxx)

如果你在 ApplicationRunner 里解密:

java
@Component
public class BadDecryptRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        // 太晚了:自动配置、条件注解、配置绑定已经执行过
    }
}

此时很多自动配置已经读取过配置。正确做法是用 EnvironmentPostProcessor 在 Environment 阶段插入明文配置,或者在配置中心接入层完成解密。

错误案例:BeanFactoryPostProcessor 里 getBean

BeanFactoryPostProcessor 的职责是修改 BeanDefinition,不是创建 Bean。

java
public class BadBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        Object service = beanFactory.getBean("orderService");
        // 错误:可能导致 Bean 过早创建,BeanPostProcessor、AOP、事务代理还没准备好
    }
}

如果过早 getBean(),可能绕开后续 BeanPostProcessor,导致事务、AOP、配置注入、生命周期回调不完整。正确做法是只读取和修改 BeanDefinition 元数据。

SmartLifecycle:管理组件启动和停止顺序

ApplicationRunner 适合启动完成后做一次性逻辑;如果你要管理一个长期运行的组件,比如采集连接、后台消费线程、定时轮询客户端,更适合使用 SmartLifecycle

mermaid
flowchart TD
    A["容器 refresh"] --> B["创建所有单例 Bean"]
    B --> C["启动 LifecycleProcessor"]
    C --> D["按 phase 启动 SmartLifecycle"]
    D --> E["应用运行中"]
    E --> F["应用关闭"]
    F --> G["按相反顺序 stop"]

Demo:启动和停止采集调度器。

java
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;

@Component
public class CollectSchedulerLifecycle implements SmartLifecycle {
    private volatile boolean running;

    @Override
    public void start() {
        this.running = true;
        // 启动后台采集线程、注册长连接、启动消费循环
    }

    @Override
    public void stop() {
        this.running = false;
        // 停止拉取新任务,等待正在执行的任务收尾,释放资源
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    @Override
    public int getPhase() {
        return 100;
    }
}

phase 越小越早启动,越晚停止。它适合表达组件依赖顺序:

组件建议 phase原因
基础连接、客户端较小其他组件依赖它
消费者、采集器中等依赖客户端就绪
对外接流量的组件较大等内部依赖准备好再接收

如果不用 SmartLifecycle,把后台线程随便放到构造方法或 @PostConstruct 里启动,常见问题是:依赖还没准备好、异常不好感知、应用关闭时线程不退出、K8s 滚动发布时任务被中断。

ResponseBodyAdvice:统一响应增强

如果要给所有接口响应补充 traceId、请求时间、统一包装结构,可以使用 ResponseBodyAdvice。它发生在 Controller 返回之后、消息转换器写响应之前。

mermaid
flowchart TD
    A["Controller 返回对象"] --> B["ResponseBodyAdvice"]
    B --> C["包装或补充字段"]
    C --> D["HttpMessageConverter 序列化 JSON"]
    D --> E["返回客户端"]

Demo:

java
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        if (body instanceof ApiResult) {
            return body;
        }
        return ApiResult.success(body);
    }
}

注意边界:

场景注意点
文件下载不要包装二进制流
字符串返回String 转 JSON 包装容易类型冲突
已经统一返回避免二次包装
异常响应通常配合 @RestControllerAdvice 统一异常

这类扩展点适合做统一协议,不适合塞业务判断。否则所有接口都会被隐藏逻辑影响,排查成本很高。

ExitCodeGenerator:启动失败或退出码

在脚本、容器、CI/CD 里,进程退出码很重要。ExitCodeGenerator 可以让应用在特定失败场景返回明确退出码。

java
import org.springframework.boot.ExitCodeGenerator;

public class CollectConfigException extends RuntimeException
        implements ExitCodeGenerator {

    public CollectConfigException(String message) {
        super(message);
    }

    @Override
    public int getExitCode() {
        return 12;
    }
}

适合场景:

场景退出码意义
配置缺失CI/CD 或运维脚本能识别是配置错误
外部依赖不可用区分业务启动失败和代码异常
初始化数据失败避免容器反复假成功

它通常和 FailureAnalyzer 搭配:一个负责给人看懂原因,一个负责给脚本或平台识别状态。

AvailabilityState:就绪和存活状态

Spring Boot 支持应用可用性状态,比如 Liveness 和 Readiness。它常和 Kubernetes 探针配合。

状态说明常见用途
Liveness应用进程是否还活着判断是否需要重启容器
Readiness应用是否准备好接流量判断是否从 Service 摘除

商业项目里很常见的需求是:应用启动了,但字典、连接池、消息订阅、采集通道还没准备好,这时不应该接流量。

mermaid
flowchart TD
    A["应用进程启动"] --> B["Liveness 可用"]
    B --> C["加载配置和初始化依赖"]
    C --> D{"核心依赖是否准备好"}
    D -- "否" --> E["Readiness 拒绝流量"]
    D -- "是" --> F["Readiness 接收流量"]

如果没有就绪状态,网关或 K8s 可能在应用还没预热完成时就把流量打进来,导致启动后第一批请求失败。

常见坑

后果正确做法
扩展点里做重网络调用启动慢或启动失败设置超时,必要时延迟执行
BeanFactoryPostProcessor 里 getBeanBean 过早创建,生命周期错乱只操作 BeanDefinition
BeanPostProcessor 返回 nullBean 注册异常不增强时返回原 Bean
Runner 执行长任务应用迟迟不可用异步执行或交给任务系统
Filter 和 Interceptor 职责混乱鉴权、日志重复执行按执行层次拆分职责
HealthIndicator 太重探针拖垮应用轻量检查、超时保护
构造方法启动后台线程Bean 还没完全初始化,关闭时不可控用 SmartLifecycle 管理启停
ResponseBodyAdvice 包装所有响应文件、字符串、异常响应被误包装排除特殊类型,配合统一异常处理
Readiness 没接入应用未准备好就接流量用健康检查和可用性状态控制流量

面试标准回答

可以这样答:

text
Spring Boot 扩展点要按启动阶段理解。启动早期如果要改配置,用 EnvironmentPostProcessor;容器创建后 refresh 前可以用 ApplicationContextInitializer;BeanDefinition 阶段用 BeanFactoryPostProcessor,Bean 初始化前后用 BeanPostProcessor;应用启动完成后做预热可以用 ApplicationRunner 或 CommandLineRunner;Web 请求链路可以用 Filter、Interceptor、WebMvcConfigurer 扩展参数解析、跨域、消息转换;生产监控可以用 HealthIndicator 和 Micrometer 指标;自定义 Starter 还可以用 FailureAnalyzer 优化启动失败提示。

我不会把所有逻辑都塞进一个扩展点,而是先判断要干预哪个生命周期阶段,再选择最小合适的扩展点。比如配置解密必须发生在自动配置读取前,适合 EnvironmentPostProcessor;当前登录用户注入 Controller 参数,适合 HandlerMethodArgumentResolver;采集服务依赖医院接口健康状态,适合 HealthIndicator。

追问:

追问回答要点
EnvironmentPostProcessor 和 Initializer 区别前者改 Environment,后者改 ApplicationContext
Runner 什么时候执行容器启动完成后执行
Filter 和 Interceptor 区别Filter 属于 Servlet,Interceptor 属于 Spring MVC
BeanPostProcessor 能做什么初始化前后增强 Bean,AOP 代理也和它有关
Starter 常用哪些扩展点AutoConfiguration、Properties、Conditional、HealthIndicator、FailureAnalyzer

关联知识点

本章小结

Spring Boot 扩展点的价值,是把通用能力放到正确生命周期阶段,而不是侵入每个业务类。配置类、Starter、监控、Web 请求链路、启动诊断都可以通过扩展点标准化。真正成熟的项目,不是到处复制工具代码,而是把可复用能力沉淀成自动配置和扩展点组合。