Spring Boot 4.1.1 深度源码解析-第 12 章 · 事件总线与 Bootstrap 上下文

16 阅读41分钟

Spring Boot 4.1.1 深度源码解析-第 12 章 · 事件总线与 Bootstrap 上下文:SpringApplicationRunListener、BootstrapRegistry、CRaC

源码根:spring-boot-4.1.1 主要包:org.springframework.boot、org.springframework.boot.context.event、org.springframework.boot.bootstrap、org.springframework.boot.availability 版本:Spring Boot 4.1.1 / Spring Framework 7.0.9


0. 本章要解决的"先有鸡还是先有蛋"

Spring Boot 启动过程中有一个本质性的困境:

很多事情需要在 ApplicationContext 存在之前完成,但它们又需要"一个容器"来共享对象。

具体例子:

  1. ConfigData 阶段:ConfigDataLocationResolver 可能需要读配置(比如 Vault 的地址),而此时 Environment 才刚建好、上下文还不存在。
  2. 配置中心客户端:Spring Cloud Config / Consul 的客户端要在 environmentPrepared 阶段拉取远程配置,它需要 HTTP 客户端、需要序列化器——这些都是"昂贵对象"。
  3. AOT / native:需要在早期记录运行时行为,供编译期使用。
  4. 测试:@SpringBootTest 需要在应用启动前注入 mock。

Boot 的答案是两套互相配合的机制:

机制解决什么本章节
SpringApplicationRunListener在启动的关键节点通知外部代码§1–§4
BootstrapRegistry在上下文之前提供一个临时容器共享对象§5–§7

再加上 CRaC(Coordinated Restore at Checkpoint)——它让"启动"这件事本身变得可以被快照和恢复,从而对上述两套机制提出了额外要求(§8)。

本章与前面章节的接口:

  • 第 04 章讲过 run() 的十二步状态机,本章把它"横切"——从事件的角度再看一遍。
  • 第 06 章讲过 EnvironmentPostProcessor,它由 ApplicationEnvironmentPreparedEvent 触发(EnvironmentPostProcessorApplicationListener)。本章能看到这条链路的完整形态。
  • 第 10 章讲过 ConfigDataEnvironment.registerBootstrapBinder——那是 BootstrapRegistry 的一个真实用法,本章补全语义。

1. SpringApplicationRunListener:七个生命周期回调点

1.1 接口全貌

// core/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java
public interface SpringApplicationRunListener {

    default void starting(ConfigurableBootstrapContext bootstrapContext) {
    }

    default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
            ConfigurableEnvironment environment) {
    }

    default void contextPrepared(ConfigurableApplicationContext context) {
    }

    default void contextLoaded(ConfigurableApplicationContext context) {
    }

    default void started(ConfigurableApplicationContext context, @Nullable Duration timeTaken) {
    }

    default void ready(ConfigurableApplicationContext context, @Nullable Duration timeTaken) {
    }

    default void failed(@Nullable ConfigurableApplicationContext context, Throwable exception) {
    }
}

七个方法全部是 default 空实现——实现类只需覆写关心的那几个。这是 Boot 的一贯风格(EnvironmentPostProcessor、ApplicationContextInitializer 都是单方法,但这里是多方法 default)。

@since 分布:

方法@since备注
starting1.0.0(接口本身)无独立 @since,随接口
environmentPrepared同上
contextPrepared同上
contextLoaded同上
started2.6.0后来加的
ready2.6.0后来加的
failed2.0.0

started 与 ready 是 2.6 才加的——在此之前只有 ApplicationStartedEvent,无法区分"ApplicationContext 刷新完"与"CommandLineRunner 跑完"。2.6 引入 ready 之后,K8s 的 readiness probe 才能精确地在"真正可以接流量"时变绿。

1.2 七个回调的精确语义

回调触发时机(在 run() 里的位置)此时可用典型用途
startingrun() 最开始,prepareEnvironment 之前bootstrapContext(无 Environment)最早期的初始化;注册 bootstrap 实例
environmentPreparedEnvironment 建好、ConfigurationPropertySources.attach 之后Environment(未绑定 spring.main)EnvironmentPostProcessor、日志系统初始化
contextPreparedApplicationContext 创建 + applyInitializers 之后context(未加载 BeanDefinition)最后的上下文定制
contextLoadedload() 之后、refresh() 之前context(已加载未刷新)注册 listener、改 BeanDefinition
startedrefresh() + afterRefresh 之后,callRunners 之前完整 context「启动完成但还没跑业务代码」
readycallRunners 之后完整 contextreadiness 探针、预热完成通知
failed任意步骤抛异常时context 可能为 null错误上报、资源清理

关键分界点:

  • started vs ready:中间夹着 callRunners。所以 ApplicationRunner / CommandLineRunner 抛异常时,started 已发、ready 不发。
  • contextPrepared vs contextLoaded:中间夹着 load()(BeanDefinition 加载)和 bootstrapContext.close(context)。
  • environmentPrepared 之后还有 bindToSpringApplication——所以在这个回调里读 SpringApplication 的属性是旧值(spring.main.* 还没绑定)。

最后一条极其重要,值得展开:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
        DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    listeners.environmentPrepared(bootstrapContext, environment);        // ← 回调在这里
    ApplicationInfoPropertySource.moveToEnd(environment);
    DefaultPropertiesPropertySource.moveToEnd(environment);
    Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
            "Environment prefix cannot be set via properties.");
    bindToSpringApplication(environment);                                 // ← 绑定到 SpringApplication 在这里
    ...
}

所以:

在 environmentPrepared 回调里,application.getProperties() 拿到的是代码里设置的值,不是 application.properties 里的值。

想读 spring.main.*,要么在 contextPrepared 之后的回调里读,要么直接从 environment 读。

另外注意这个断言:

Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
        "Environment prefix cannot be set via properties.");

spring.main.environment-prefix 不能通过配置文件设置——只能用 SpringApplication.setEnvironmentPrefix(...)(第 11 章 §5.5)。为什么?因为前缀要在 Environment 构建之前 就生效(它影响属性名的解释方式),而配置文件是在 Environment 之后才读的。这是一个自指问题,Boot 用断言把它封死。

(注意:这个属性名 spring.main.environment-prefix 在 ApplicationProperties 里是否有对应字段,我没核实——它可能是"虚拟属性",仅用于检测。标注为待查。)

1.3 构造约定与 ArgumentResolver

接口 Javadoc 说:

SpringApplicationRunListeners are loaded through the SpringFactoriesLoader and should declare a public constructor that accepts a SpringApplication instance and a String[] of arguments. A new SpringApplicationRunListener instance will be created for each run.

而实际实现用的是 4.x 的 ArgumentResolver:

private SpringApplicationRunListeners getRunListeners(String[] args) {
    ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
    argumentResolver = argumentResolver.and(String[].class, args);
    List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
            argumentResolver);
    SpringApplicationHook hook = applicationHook.get();
    SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null;
    if (hookListener != null) {
        listeners = new ArrayList<>(listeners);
        listeners.add(hookListener);
    }
    return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup);
}

两点:

  1. ArgumentResolver 显式声明 SpringApplication 与 String[] 两个可注入类型(对比第 11 章 §1.4 的 ConfigData resolver 注入六种类型)。
  2. SpringApplicationHook 追加在最后(§10 详述)。

"每次 run 都新建实例"——所以 listener 里不能有跨 run 的静态状态(或者要小心处理)。

1.4 SpringApplicationRunListeners:门面与可观测

class SpringApplicationRunListeners {

    private final Log log;
    private final List<SpringApplicationRunListener> listeners;
    private final ApplicationStartup applicationStartup;

    SpringApplicationRunListeners(Log log, List<SpringApplicationRunListener> listeners,
            ApplicationStartup applicationStartup) {
        this.log = log;
        this.listeners = List.copyOf(listeners);
        this.applicationStartup = applicationStartup;
    }

    void starting(ConfigurableBootstrapContext bootstrapContext, @Nullable Class<?> mainApplicationClass) {
        doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
                (step) -> {
                    if (mainApplicationClass != null) {
                        step.tag("mainApplicationClass", mainApplicationClass.getName());
                    }
                });
    }
    ...

    private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,
            @Nullable Consumer<StartupStep> stepAction) {
        StartupStep step = this.applicationStartup.start(stepName);
        this.listeners.forEach(listenerAction);
        if (stepAction != null) {
            stepAction.accept(step);
        }
        step.end();
    }
}

七个 StartupStep 名字:

回调step 名
startingspring.boot.application.starting
environmentPreparedspring.boot.application.environment-prepared
contextPreparedspring.boot.application.context-prepared
contextLoadedspring.boot.application.context-loaded
startedspring.boot.application.started
readyspring.boot.application.ready
failedspring.boot.application.failed

这些是 ApplicationStartup 的埋点——配合 BufferingApplicationStartup(spring.main.application-startup=buffering)可以在 /actuator/startup 看到每一步耗时。

两个带 tag 的 step:

// starting
step.tag("mainApplicationClass", mainApplicationClass.getName());

// failed
step.tag("exception", exception.getClass().toString());
String message = exception.getMessage();
if (message != null) {
    step.tag("message", message);
}

failed 的 step 会记录异常类名与消息——便于事后分析"哪一步失败、失败原因是什么"。

List.copyOf(listeners)——不可变副本,防止外部修改。

1.5 callFailedListener:异常隔离

private void callFailedListener(SpringApplicationRunListener listener,
        @Nullable ConfigurableApplicationContext context, Throwable exception) {
    try {
        listener.failed(context, exception);
    }
    catch (Throwable ex) {
        if (exception == null) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        if (this.log.isDebugEnabled()) {
            this.log.error("Error handling failed", ex);
        }
        else {
            String message = ex.getMessage();
            message = (message != null) ? message : "no error message";
            this.log.warn("Error handling failed (" + message + ")");
        }
    }
}

语义:listener 在处理 failed 时自己抛了异常,怎么办?

  • 若原始异常 exception == null(理论上不该发生)→ 直接抛出新异常
  • 否则 → 只记日志(DEBUG 时打堆栈,否则只打消息),不中断原始的失败处理流程

为什么这样设计? 因为 failed 回调通常用于"错误上报"这类非关键操作。如果上报本身失败,不应该掩盖真正的应用启动失败原因。这是一个**"不要因为诊断工具坏了就丢掉原始错误"**的原则。

1.6 与 ApplicationContext 生命周期的对照

把两条时间线对齐:

run()
 ├─ Startup.create()
 ├─ createBootstrapContext()                    ← BootstrapContext 诞生
 ├─ getRunListeners(args)
 ├─ listeners.starting(bootstrapContext, mainClass)
 │     └─ ApplicationStartingEvent
 ├─ prepareEnvironment(...)
 │     ├─ getOrCreateEnvironment()
 │     ├─ configureEnvironment()
 │     ├─ ConfigurationPropertySources.attach()
 │     ├─ listeners.environmentPrepared(...)    ★ EnvironmentPostProcessor 在这里跑
 │     │     └─ ApplicationEnvironmentPreparedEvent
 │     ├─ ApplicationInfoPropertySource.moveToEnd()
 │     ├─ DefaultPropertiesPropertySource.moveToEnd()
 │     ├─ bindToSpringApplication()
 │     └─ EnvironmentConverter.convertEnvironmentIfNecessary()
 ├─ printBanner(environment)
 ├─ createApplicationContext()                  ← ApplicationContext 诞生
 ├─ prepareContext(...)
 │     ├─ context.setEnvironment(environment)
 │     ├─ postProcessApplicationContext(context)
 │     ├─ addAotGeneratedInitializerIfNecessary(...)
 │     ├─ applyInitializers(context)            ★ ApplicationContextInitializer 在这里跑
 │     ├─ listeners.contextPrepared(context)
 │     │     └─ ApplicationContextInitializedEvent
 │     ├─ bootstrapContext.close(context)       ★ BootstrapContext 死亡
 │     │     └─ BootstrapContextClosedEvent
 │     ├─ logStartupInfo / logStartupProfileInfo
 │     ├─ registerSingleton("springApplicationArguments", ...)
 │     ├─ registerSingleton("springBootBanner", ...)
 │     ├─ LazyInitializationBeanFactoryPostProcessor(若开启)
 │     ├─ context.addApplicationListener(new KeepAlive())(若开启)
 │     ├─ PropertySourceOrderingBeanFactoryPostProcessor
 │     ├─ load(context, sources)                ★ BeanDefinition 加载(第 13 章)
 │     └─ listeners.contextLoaded(context)
 │           └─ ApplicationPreparedEvent
 ├─ refreshContext(context)                     ★ refresh()(第 13/14 章)
 │     └─ ContextRefreshedEvent → KeepAlive 启动保活线程
 ├─ afterRefresh(context, applicationArguments)
 ├─ startup.started()
 ├─ logStarted(...)
 ├─ listeners.started(context, timeTakenToStarted)
 │     ├─ ApplicationStartedEvent
 │     └─ AvailabilityChangeEvent(LivenessState.CORRECT)
 ├─ callRunners(context, applicationArguments)  ★ ApplicationRunner / CommandLineRunner
 └─ listeners.ready(context, startup.ready())
       ├─ ApplicationReadyEvent
       └─ AvailabilityChangeEvent(ReadinessState.ACCEPTING_TRAFFIC)

三个"诞生"节点与一个"死亡"节点:

对象诞生死亡
DefaultBootstrapContextcreateBootstrapContext()bootstrapContext.close(context)
ConfigurableEnvironmentgetOrCreateEnvironment()随 context
ConfigurableApplicationContextcreateApplicationContext()context.close()

BootstrapContext 的存活窗口是:从 run() 开头,到 contextPrepared 之后、load() 之前。

为什么在这个点关闭? 因为:

  1. contextPrepared 已经发过,ApplicationContextInitializer 已经跑完——它们可能需要 bootstrap 实例。
  2. load() 之后 BeanDefinition 就进 context 了,再需要共享对象就应该用 Spring bean 而不是 bootstrap 实例。
  3. BootstrapContextClosedEvent 让 bootstrap 实例有机会把自己注册成 Spring bean(见 §7.3)。

2. EventPublishingRunListener:从回调到 ApplicationEvent

2.1 它是什么

class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

    private final SpringApplication application;
    private final String[] args;
    private final SimpleApplicationEventMulticaster initialMulticaster;

    EventPublishingRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
    }

    @Override
    public int getOrder() {
        return 0;
    }
    ...
}

它是 spring.factories 里唯一注册的 SpringApplicationRunListener:

org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

它的职责:把七个回调翻译成七个 ApplicationEvent,并分发给 SpringApplication.getListeners() 里注册的监听器。

getOrder() 返回 0——它在 listener 列表中的位置由 SpringApplicationRunListeners 的遍历顺序决定。因为只有一个内置实现,顺序不敏感;但第三方 listener 追加进来时,0 意味着"中间位置"(@Order 更小者在前)。

2.2 七个回调 → 七个事件

回调发布的事件
startingApplicationStartingEvent
environmentPreparedApplicationEnvironmentPreparedEvent
contextPreparedApplicationContextInitializedEvent
contextLoadedApplicationPreparedEvent
startedApplicationStartedEvent(+ AvailabilityChangeEvent(LivenessState.CORRECT))
readyApplicationReadyEvent(+ AvailabilityChangeEvent(ReadinessState.ACCEPTING_TRAFFIC))
failedApplicationFailedEvent

2.3 initialMulticaster:为什么需要自己的多播器

private void multicastInitialEvent(ApplicationEvent event) {
    refreshApplicationListeners();
    this.initialMulticaster.multicastEvent(event);
}

private void refreshApplicationListeners() {
    this.application.getListeners().forEach(this.initialMulticaster::addApplicationListener);
}

问题:前四个事件(starting / environmentPrepared / contextPrepared / contextLoaded)发生时,ApplicationContext 还没有 refresh,它没有可用的 ApplicationEventMulticaster。

解决:EventPublishingRunListener 自带一个 SimpleApplicationEventMulticaster。

refreshApplicationListeners() 每次都重新添加——这是为了捕获"在启动过程中被动态添加的 listener"(比如某个 EnvironmentPostProcessor 调 application.addListeners(...))。

一个副作用:addApplicationListener 对 SmartApplicationListener 等实现是幂等的吗?Spring 的 AbstractApplicationEventMulticaster.addApplicationListener 内部用 LinkedHashSet(applicationListeners 是 LinkedHashSet),所以重复添加同一实例不会重复触发。✓

但 AbstractApplicationEventMulticaster 的 addApplicationListener 还会把 listener bean 名加入 applicationListenerBeans(Set<String>)。因为这里传的是实例不是 bean 名,无影响。

2.4 contextLoaded 的特殊性:把 listener 转交给 context

@Override
public void contextLoaded(ConfigurableApplicationContext context) {
    for (ApplicationListener<?> listener : this.application.getListeners()) {
        if (listener instanceof ApplicationContextAware contextAware) {
            contextAware.setApplicationContext(context);
        }
        context.addApplicationListener(listener);
    }
    multicastInitialEvent(new ApplicationPreparedEvent(this.application, this.args, context));
}

做了两件事,顺序很重要:

  1. 把 SpringApplication.getListeners() 的所有 listener 注册进 ApplicationContext
    • 若 listener 实现了 ApplicationContextAware,先注入 context
  2. 然后发布 ApplicationPreparedEvent

为什么是 contextLoaded 而不是更早? 因为:

  • contextPrepared 时 context 刚创建,还没 setEnvironment(实际上 prepareContext 里 setEnvironment 在 applyInitializers 之前,所以 contextPrepared 时已有 environment)
  • 但 listener 的注册需要 context 足够完整;而且 ApplicationPreparedEvent 的语义就是"上下文已准备好"

注册之后会发生什么? 从 ApplicationPreparedEvent 开始,监听器会收到两份通知吗?

不会。因为:ApplicationPreparedEvent 用 initialMulticaster 发(此时 listener 已被加进 context 的 multicaster,但 context 的 multicaster 在 refresh() 时才初始化)。

而 refresh() 之后(ApplicationStartedEvent),EventPublishingRunListener 改用 context.publishEvent(...):

@Override
public void started(ConfigurableApplicationContext context, @Nullable Duration timeTaken) {
    context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, timeTaken));
    AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}

分界线就是 contextLoaded:

  • starting / environmentPrepared / contextPrepared / contextLoaded → initialMulticaster
  • started / ready → context.publishEvent
  • failed → 视情况(见 2.5)

2.5 failed 的双路径

@Override
public void failed(@Nullable ConfigurableApplicationContext context, Throwable exception) {
    ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, this.args, context, exception);
    if (context != null && context.isActive()) {
        // Listeners have been registered to the application context so we should
        // use it at this point if we can
        context.publishEvent(event);
    }
    else {
        // An inactive context may not have a multicaster so we use our multicaster to
        // call all the context's listeners instead
        if (context instanceof AbstractApplicationContext abstractApplicationContext) {
            for (ApplicationListener<?> listener : abstractApplicationContext.getApplicationListeners()) {
                this.initialMulticaster.addApplicationListener(listener);
            }
        }
        this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
        this.initialMulticaster.multicastEvent(event);
    }
}

两条路径的判断依据:context != null && context.isActive()。

情形路径说明
上下文已激活(refresh 成功过)context.publishEvent(event)用 context 自己的多播器
上下文为 null 或未激活initialMulticaster把 context 已注册的 listener 借过来

"借过来"这一步很巧妙:

if (context instanceof AbstractApplicationContext abstractApplicationContext) {
    for (ApplicationListener<?> listener : abstractApplicationContext.getApplicationListeners()) {
        this.initialMulticaster.addApplicationListener(listener);
    }
}

即:context 已经 addApplicationListener 过(在 contextLoaded 里),但它的多播器还没初始化(因为没 refresh)。此时把那些 listener 拷到 initialMulticaster 里,用后者发事件。

为什么还要 setErrorHandler(new LoggingErrorHandler())? 因为失败处理期间,监听器自己抛异常不应该掩盖原始异常:

private static final class LoggingErrorHandler implements ErrorHandler {
    private static final Log logger = LogFactory.getLog(EventPublishingRunListener.class);

    @Override
    public void handleError(Throwable throwable) {
        logger.warn("Error calling ApplicationEventListener", throwable);
    }
}

这与 SpringApplicationRunListeners.callFailedListener 是同一个原则的两次实现(§1.5)。

注意:这个 setErrorHandler 是持久的——initialMulticaster 是实例字段,之后所有事件都会用这个 error handler。但因为 failed 之后不会再有事件,所以无副作用。

2.6 事件能拿到什么

七个事件都继承 SpringApplicationEvent:

public abstract class SpringApplicationEvent extends ApplicationEvent {

    private final String[] args;

    public SpringApplicationEvent(SpringApplication application, String[] args) {
        super(application);
        this.args = args;
    }

    public SpringApplication getSpringApplication() {
        return (SpringApplication) getSource();
    }

    public final String[] getArgs() {
        return this.args;
    }
}

提供 getSpringApplication()(getSource() 的类型安全包装)与 getArgs()。

事件额外提供
ApplicationStartingEventgetBootstrapContext()
ApplicationEnvironmentPreparedEventgetBootstrapContext()、getEnvironment()
ApplicationContextInitializedEventgetApplicationContext()
ApplicationPreparedEventgetApplicationContext()
ApplicationStartedEventgetApplicationContext()、getTimeTaken()
ApplicationReadyEventgetApplicationContext()、getTimeTaken()
ApplicationFailedEventgetApplicationContext()(可 null)、getException()

ApplicationFailedEvent 的 context 可能为 null——失败发生在 context 创建之前时。

2.7 事件的典型消费者

Boot 内部谁在听这些事件?

监听器监听的事件干什么
EnvironmentPostProcessorApplicationListenerApplicationEnvironmentPreparedEvent跑全部 EnvironmentPostProcessor(第 06 章)
LoggingApplicationListenerApplicationEnvironmentPreparedEvent 等初始化日志系统(第 24 章)
AnsiOutputApplicationListenerApplicationEnvironmentPreparedEvent配置 ANSI
FileEncodingApplicationListenerApplicationEnvironmentPreparedEvent校验 file.encoding
ClearCachesApplicationListenerContextRefreshedEvent清理反射缓存
ParentContextCloseApplicationListener多个父上下文联动关闭

从 spring.factories 可以看到完整列表:

org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.support.AnsiOutputApplicationListener,\
org.springframework.boot.support.EnvironmentPostProcessorApplicationListener

注意:这些是 ApplicationListener(注册到 SpringApplication.getListeners()),不是 SpringApplicationRunListener。它们通过 EventPublishingRunListener 的 refreshApplicationListeners() 被加进 initialMulticaster。

包名再次验证 4.x 迁移:AnsiOutputApplicationListener 和 EnvironmentPostProcessorApplicationListener 在 org.springframework.boot.support(3.x 是 org.springframework.boot.context)。


3. 可用性状态:Liveness 与 Readiness

3.1 started 与 ready 同时发布可用性事件

@Override
public void started(ConfigurableApplicationContext context, @Nullable Duration timeTaken) {
    context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, timeTaken));
    AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
}

@Override
public void ready(ConfigurableApplicationContext context, @Nullable Duration timeTaken) {
    context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, timeTaken));
    AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}

两个可用性状态在两个不同时点被设置:

状态值时点K8s 对应
LivenessState.CORRECT"存活"started(refresh 后、runners 前)liveness probe
ReadinessState.ACCEPTING_TRAFFIC"就绪"ready(runners 后)readiness probe

差别就是 callRunners——这就是为什么 CommandLineRunner 里做数据预热时,readiness 不会过早变绿。

3.2 AvailabilityChangeEvent

public class AvailabilityChangeEvent<S extends AvailabilityState> extends PayloadApplicationEvent<S> {

    ...
    public static <S extends AvailabilityState> void publish(ApplicationContext context, S state) {
        ...
    }

    public static <S extends AvailabilityState> void publish(ApplicationEventPublisher publisher, Object source,
            S state) {
        ...
    }
}

是 PayloadApplicationEvent 的子类——payload 就是状态对象。

availability 包的文件列表:

ApplicationAvailability.java
ApplicationAvailabilityBean.java
AvailabilityChangeEvent.java
AvailabilityState.java
LivenessState.java
ReadinessState.java

ApplicationAvailability 是"当前状态"的持有者,ApplicationAvailabilityBean 是把它暴露成 bean 的版本(供 Actuator 的 health 端点使用,第 25 章)。

3.3 为什么不用 @EventListener 直接监听

AvailabilityChangeEvent 用 static publish 方法而不是直接 context.publishEvent(new AvailabilityChangeEvent<>(...)),是因为它需要做类型推断 + 去重:

  • 推导出 AvailabilityState 的实现类型(LivenessState vs ReadinessState),只通知关心该类型的监听器
  • 如果状态没变化,可能跳过(具体实现我没读 AvailabilityChangeEvent 全文,标注为未核实)

实践用法(用户代码):

@Component
public class MyReadinessListener {

    @EventListener
    public void onReady(AvailabilityChangeEvent<ReadinessState> event) {
        if (event.getPayload() == ReadinessState.ACCEPTING_TRAFFIC) {
            // 可以接流量了
        }
    }
}

4. BootstrapRegistry:上下文之前的临时容器

4.1 为什么需要它

回到 §0 的困境。Boot 需要一个地方放"早期共享对象",要求是:

  1. 比 ApplicationContext 早
  2. 能表达"延迟创建"(不一定都要实例化)
  3. 能表达"作用域"(单例 or 每次新建)
  4. 能在上下文就绪后优雅交接(把自己注册成 bean)

BootstrapRegistry 就是这四个要求的答案。

4.2 接口方法

public interface BootstrapRegistry {

    <T> void register(Class<T> type, InstanceSupplier<T> instanceSupplier);

    <T> void registerIfAbsent(Class<T> type, InstanceSupplier<T> instanceSupplier);

    <T> boolean isRegistered(Class<T> type);

    <T> @Nullable InstanceSupplier<T> getRegisteredInstanceSupplier(Class<T> type);

    void addCloseListener(ApplicationListener<BootstrapContextClosedEvent> listener);
    ...
}

五个方法:

方法语义
register注册;若已注册且未实例化,则替换
registerIfAbsent仅在未注册时注册
isRegistered是否已注册
getRegisteredInstanceSupplier拿回 supplier(不触发实例化)
addCloseListener注册"关闭时"的监听器

register 的"且未实例化"限定是关键——见 §4.4。

4.3 InstanceSupplier 与 Scope

@FunctionalInterface
interface InstanceSupplier<T> {

    @Nullable T get(BootstrapContext context);

    default Scope getScope() {
        return Scope.SINGLETON;
    }

    default InstanceSupplier<T> withScope(Scope scope) {
        Assert.notNull(scope, "'scope' must not be null");
        InstanceSupplier<T> parent = this;
        return new InstanceSupplier<>() {
            @Override
            public @Nullable T get(BootstrapContext context) {
                return parent.get(context);
            }
            @Override
            public Scope getScope() {
                return scope;
            }
        };
    }

    static <T> InstanceSupplier<T> of(@Nullable T instance) {
        return (registry) -> instance;
    }

    static <T> InstanceSupplier<T> from(@Nullable Supplier<T> supplier) {
        return (registry) -> (supplier != null) ? supplier.get() : null;
    }
}

三个工厂方法:

方法用途Scope 默认
lambda (ctx) -> ...可访问 BootstrapContext(拿别的实例)SINGLETON
of(instance)固定实例SINGLETON
from(supplier)普通 Supplier 包装SINGLETON

withScope 是装饰器:包一层只改 getScope(),委托 get()。

Scope 枚举:

enum Scope {
    /**
     * A singleton instance. The InstanceSupplier will be called only once and
     * the same instance will be returned each time.
     */
    SINGLETON,

    /**
     * A prototype instance. The InstanceSupplier will be called whenever an
     * instance is needed.
     */
    PROTOTYPE
}

注意 PROTOTYPE 的语义:每次 get() 都调 supplier。这与 Spring bean 的 prototype 一致。

Scope.SINGLETON 是默认——大多数早期共享对象应该是单例。

4.4 BootstrapContext:读取侧

public interface BootstrapContext {

    <T> @Nullable T get(Class<T> type) throws IllegalStateException;

    <T> @Nullable T getOrElse(Class<T> type, @Nullable T other);

    <T> @Nullable T getOrElseSupply(Class<T> type, Supplier<@Nullable T> other);

    <T, X extends Throwable> @Nullable T getOrElseThrow(Class<T> type, Supplier<? extends X> exceptionSupplier)
            throws X;

    <T> boolean isRegistered(Class<T> type);
}

四个 get 变体:

方法未注册时
get(type)抛 IllegalStateException("... has not been registered")
getOrElse(type, other)返回 other
getOrElseSupply(type, supplier)调 supplier
getOrElseThrow(type, exceptionSupplier)抛 supplier 给的异常

ConfigurableBootstrapContext 是两者的合并:

public interface ConfigurableBootstrapContext extends BootstrapRegistry, BootstrapContext {
}

一个空接口,纯粹是"注册 + 读取"的组合视图。run() 里用的就是它。

4.5 DefaultBootstrapContext 的实现细节

public class DefaultBootstrapContext implements ConfigurableBootstrapContext {

    private final Map<Class<?>, InstanceSupplier<?>> instanceSuppliers = new HashMap<>();
    private final Map<Class<?>, Object> instances = new HashMap<>();
    private final ApplicationEventMulticaster events = new SimpleApplicationEventMulticaster();

    @Override
    public <T> void register(Class<T> type, InstanceSupplier<T> instanceSupplier) {
        register(type, instanceSupplier, true);
    }

    @Override
    public <T> void registerIfAbsent(Class<T> type, InstanceSupplier<T> instanceSupplier) {
        register(type, instanceSupplier, false);
    }

    private <T> void register(Class<T> type, InstanceSupplier<T> instanceSupplier, boolean replaceExisting) {
        Assert.notNull(type, "'type' must not be null");
        Assert.notNull(instanceSupplier, "'instanceSupplier' must not be null");
        synchronized (this.instanceSuppliers) {
            boolean alreadyRegistered = this.instanceSuppliers.containsKey(type);
            if (replaceExisting || !alreadyRegistered) {
                Assert.state(!this.instances.containsKey(type), () -> type.getName() + " has already been created");
                this.instanceSuppliers.put(type, instanceSupplier);
            }
        }
    }
    ...
}

五个要点:

(1) 两个 Map
  • instanceSuppliers:类型 → supplier(未实例化)
  • instances:类型 → 实例(已实例化,仅 SINGLETON 会放进来)
(2) "已实例化就不能再注册"
Assert.state(!this.instances.containsKey(type), () -> type.getName() + " has already been created");

这是一个强约束:一旦某个类型被 get() 过(且是 SINGLETON),就不能再 register 替换它。

为什么? 因为可能已经有代码持有旧实例的引用了。此时替换会造成"同一个类型有两个不同实例"的混乱状态。

这条约束的可观测后果:

bootstrapContext.register(Foo.class, InstanceSupplier.of(foo1));
Foo f = bootstrapContext.get(Foo.class);       // f == foo1,且 instances[Foo] = foo1
bootstrapContext.register(Foo.class, InstanceSupplier.of(foo2));   // ✗ IllegalStateException

而如果没 get() 过:

bootstrapContext.register(Foo.class, InstanceSupplier.of(foo1));
bootstrapContext.register(Foo.class, InstanceSupplier.of(foo2));   // ✓ 替换成功
Foo f = bootstrapContext.get(Foo.class);       // f == foo2

这正是第 10 章 §4.4 registerBootstrapBinder 能反复重注册 Binder.class 的原因!

private void registerBootstrapBinder(ConfigDataEnvironmentContributors contributors,
        @Nullable ConfigDataActivationContext activationContext, BinderOption... binderOptions) {
    this.bootstrapContext.register(Binder.class,
            InstanceSupplier.from(() -> contributors.getBinder(activationContext, binderOptions))
                .withScope(Scope.PROTOTYPE));
}

它用了 Scope.PROTOTYPE——所以 instances 里永远不会有 Binder 的条目,Assert.state 永远通过,可以无限次重注册。

这是 PROTOTYPE 的一个精妙用法:用作用域来规避"已实例化不可替换"的约束,实现"每次拿到的都是最新快照"。

(3) synchronized 的用法
synchronized (this.instanceSuppliers) { ... }

所有访问都锁 instanceSuppliers——包括 instances 的访问(getInstance 在 getOrElseThrow 的同步块里被调用)。

不一致之处:instances 本身不是锁对象,但因为所有访问路径都在 instanceSuppliers 的锁内,所以逻辑上是安全的。

为什么需要锁? 因为 ConfigData 的处理可能涉及多线程(尤其是自定义 resolver 做并发 IO)。

(4) getInstance 的懒加载
@SuppressWarnings("unchecked")
private <T> @Nullable T getInstance(Class<T> type, InstanceSupplier<?> instanceSupplier) {
    T instance = (T) this.instances.get(type);
    if (instance == null) {
        instance = (T) instanceSupplier.get(this);
        if (instance == null) {
            return null;
        }
        if (instanceSupplier.getScope() == Scope.SINGLETON) {
            this.instances.put(type, instance);
        }
    }
    return instance;
}

instance == null 时不缓存——所以 SINGLETON 的 supplier 若返回 null,每次都会重新调用 supplier。这是一个小细节,但对"故意返回 null 表示不可用"的场景有影响。

(5) close
public void close(ConfigurableApplicationContext applicationContext) {
    this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext));
}

只是发一个事件,不做任何清理(instanceSuppliers / instances 不清空)。

为什么不清空? 因为 close 之后可能还有代码持有 bootstrapContext 引用并 get()。Boot 选择"软关闭"——发事件通知交接,但不切断访问。

4.6 BootstrapContextClosedEvent

public class BootstrapContextClosedEvent extends ApplicationEvent {

    private final ConfigurableApplicationContext applicationContext;

    BootstrapContextClosedEvent(BootstrapContext source, ConfigurableApplicationContext applicationContext) {
        super(source);
        this.applicationContext = applicationContext;
    }

    public BootstrapContext getBootstrapContext() {
        return (BootstrapContext) this.source;
    }

    public ConfigurableApplicationContext getApplicationContext() {
        return this.applicationContext;
    }
}

两个 getter——这是"交接"的关键:拿到已 prepared 的 ApplicationContext,就可以往里面注册 bean。

注意构造器是包级私有——只有 DefaultBootstrapContext(同包)能创建。外部只能监听。

4.7 4.x 的包名迁移

BootstrapRegistry 等接口的 Javadoc 都标了 @since 4.0.0,但 BootstrapContext 上还有 @since 2.4.0:

 * @author Phillip Webb
 * @since 4.0.0
 * @since 2.4.0

(这是我从 grep 输出里看到的连续两行 @since——我未确认原始 Javadoc 的确切形态,推测是"接口在 4.0.0 被移到新包,原逻辑自 2.4.0 存在"。)

4.x 的包是 org.springframework.boot.bootstrap(3.x 是 org.springframework.boot)。这与第 01 章总结的"4.x 包名迁移"规律一致:

3.x4.x
org.springframework.boot.BootstrapRegistryorg.springframework.boot.bootstrap.BootstrapRegistry
org.springframework.boot.BootstrapContextorg.springframework.boot.bootstrap.BootstrapContext
org.springframework.boot.BootstrapRegistryInitializerorg.springframework.boot.bootstrap.BootstrapRegistryInitializer
org.springframework.boot.env.EnvironmentPostProcessorApplicationListenerorg.springframework.boot.support.*

4.8 BootstrapRegistryInitializer

@FunctionalInterface
public interface BootstrapRegistryInitializer {

    void initialize(BootstrapRegistry registry);
}

用途:在 BootstrapContext 创建后立刻做初始化注册。

调用点:

private DefaultBootstrapContext createBootstrapContext() {
    DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
    this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
    return bootstrapContext;
}

注入方式:SpringApplication.addBootstrapRegistryInitializer(...)。

注意它拿到的是 BootstrapRegistry(写视图),不是 ConfigurableBootstrapContext——所以在 initializer 里不能 get(),只能注册。

这是一个刻意的设计:初始化阶段只允许注册,不允许读取,避免初始化顺序依赖。


5. 完整时序:BootstrapContext 的一生

① run() 开头
   createBootstrapContext()
     ├─ new DefaultBootstrapContext()
     └─ bootstrapRegistryInitializers.forEach(init -> init.initialize(bootstrapContext))
        ★ 此时只能注册,不能 get

② getRunListeners(args)
   listeners.starting(bootstrapContext, mainClass)
     ├─ ApplicationStartingEvent(bootstrapContext, application, args)
     └─ 监听器可以 bootstrapContext.get(...)  → 触发实例化

③ prepareEnvironment()
   ConfigurationPropertySources.attach(environment)
   listeners.environmentPrepared(bootstrapContext, environment)
     └─ ApplicationEnvironmentPreparedEvent(bootstrapContext, ...)
        → EnvironmentPostProcessorApplicationListener
           → ConfigDataEnvironmentPostProcessor
              → ConfigDataEnvironment
                 ├─ 每个阶段后 registerBootstrapBinder(...)
                 │    → bootstrapContext.register(Binder.class, PROTOTYPE)
                 └─ 自定义 ConfigDataLocationResolver 可
                    context.getBootstrapContext().get(MyClient.class)
   bindToSpringApplication(environment)

④ createApplicationContext()
   prepareContext()
     ├─ applyInitializers(context)
     │    → ApplicationContextInitializer 仍可访问 bootstrapContext(如果有引用)
     ├─ listeners.contextPrepared(context)
     │    └─ ApplicationContextInitializedEvent
     └─ bootstrapContext.close(context)
          └─ BootstrapContextClosedEvent(bootstrapContext, context)
             → 监听器:把自己注册成 Spring bean

⑤ load(context, sources)   ← BeanDefinition 加载,bootstrap 已经"退休"
   listeners.contextLoaded(context)

关键观察:

  1. BootstrapContext 的"活跃期"覆盖了整个 Environment 构建阶段——包括 ConfigData 的全部三轮处理(第 10 章)。这让自定义 ConfigDataLocationResolver 能拿到早期共享实例(比如 HTTP 客户端)。

  2. Binder.class 在 bootstrap 里被反复重注册(第 10 章 §4.4),每次都是最新的 contributors 快照 + PROTOTYPE 作用域。

  3. 关闭点在 contextPrepared 之后——ApplicationContextInitializer 还能用 bootstrap 实例(如果它持有引用),但 load() 就不行了。


6. 实战:用 BootstrapRegistry 做早期共享实例

Boot 的 smoke test 里有一个完整的例子:spring-boot-smoke-test-bootstrap-registry,模拟"从 Subversion 拉取配置"。

6.1 场景

配置位置语法:svn:host/path
需要从 Subversion 服务器拉取配置文件
→ 需要在 ConfigData 阶段有一个可用的 SVN 客户端
→ SVN 客户端的创建需要证书等资源,不应该每次都新建
→ 但 ApplicationContext 还不存在,没法用 Spring bean

6.2 代码走读

Step 1:定义"昂贵的"客户端

// smoke-test/.../external/svn/SubversionClient.java(简化)
public class SubversionClient {

    private final SubversionServerCertificate certificate;

    SubversionClient(SubversionServerCertificate certificate) {
        this.certificate = certificate;
    }

    public String fetchFile(String path) {
        // 实际的网络调用
    }
}

Step 2:BootstrapRegistryInitializer 注册它

// smoke-test/.../external/svn/SubversionBootstrap.java(简化)
public class SubversionBootstrap implements BootstrapRegistryInitializer {

    @Override
    public void initialize(BootstrapRegistry registry) {
        registry.register(SubversionServerCertificate.class,
                InstanceSupplier.from(SubversionServerCertificate::load));
        registry.register(SubversionClient.class, (context) ->
                new SubversionClient(context.get(SubversionServerCertificate.class)));
    }
}

注意第二行的 context.get(...)——InstanceSupplier.get(BootstrapContext) 参数就是为了这个:创建 A 时可能需要 B,而 B 也在 bootstrap 里。

Step 3:在 ConfigDataLocationResolver 里用它

// smoke-test/.../external/svn/SubversionConfigDataLocationResolver.java(简化)
public class SubversionConfigDataLocationResolver
        implements ConfigDataLocationResolver<SubversionConfigDataResource> {

    @Override
    public List<SubversionConfigDataResource> resolve(ConfigDataLocationResolverContext context,
            ConfigDataLocation location) {
        SubversionClient client = context.getBootstrapContext().get(SubversionClient.class);
        // 用 client 列举远端文件,返回 resource 列表
    }
}

Step 4:在 ConfigDataLoader 里用它

// smoke-test/.../external/svn/SubversionConfigDataLoader.java(简化)
public class SubversionConfigDataLoader implements ConfigDataLoader<SubversionConfigDataResource> {

    @Override
    public ConfigData load(ConfigDataLoaderContext context, SubversionConfigDataResource resource) {
        SubversionClient client = context.getBootstrapContext().get(SubversionClient.class);
        String content = client.fetchFile(resource.getPath());
        // 解析 content → ConfigData
    }
}

Step 5:注册 initializer

// smoke-test/.../SampleBootstrapRegistryApplication.java(简化)
@SpringBootApplication
public class SampleBootstrapRegistryApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(SampleBootstrapRegistryApplication.class)
            .bootstrapRegistryInitializers(new SubversionBootstrap())
            .run(args);
    }
}

Step 6:把客户端交接给 Spring(可选)

// 在 SubversionBootstrap 里
registry.addCloseListener((event) -> {
    ConfigurableApplicationContext applicationContext = event.getApplicationContext();
    applicationContext.getBeanFactory().registerSingleton("subversionClient",
            event.getBootstrapContext().get(SubversionClient.class));
});

这就是 BootstrapContextClosedEvent 的用途——把 bootstrap 实例"晋升"为 Spring bean,之后应用代码就能 @Autowired SubversionClient。

6.3 三个可复用的模式

模式做法适用
纯早期(用完即弃)只注册,不 addCloseListener只在 ConfigData 阶段需要(如临时 HTTP 客户端)
交接为 beanaddCloseListener + registerSingleton应用代码也要用
懒加载用 InstanceSupplier.from(Supplier) 而不是 of(instance)创建昂贵,且可能不需要

6.4 陷阱清单

  1. initialize 里不能 get()——参数是 BootstrapRegistry,没有 get 方法。想在注册时就实例化,只能自己调 supplier。

  2. register 的顺序无关——因为是懒加载。register(A, ctx -> new A(ctx.get(B))) 可以在 register(B, ...) 之前写。

  3. "已实例化不可替换"(§4.5)——注册顺序在第一次 get() 之后就固定了。

  4. close 不清空——BootstrapContextClosedEvent 之后仍可 get()。别依赖"关闭后不可用"的假设。

  5. 线程安全——DefaultBootstrapContext 用 synchronized,但你的 InstanceSupplier 实现本身可能不是线程安全的。SINGLETON 只调一次,PROTOTYPE 可能被并发调用。


7. 七个事件的详细规格

7.1 ApplicationStartingEvent

multicastInitialEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));

可用:BootstrapContext(新建,已跑完 initializer) 不可用:Environment、ApplicationContext

典型用途:

  • 最早期的日志/监控埋点
  • 修改 SpringApplication 的属性(因为还没 bindToSpringApplication)
  • 注册额外的 bootstrap 实例

7.2 ApplicationEnvironmentPreparedEvent

可用:BootstrapContext、ConfigurableEnvironment(ConfigurationPropertySources 已 attach) 不可用:ApplicationContext

这是最重要的事件——EnvironmentPostProcessor 全在这里跑(第 06 章)。

注意 ConfigurationPropertySources.attach(environment) 已经执行——所以 environment.getProperty("spring.datasource.url") 能走 relaxed binding 的完整路径。

但 spring.main.* 还没绑定到 SpringApplication——见 §1.2。

7.3 ApplicationContextInitializedEvent

可用:ApplicationContext(已 setEnvironment + applyInitializers,未 load) 不可用:BeanDefinition

典型用途:在 BeanDefinition 加载前做最后的上下文配置。

7.4 ApplicationPreparedEvent

可用:ApplicationContext(已 load,未 refresh) 不可用:已实例化的 bean

这是修改 BeanDefinition 的最后机会(BeanFactoryPostProcessor 也在 refresh 里跑,但此事件更早)。

注意:EventPublishingRunListener.contextLoaded 在这一刻把 application.getListeners() 全部注册进 context——所以从下一个事件开始,context 自己的多播器会接管。

7.5 ApplicationStartedEvent

可用:完整刷新的 ApplicationContext、Duration timeTaken 附加:AvailabilityChangeEvent(LivenessState.CORRECT)

此时 runner 还没跑。

7.6 ApplicationReadyEvent

可用:完整 context、runner 已跑完 附加:AvailabilityChangeEvent(ReadinessState.ACCEPTING_TRAFFIC)

这是"应用真正可用"的信号。

注意 run() 里的判断:

try {
    if (context.isRunning()) {
        listeners.ready(context, startup.ready());
    }
}
catch (Throwable ex) {
    throw handleRunFailure(context, ex, null);
}

只有 context.isRunning() 为 true 才发 ready。若 context 在 refresh 后、ready 前被关闭(比如 callRunners 里调 context.close()),ready 不发。

7.7 ApplicationFailedEvent

可用:context(可能为 null)、exception

发布路径:见 §2.5 双路径。

典型用途:错误上报(Sentry 等)、清理外部资源。

注意:failed 回调里抛异常不会中断流程(§1.5 + §2.5 的双重保护)。


8. CRaC:让"启动"可快照

8.1 什么是 CRaC

CRaC = Coordinated Restore at Checkpoint(源自 Azul 的 OpenJDK 项目)。

核心思想:

  1. 应用启动完成后,对 JVM 做一次 checkpoint(快照到磁盘)
  2. 之后启动新实例时,直接 restore(恢复)这个快照,跳过所有初始化
  3. 启动时间从秒级降到毫秒级

代价:应用必须"协调"——在 checkpoint 前关闭不能序列化的资源(网络连接、文件句柄),在 restore 后重新打开。这就是 "Coordinated" 的含义。

8.2 Boot 的支持:Startup 抽象

abstract static class Startup {

    private @Nullable Duration timeTakenToStarted;

    protected abstract long startTime();

    protected abstract @Nullable Long processUptime();

    protected abstract String action();

    final Duration started() {
        long now = System.currentTimeMillis();
        this.timeTakenToStarted = Duration.ofMillis(now - startTime());
        return this.timeTakenToStarted;
    }

    Duration timeTakenToStarted() {
        Assert.state(this.timeTakenToStarted != null,
                "timeTakenToStarted is not set. Make sure to call started() before this method");
        return this.timeTakenToStarted;
    }

    private Duration ready() {
        long now = System.currentTimeMillis();
        return Duration.ofMillis(now - startTime());
    }

    static Startup create() {
        ClassLoader classLoader = Startup.class.getClassLoader();
        return (ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", classLoader)
                && ClassUtils.isPresent("org.crac.management.CRaCMXBean", classLoader))
                        ? new CoordinatedRestoreAtCheckpointStartup() : new StandardStartup();
    }
}

三个抽象方法:

方法作用
startTime()计算"启动开始时间"(用于算 timeTaken)
processUptime()JVM 进程运行时长
action()日志里的动作词("Started" vs "Restored")

create() 的双条件探测:

ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", classLoader)
    && ClassUtils.isPresent("org.crac.management.CRaCMXBean", classLoader)

两个类都要存在:

  • jdk.crac.management.CRaCMXBean —— JDK 内置的(只有 CRaC 版 JDK 才有)
  • org.crac.management.CRaCMXBean —— org.crac 库提供的(需要额外依赖)

为什么要两个? 因为 jdk.crac.* 是 JDK 内部 API,编译时不一定可见;org.crac.* 是可以正常编译依赖的 shim。运行时两者都在才说明 CRaC 真的可用。

8.3 StandardStartup

private static final class StandardStartup extends Startup {

    private final Long startTime = System.currentTimeMillis();

    @Override
    protected long startTime() {
        return this.startTime;
    }

    @Override
    protected @Nullable Long processUptime() {
        try {
            return ManagementFactory.getRuntimeMXBean().getUptime();
        }
        catch (Throwable ex) {
            return null;
        }
    }

    @Override
    protected String action() {
        return "Started";
    }
}

startTime 在字段初始化时取值——即 Startup.create() 被调用的那一刻(在 run() 的第一行)。

processUptime 有异常兜底——某些受限环境(如某些容器)无法访问 RuntimeMXBean。

action() 返回 "Started" → 日志里是 Started MyApp in 3.456 seconds。

8.4 CoordinatedRestoreAtCheckpointStartup

private static final class CoordinatedRestoreAtCheckpointStartup extends Startup {

    private final StandardStartup fallback = new StandardStartup();

    @Override
    protected @Nullable Long processUptime() {
        Long uptime = CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore();
        return (uptime >= 0) ? uptime : this.fallback.processUptime();
    }

    @Override
    protected String action() {
        return (restoreTime() >= 0) ? "Restored" : this.fallback.action();
    }

    private long restoreTime() {
        return CRaCMXBean.getCRaCMXBean().getRestoreTime();
    }

    @Override
    protected long startTime() {
        long restoreTime = restoreTime();
        return (restoreTime >= 0) ? restoreTime : this.fallback.startTime();
    }
}

核心判据:getRestoreTime() 是否 >= 0。

getRestoreTime()含义行为
>= 0本次是从 checkpoint 恢复的用 restore 时间作为 startTime;日志动作词 = "Restored"
< 0本次是正常启动(还没 checkpoint 过)回退到 StandardStartup

startTime() 用 restoreTime —— 这样 timeTaken = now - restoreTime 就是"恢复耗时",而不是"原始启动 + 恢复"的总时间。这正是 CRaC 要展示的价值。

日志输出差异:

# 正常启动
Started MyApp in 3.456 seconds (JVM running for 4.123)

# 从 checkpoint 恢复
Restored MyApp in 0.045 seconds (JVM running for 0.089)

fallback 是组合而非继承——CoordinatedRestoreAtCheckpointStartup 持有一个 StandardStartup 实例,而不是继承它。这让"回退"逻辑非常清晰。

8.5 @ConditionalOnCheckpointRestore

@Target({ ElementType.TYPE, METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConditionalOnClass(name = "org.crac.Resource")
public @interface ConditionalOnCheckpointRestore {

}

@since 3.2.0。

它只是一个 @ConditionalOnClass(name = "org.crac.Resource") 的语义化包装——没有任何自己的条件逻辑。

用途:标记"只在 CRaC 环境下才需要的配置"。

@Configuration(proxyBeanMethods = false)
@ConditionalOnCheckpointRestore
public class CracConfiguration {

    @Bean
    public MyResourceHolder myResourceHolder() {
        // 注册 org.crac.Resource,在 beforeCheckpoint / afterRestore 时开关资源
    }
}

注意它用的是 @ConditionalOnClass(name = ...) 而不是 value = ...:

  • value 是 Class<?>[](编译期必须可见)
  • name 是 String[](只在运行时检查,编译期不需要依赖)

因为 org.crac 是 optional 依赖,用 name 才能避免在普通环境里 NoClassDefFoundError。

(第 14/15 章会详细讲 @ConditionalOnClass 的这两种形态。)

8.6 CRaC 与配置体系的冲突

这是本章最值得思考的一点。CRaC 与前面几章讲的配置机制有根本性的张力:

配置机制CRaC 下的行为问题
application.yaml(classpath)被快照进 checkpoint启动时改配置无效(除非重新打包)
file:./config/恢复后重新读?取决于是否在 restore 后重新触发
configtree:符号链接指向的 ..data 目录在 restore 后变了文件可能不存在
env: 环境变量恢复后的环境变量是新的快照里的值是旧的
RandomValuePropertySource快照时固定恢复后所有实例共享同一个随机值
spring.profiles.active快照时固定恢复后无法改
SSL / 网络连接快照时必须关闭不协调会 restore 失败

Boot 的应对(我的观察,部分为推断):

  1. Startup 抽象(§8.2)——解决"启动耗时"的语义,让日志正确。
  2. @ConditionalOnCheckpointRestore——让用户能写"只在 CRaC 下的配置"。
  3. spring.main.keep-alive(见 8.7)——保持 JVM 不退出,便于做 checkpoint。

但 Boot 没有(也不能)自动解决"配置陈旧"问题——这需要应用自己用 org.crac.Resource 回调在 afterRestore 时重新读取。

实践建议:

  • 不要把会变的配置放在 classpath 的 application.yaml 里——用 ConfigMap / 环境变量 / configtree
  • configtree 用 ALWAYS_READ(第 11 章 §2.7)——这样 restore 后能读到新内容
  • 随机值、实例 ID 之类的要在 afterRestore 里重置

8.7 KeepAlive:为了让 JVM 活到 checkpoint

private static final class KeepAlive implements ApplicationListener<ApplicationContextEvent> {

    private final AtomicReference<@Nullable Thread> thread = new AtomicReference<>();

    @Override
    public void onApplicationEvent(ApplicationContextEvent event) {
        if (event instanceof ContextRefreshedEvent) {
            startKeepAliveThread();
        }
        else if (event instanceof ContextClosedEvent) {
            stopKeepAliveThread();
        }
    }

    private void startKeepAliveThread() {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(Long.MAX_VALUE);
                }
                catch (InterruptedException ex) {
                    break;
                }
            }
        });
        if (this.thread.compareAndSet(null, thread)) {
            thread.setDaemon(false);
            thread.setName("keep-alive");
            thread.start();
        }
    }

    private void stopKeepAliveThread() {
        Thread thread = this.thread.getAndSet(null);
        if (thread == null) {
            return;
        }
        thread.interrupt();
    }
}

安装点:

if (this.properties.isKeepAlive()) {
    context.addApplicationListener(new KeepAlive());
}

作用:启动一个非 daemon线程,它的唯一任务是 Thread.sleep(Long.MAX_VALUE)。

为什么需要? 因为 JVM 在所有非 daemon 线程结束后就退出。如果你启动应用只是为了做 checkpoint(jcmd <pid> JDK.checkpoint),但应用没有 web 服务器或任何非 daemon 线程,JVM 会立刻退出,来不及做 checkpoint。

thread.setDaemon(false)——显式非 daemon。

AtomicReference + compareAndSet——保证只启动一次(即使收到多个 ContextRefreshedEvent)。

ContextClosedEvent 时 interrupt——优雅退出。

开启方式:spring.main.keep-alive=true 或 SpringApplication.setKeepAlive(true)。


9. 错误处理链:handleRunFailure

9.1 五步

private RuntimeException handleRunFailure(@Nullable ConfigurableApplicationContext context, Throwable exception,
        @Nullable SpringApplicationRunListeners listeners) {
    if (exception instanceof AbandonedRunException abandonedRunException) {
        return abandonedRunException;
    }
    try {
        try {
            handleExitCode(context, exception);
            if (listeners != null) {
                listeners.failed(context, exception);
            }
        }
        finally {
            reportFailure(getExceptionReporters(context), exception);
            if (context != null) {
                context.close();
                shutdownHook.deregisterFailedApplicationContext(context);
            }
        }
    }
    catch (Exception ex) {
        logger.warn("Unable to close ApplicationContext", ex);
    }
    return (exception instanceof RuntimeException runtimeException) ? runtimeException
            : new IllegalStateException(exception);
}

顺序(注意 try/finally):

1. AbandonedRunException 特判 → 直接返回
2. try {
     2a. handleExitCode(context, exception)       ← 设置退出码(供 ExitCodeGenerator)
     2b. listeners.failed(context, exception)     ← 发 ApplicationFailedEvent
   } finally {
     2c. reportFailure(...)                       ← FailureAnalyzer 输出诊断
     2d. context.close()                          ← 关闭上下文
     2e. shutdownHook.deregisterFailedApplicationContext(context)
   }
3. catch → 只 warn
4. 规范异常类型(非 RuntimeException 包成 IllegalStateException)

关键:reportFailure 在 finally 里——即使 listeners.failed 抛异常,诊断信息也会输出。

9.2 AbandonedRunException

if (exception instanceof AbandonedRunException abandonedRunException) {
    return abandonedRunException;
}

直接返回,不做任何处理。语义:"这次 run 是被主动放弃的(比如 SpringApplication 在 environmentPrepared 阶段被外部代码终止),不需要报错"。

典型场景:SpringApplicationBuilder 的某些用法,或者测试框架主动中止。

AbandonedRunException 的具体定义我未在本章阅读——标注待查。

9.3 handleExitCode

遍历 context 里的 ExitCodeGenerator bean,取最大退出码。第 04 章讲过。

9.4 reportFailure 与 FailureAnalyzer

private Collection<SpringBootExceptionReporter> getExceptionReporters(
        @Nullable ConfigurableApplicationContext context) {
    try {
        ArgumentResolver argumentResolver = (context != null)
                ? ArgumentResolver.of(ConfigurableApplicationContext.class, context) : ArgumentResolver.none();
        return getSpringFactoriesInstances(SpringBootExceptionReporter.class, argumentResolver);
    }
    catch (Throwable ex) {
        return Collections.emptyList();
    }
}

从 spring.factories 加载 SpringBootExceptionReporter(唯一实现是 FailureAnalyzers),注入 ConfigurableApplicationContext(可为 null,此时用 ArgumentResolver.none())。

这就是"启动失败时那个漂亮的诊断报告"的来源(第 07 章 §诊断、第 11 章 §7.4 都是它的具体 analyzer)。

9.5 context.close() 与 shutdownHook

if (context != null) {
    context.close();
    shutdownHook.deregisterFailedApplicationContext(context);
}

失败时也要关 context——释放资源(连接池、文件句柄)。

deregisterFailedApplicationContext —— 从 shutdown hook 里移除,避免重复关闭。


10. SpringApplicationHook:4.x 的测试友好能力

@FunctionalInterface
public interface SpringApplicationHook {

    @Nullable SpringApplicationRunListener getRunListener(SpringApplication springApplication);
}

@since 3.0.0。

用途:在不改 spring.factories 的前提下,给某次 run() 附加一个 listener。

"per-thread basis"(Javadoc 明确写了):

Hooks are managed on a per-thread basis providing isolation when multiple applications are executed in parallel.

这是为测试设计的——并行跑多个 @SpringBootTest 时,每个测试线程可以有自己的 hook,互不干扰。

使用:

try {
    SpringApplication.withHook(myHook, () -> {
        SpringApplication.run(MyApp.class, args);
    });
}

(withHook 的确切签名我未核实——Javadoc 里提到了 @see SpringApplication#withHook。)

hook listener 被追加在最后:

if (hookListener != null) {
    listeners = new ArrayList<>(listeners);
    listeners.add(hookListener);
}

配合 EventPublishingRunListener 的 getOrder() == 0,hook listener 会在内置监听器之后收到通知。


11. 反直觉结论(本章十四条)

11.1 environmentPrepared 时 spring.main.* 还没绑定到 SpringApplication

bindToSpringApplication(environment) 在回调之后。想读 spring.main.* 请直接读 environment。

11.2 spring.main.environment-prefix 不能通过配置文件设置

Boot 有硬断言:Assert.state(!environment.containsProperty("spring.main.environment-prefix"), ...)。只能用 setEnvironmentPrefix()——因为前缀要在 Environment 构建前生效(第 11 章 §5.5)。

11.3 started 与 ready 之间夹着 callRunners

所以 ApplicationRunner 抛异常时:started 已发、ApplicationReadyEvent 不发、readiness 不变绿。

11.4 ready 只在 context.isRunning() 时发

若 runner 里关了 context,ready 静默不发。

11.5 前四个事件用自己的多播器,后两个用 context 的

分界在 contextLoaded:EventPublishingRunListener 在这一刻把 application.getListeners() 注册进 context,之后 started/ready 改用 context.publishEvent。

11.6 failed 有两条发布路径

context.isActive() 为真 → context.publishEvent;否则 → 把 context 的 listener 借给 initialMulticaster。

11.7 failed 回调里抛异常不会中断原始失败

双重保护:SpringApplicationRunListeners.callFailedListener 只记日志;EventPublishingRunListener 设 LoggingErrorHandler。

原则:诊断工具坏了,不能掩盖真正的错误。

11.8 reportFailure 在 finally 里

即使 listeners.failed 抛异常,诊断报告也会输出。

11.9 BootstrapContext 在 contextPrepared 之后、load() 之前关闭

关闭点是刻意的:让 ApplicationContextInitializer 还能用 bootstrap 实例,让 BootstrapContextClosedEvent 的监听器能把自己注册成 bean(赶在 BeanDefinition 加载之前)。

11.10 register 在实例已创建后会抛 IllegalStateException

Assert.state(!this.instances.containsKey(type), ...)。

规避方法:用 Scope.PROTOTYPE——instances 里永远没有条目。这正是 registerBootstrapBinder 反复重注册 Binder.class 的原因(第 10 章 §4.4)。

11.11 BootstrapContextInitializer 拿到的是 BootstrapRegistry(写视图)

不能 get()。这是刻意的:初始化阶段只允许注册,避免顺序依赖。

11.12 bootstrapContext.close() 不清空任何 Map

只发一个事件。之后 get() 仍然可用。别依赖"关闭后不可用"。

11.13 CRaC 下"启动耗时"是恢复耗时,不是原始启动耗时

CoordinatedRestoreAtCheckpointStartup.startTime() 返回 getRestoreTime()。日志动词也从 "Started" 变成 "Restored"。

11.14 CRaC 会让"快照时的配置"固化

classpath 配置、随机值、实例 ID 都会被冻结。会变的配置必须走 ConfigMap / 环境变量 / configtree(且开 ALWAYS_READ)。

11.15(补)KeepAlive 是一个"什么都不做的非 daemon 线程"

Thread.sleep(Long.MAX_VALUE)。它存在的唯一目的是让 JVM 不退出,以便做 checkpoint。


12. 动手验证清单

12.1 观察七个事件的完整时序

@Component
public class LifecycleLogger implements ApplicationListener<SpringApplicationEvent> {

    private static final Log logger = LogFactory.getLog(LifecycleLogger.class);

    @Override
    public void onApplicationEvent(SpringApplicationEvent event) {
        logger.info(">>> " + event.getClass().getSimpleName()
            + " | context=" + (event instanceof ApplicationStartingEvent ? "null" : "..."));
    }
}

注册:new SpringApplicationBuilder(App.class).listeners(new LifecycleLogger()).run(args)。

再把 logging.level.org.springframework.boot.context.config=TRACE 打开,对照 §1.6 的时序图。

12.2 验证 bindToSpringApplication 的时机

@Component
public class MainPropertiesProbe implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {

    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        SpringApplication app = event.getSpringApplication();
        System.out.println("from SpringApplication: " + app.getProperties().getWebApplicationType());
        System.out.println("from Environment:      "
            + event.getEnvironment().getProperty("spring.main.web-application-type"));
    }
}

application.properties 里写 spring.main.web-application-type=none。

期望:两行输出不一致——SpringApplication 的还是推断值,Environment 的是 none。

12.3 验证 started 与 ready 的间隔

@Component
public class SlowRunner implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        Thread.sleep(5000);
    }
}

再监听两个事件打时间戳。期望相差 ≥ 5 秒。

12.4 验证 BootstrapContext 的"已实例化不可替换"

DefaultBootstrapContext ctx = new DefaultBootstrapContext();
ctx.register(String.class, InstanceSupplier.of("a"));
System.out.println(ctx.get(String.class));       // a
ctx.register(String.class, InstanceSupplier.of("b"));   // ✗ IllegalStateException

对照:

DefaultBootstrapContext ctx = new DefaultBootstrapContext();
ctx.register(String.class, InstanceSupplier.of("a"));
ctx.register(String.class, InstanceSupplier.of("b"));   // ✓ 替换成功(还没 get)
System.out.println(ctx.get(String.class));       // b

以及 PROTOTYPE 规避:

ctx.register(String.class, InstanceSupplier.from(() -> "x").withScope(Scope.PROTOTYPE));
System.out.println(ctx.get(String.class));
ctx.register(String.class, InstanceSupplier.from(() -> "y").withScope(Scope.PROTOTYPE));   // ✓
System.out.println(ctx.get(String.class));       // y

12.5 验证 BootstrapContextClosedEvent 能注册 bean

public class MyBootstrap implements BootstrapRegistryInitializer {

    @Override
    public void initialize(BootstrapRegistry registry) {
        registry.register(MyClient.class, InstanceSupplier.from(MyClient::new));
        registry.addCloseListener((event) -> {
            event.getApplicationContext().getBeanFactory()
                .registerSingleton("myClient", event.getBootstrapContext().get(MyClient.class));
        });
    }
}

然后在任意 @Component 里 @Autowired MyClient——应该能注入。

12.6 验证 ConfigData 阶段能拿到 bootstrap 实例

写一个自定义 ConfigDataLocationResolver,在 resolve() 里:

MyClient client = context.getBootstrapContext().get(MyClient.class);
System.out.println("got client in resolver: " + client);

若 MyClient 未注册,应抛 IllegalStateException("... has not been registered")。

12.7 验证 KeepAlive

java -jar app.jar --spring.main.keep-alive=true &
jps -l          # 找 pid
jstack <pid> | grep -A 3 '"keep-alive"'

应看到一个名为 keep-alive 的线程,状态 TIMED_WAITING (sleeping)。

12.8 验证 CRaC 的日志差异

需要 CRaC 版 JDK(Azul Zulu / OpenJDK CRaC 分支)+ org.crac 依赖。

java -XX:CRaCCheckpointTo=/tmp/cr -jar app.jar
# 日志:Started MyApp in 3.456 seconds

jcmd <pid> JDK.checkpoint
# JVM 退出,快照在 /tmp/cr

java -XX:CRaCRestoreFrom=/tmp/cr
# 日志:Restored MyApp in 0.045 seconds

关键观察:动词从 Started 变 Restored,耗时从秒级降到毫秒级。

12.9 验证 @ConditionalOnCheckpointRestore

在普通 JDK 上:标注了该注解的 @Configuration 不应生效(org.crac.Resource 不存在)。 在 CRaC JDK + org.crac 依赖下:应生效。

12.10 验证错误处理链

故意让一个 @PostConstruct 抛异常,观察:

  1. ApplicationFailedEvent 是否被收到
  2. FailureAnalyzer 的诊断报告是否输出(即使你在 failed 监听器里抛异常)
  3. 退出码是否正确

13. 本章源码索引

13.1 核心

文件关注点
SpringApplication.javarun()(304 起)、createBootstrapContext()、prepareEnvironment()、prepareContext()、getRunListeners()、handleRunFailure()、Startup 抽象类与两个实现、KeepAlive
SpringApplicationRunListener.java七个 default 方法
SpringApplicationRunListeners.javadoWithListeners、七个 step 名、callFailedListener
context/event/EventPublishingRunListener.javainitialMulticaster、七个事件映射、contextLoaded 的 listener 转交、failed 双路径、LoggingErrorHandler
bootstrap/BootstrapRegistry.java五个方法、InstanceSupplier、Scope
bootstrap/DefaultBootstrapContext.java两个 Map、register 的断言、getInstance 懒加载、close
bootstrap/BootstrapContext.java四个 get 变体
bootstrap/BootstrapContextClosedEvent.java两个 getter
bootstrap/BootstrapRegistryInitializer.java单方法
bootstrap/ConfigurableBootstrapContext.java空合并接口

13.2 支撑

文件关注点
context/event/SpringApplicationEvent.java基类:getSpringApplication()、getArgs()
context/event/ 下七个事件类各自额外提供的 getter
availability/AvailabilityChangeEvent.javapublish 静态方法
availability/LivenessState.java / ReadinessState.java两个枚举
SpringApplicationHook.java单方法、per-thread 语义
core/spring-boot-autoconfigure/.../condition/ConditionalOnCheckpointRestore.java@ConditionalOnClass(name=...)

13.3 测试 / 样例

文件关注点
smoke-test/spring-boot-smoke-test-bootstrap-registry/本章最重要的实践样例:完整的 ConfigData + BootstrapRegistry 组合
SpringApplicationTests.java(未确认,待核实)生命周期断言

14. 下一章

第 13 章 · Bean 定义加载:BeanDefinitionLoader、组件扫描与 Bean 命名

本章横切了整个启动流程(从 run() 到 ready),第 13 章回到主线,进入卷四 · IoC 与自动配置引擎的第一站。

prepareContext() 里的 load(context, sources.toArray(...)) 是本章时序图里唯一没有展开的一步。第 13 章讲:

  1. BeanDefinitionLoader:五种 source 类型(Class / Resource / Package / CharSequence / AnnotatedBeanDefinitionReader 直通)
  2. 组件扫描:ClassPathBeanDefinitionScanner 的过滤链、TypeExcludeFilter、AutoConfigurationExcludeFilter
  3. Bean 命名:AnnotationBeanNameGenerator、BeanNameGenerator 的两种策略、为什么同名 bean 会被覆盖(或报错)
  4. AOT 下的差异:AotDetector.useGeneratedArtifacts() 时 load() 被跳过(本章时序图里已经能看到这个分支)

第 14 章再讲 AutoConfigurationImportSelector——自动配置是怎么被"注入"到 BeanDefinition 集合里的。


附:本章遗留悬念

悬念将在
AbandonedRunException 的定义与触发场景待查
spring.main.environment-prefix 在 ApplicationProperties 中是否有字段待查
SpringApplication.withHook 的确切签名待查
AvailabilityChangeEvent.publish 是否做去重待查
ConfigurationWarningsApplicationContextInitializer / ContextIdApplicationContextInitializer第 13 章
PropertySourceOrderingBeanFactoryPostProcessor第 13 章
LazyInitializationBeanFactoryPostProcessor第 13 章
LoggingApplicationListener第 24 章
FailureAnalyzers 的完整 analyzer 列表散见各章

附录 A · 七个事件类源码逐个精读

正文 §7 给了七个事件的规格摘要。这里给出每个类的真实源码要点,便于检索与核对。

A.1 ApplicationStartingEvent

/**
 * Event published as early as conceivably possible as soon as a SpringApplication
 * has been started - before the Environment or ApplicationContext is available, but
 * after the ApplicationListeners have been registered. The source of the event is the
 * SpringApplication itself, but beware of using its internal state too much at this
 * early stage since it might be modified later in the lifecycle.
 *
 * @author Phillip Webb
 * @author Madhura Bhave
 * @since 1.5.0
 */
public class ApplicationStartingEvent extends SpringApplicationEvent {

    private final ConfigurableBootstrapContext bootstrapContext;

    public ApplicationStartingEvent(ConfigurableBootstrapContext bootstrapContext, SpringApplication application,
            String[] args) {
        super(application, args);
        this.bootstrapContext = bootstrapContext;
    }

    /**
     * @since 2.4.0
     */
    public ConfigurableBootstrapContext getBootstrapContext() {
        return this.bootstrapContext;
    }
}

@since 1.5.0——它是七个事件里第二新的(started/ready 的 Duration 参数是 2.6)。

Javadoc 的两条警告:

  1. "before the Environment or ApplicationContext is available, but after the ApplicationListeners have been registered"

    精确的时间窗:listener 已注册(refreshApplicationListeners() 已跑),但 Environment 还没创建。

  2. "beware of using its internal state too much at this early stage since it might be modified later in the lifecycle"

    因为 bindToSpringApplication(environment) 还没跑,SpringApplication 的字段值(比如 properties)还是"代码设置值",之后会被配置覆盖。不要在这个事件里缓存 SpringApplication 的状态。

getBootstrapContext() 的 @since 2.4.0——bootstrap context 是 2.4 引入的(ConfigData 重写),所以这个 getter 比事件本身晚。

A.2 ApplicationEnvironmentPreparedEvent

/**
 * Event published when a SpringApplication is starting up and the
 * Environment is first available for inspection and modification.
 *
 * @author Dave Syer
 * @since 1.0.0
 */
public class ApplicationEnvironmentPreparedEvent extends SpringApplicationEvent {

    private final ConfigurableBootstrapContext bootstrapContext;
    private final ConfigurableEnvironment environment;
    ...
}

@since 1.0.0——最老的事件之一,作者是 Dave Syer(Spring Boot 共同创始人)。

Javadoc 的关键词是 "first available for inspection and modification":

  • inspection:可以读环境了
  • modification:可以改。EnvironmentPostProcessor 就是在改(添加 PropertySource、改 profile)

这是唯一一个"鼓励你修改"的事件。其它事件基本只让你观察。

两个 getter 的 @since 不同:getEnvironment() 是 1.0.0,getBootstrapContext() 是 2.4.0。

A.3 ApplicationContextInitializedEvent

/**
 * Event published when a SpringApplication is starting up and the
 * ApplicationContext is prepared and ApplicationContextInitializers have been
 * called but before any bean definitions are loaded.
 *
 * @author Artsiom Yudovin
 * @since 2.1.0
 */
public class ApplicationContextInitializedEvent extends SpringApplicationEvent {

    private final ConfigurableApplicationContext context;
    ...
}

@since 2.1.0。

Javadoc 明确了三个已完成状态:

  1. ApplicationContext is prepared
  2. ApplicationContextInitializers have been called
  3. before any bean definitions are loaded

第三条是它与 ApplicationPreparedEvent 的唯一区别。

典型用途:在 BeanDefinition 加载前,调整 ClassPathBeanDefinitionScanner 的过滤器,或注册自定义的 BeanDefinitionRegistryPostProcessor。

A.4 ApplicationPreparedEvent

/**
 * Event published as when a SpringApplication is starting up and the
 * ApplicationContext is fully prepared but not refreshed. The bean definitions
 * will be loaded and the Environment is ready for use at this stage.
 *
 * @author Dave Syer
 * @since 1.0.0
 */

"fully prepared but not refreshed"——BeanDefinition 已加载,但还没实例化任何单例。

在这是最后的机会做:

  • 修改 BeanDefinition(比如改 bean 的 scope、改属性值)
  • 添加/移除 BeanFactoryPostProcessor
  • 注册额外的 ApplicationListener(EventPublishingRunListener 就是在这时把 application.getListeners() 倒进 context 的)

A.5 ApplicationStartedEvent

/**
 * Event published once the application context has been refreshed but before any
 * ApplicationRunner and CommandLineRunner runners have been called.
 *
 * @author Andy Wilkinson
 * @since 2.0.0
 */
public class ApplicationStartedEvent extends SpringApplicationEvent {

    private final ConfigurableApplicationContext context;
    private final @Nullable Duration timeTaken;

    /**
     * @since 2.6.0
     */
    public @Nullable Duration getTimeTaken() {
        return this.timeTaken;
    }
}

Javadoc 里显式点名了 ApplicationRunner 和 CommandLineRunner——这是区分 started 与 ready 的关键。

timeTaken 是 @since 2.6.0——与 started/ready 回调的新增版本一致。

timeTaken 可能为 null(@Nullable),来源:

Duration timeTakenToStarted = startup.started();
...
listeners.started(context, timeTakenToStarted);

Startup.started() 总是返回非 null 的 Duration。但 SpringApplicationRunListener.started 的声明允许 null(给第三方实现用)。

A.6 ApplicationReadyEvent

/**
 * Event published as late as conceivably possible to indicate that the application is
 * ready to service requests. The source of the event is the SpringApplication
 * itself, but beware of modifying its internal state since all initialization steps
 * will have been completed by then.
 *
 * @author Stephane Nicoll
 * @author Chris Bono
 * @since 1.3.0
 * @see ApplicationFailedEvent
 */

"as late as conceivably possible"——与 ApplicationStartingEvent 的 "as early as conceivably possible" 形成对仗。

"beware of modifying its internal state"——所有初始化已完成,此时改 SpringApplication 的状态不会有任何效果(因为已经没人再读它了)。

@see ApplicationFailedEvent——Javadoc 显式交叉引用。ready 与 failed 是 run() 的两个出口。

注意 run() 里的条件:

try {
    if (context.isRunning()) {
        listeners.ready(context, startup.ready());
    }
}

ApplicationReadyEvent 不是必然发出的。

A.7 ApplicationFailedEvent

/**
 * Event published by a SpringApplication when it fails to start.
 *
 * @since 1.0.0
 * @see ApplicationReadyEvent
 */
public class ApplicationFailedEvent extends SpringApplicationEvent {

    private final @Nullable ConfigurableApplicationContext context;
    private final Throwable exception;

    public @Nullable ConfigurableApplicationContext getApplicationContext() {
        return this.context;
    }

    public Throwable getException() {
        return this.exception;
    }
}

context 是 @Nullable——失败发生在 context 创建之前时为 null。

exception 不是 @Nullable——一定不为 null。

双向 @see:ApplicationReadyEvent 的 Javadoc 引用 ApplicationFailedEvent,反之亦然。Boot 用 Javadoc 表达了"这是一个二选一的出口"。

A.8 事件规格总表

事件@since作者额外 getter多播器
ApplicationStartingEvent1.5.0Phillip Webb, Madhura BhavegetBootstrapContext()(2.4.0)initial
ApplicationEnvironmentPreparedEvent1.0.0Dave SyergetBootstrapContext()(2.4.0)、getEnvironment()initial
ApplicationContextInitializedEvent2.1.0Artsiom YudovingetApplicationContext()initial
ApplicationPreparedEvent1.0.0Dave SyergetApplicationContext()initial
ApplicationStartedEvent2.0.0Andy WilkinsongetApplicationContext()、getTimeTaken()(2.6.0)context
ApplicationReadyEvent1.3.0Stephane Nicoll, Chris BonogetApplicationContext()、getTimeTaken()(2.6.0)context
ApplicationFailedEvent1.0.0Dave SyergetApplicationContext()(nullable)、getException()视情况

附录 B · EnvironmentPostProcessorApplicationListener:事件到 SPI 的桥

这是"事件总线"如何驱动"S.PI"的最佳范例——它把 ApplicationEnvironmentPreparedEvent 转成 EnvironmentPostProcessor 的调用。第 06 章讲过后者,这里补全前者。

B.1 类定位

/**
 * SmartApplicationListener used to trigger EnvironmentPostProcessors registered in
 * the spring.factories file.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 4.0.0
 */
public class EnvironmentPostProcessorApplicationListener implements SmartApplicationListener, Ordered {

    static final String AOT_FEATURE_NAME = "EnvironmentPostProcessor";

    public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;

    private final DeferredLogs deferredLogs;

    private int order = DEFAULT_ORDER;

    private final Function<@Nullable ClassLoader, EnvironmentPostProcessorsFactory> postProcessorsFactory;
    ...
}

四点:

  1. @since 4.0.0——虽然是 4.0 才"新建"的类,但它的职责(触发 EnvironmentPostProcessor)从 1.x 就有(旧实现在 ConfigFileApplicationListener 里)。4.0 把它拆出来独立成类。

  2. 实现 SmartApplicationListener(不是 ApplicationListener<...>)——因为它要监听三种事件类型,用泛型表达不了。

  3. Ordered,DEFAULT_ORDER = HIGHEST_PRECEDENCE + 10

    Integer.MIN_VALUE + 10——几乎是最高优先级。它必须在其它监听 ApplicationEnvironmentPreparedEvent 的监听器之前跑,因为 LoggingApplicationListener 等需要 EnvironmentPostProcessor 处理完的环境。

  4. 包名 org.springframework.boot.support(4.x 迁移,3.x 是 org.springframework.boot.context.config 或 org.springframework.boot.context)。

B.2 监听三种事件

@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
    return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
            || ApplicationPreparedEvent.class.isAssignableFrom(eventType)
            || ApplicationFailedEvent.class.isAssignableFrom(eventType);
}

@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent environmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent(environmentPreparedEvent);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent();
    }
    if (event instanceof ApplicationFailedEvent) {
        onApplicationFailedEvent();
    }
}

注意 onApplicationEvent 用的是三个独立的 if,不是 else if——虽然三种事件类型互斥,但这样写更防御。

为什么还要监听 ApplicationPreparedEvent 和 ApplicationFailedEvent?

private void onApplicationPreparedEvent() {
    finish();
}

private void onApplicationFailedEvent() {
    finish();
}

private void finish() {
    this.deferredLogs.switchOverAll();
}

答案:DeferredLogs 的回放。

B.3 DeferredLogs:日志系统的先有鸡还是先有蛋

问题:EnvironmentPostProcessor 在 ApplicationEnvironmentPreparedEvent 阶段跑,此时日志系统还没初始化(LoggingApplicationListener 也是在这个阶段跑,有顺序依赖)。

解决:DeferredLogs——一个"延迟日志"收集器。

private final DeferredLogs deferredLogs = new DeferredLogs();

EnvironmentPostProcessor 拿到的 DeferredLogFactory 会创建 DeferredLog,把消息缓存在内存里。等日志系统就绪后,switchOverAll() 把缓存的消息回放到真正的 logger。

三个切换时机:

事件调用含义
ApplicationEnvironmentPreparedEvent(不切换,跑 processor)收集
ApplicationPreparedEventfinish()正常路径:上下文准备好,日志系统已就绪 → 回放
ApplicationFailedEventfinish()失败路径:也要回放,否则诊断信息丢失

为什么失败路径也要回放? 因为失败往往发生在日志系统就绪之前。如果不回放,"Config data location not found" 这类关键信息就永远看不到了。

这是一个非常实用主义的细节——Boot 宁可多写两个 if,也不让错误日志丢失。

B.4 主逻辑

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    ConfigurableEnvironment environment = event.getEnvironment();
    SpringApplication application = event.getSpringApplication();
    List<EnvironmentPostProcessor> postProcessors = getEnvironmentPostProcessors(application.getResourceLoader(),
            event.getBootstrapContext());
    addAotGeneratedEnvironmentPostProcessorIfNecessary(postProcessors, application);
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(environment, application);
    }
}

List<EnvironmentPostProcessor> getEnvironmentPostProcessors(@Nullable ResourceLoader resourceLoader,
        ConfigurableBootstrapContext bootstrapContext) {
    ClassLoader classLoader = (resourceLoader != null) ? resourceLoader.getClassLoader() : null;
    EnvironmentPostProcessorsFactory postProcessorsFactory = this.postProcessorsFactory.apply(classLoader);
    return postProcessorsFactory.getEnvironmentPostProcessors(this.deferredLogs, bootstrapContext);
}

三个可注入给 EnvironmentPostProcessor 的东西:

  1. DeferredLogFactory(来自 this.deferredLogs)
  2. ConfigurableBootstrapContext(来自事件的 event.getBootstrapContext())
  3. ClassLoader(来自 application.getResourceLoader())

ConfigurableBootstrapContext 出现在这里很关键——这就是 ConfigDataEnvironmentPostProcessor 能拿到 bootstrap context、进而 registerBootstrapBinder 的通道(第 10 章 §4.4)。

B.5 AOT 支持

private void addAotGeneratedEnvironmentPostProcessorIfNecessary(List<EnvironmentPostProcessor> postProcessors,
        SpringApplication springApplication) {
    if (AotDetector.useGeneratedArtifacts()) {
        ClassLoader classLoader = (springApplication.getResourceLoader() != null)
                ? springApplication.getResourceLoader().getClassLoader() : null;
        Class<?> mainApplicationClass = springApplication.getMainApplicationClass();
        Assert.state(mainApplicationClass != null, "mainApplicationClass not found");
        String postProcessorClassName = mainApplicationClass.getName() + "__" + AOT_FEATURE_NAME;
        if (ClassUtils.isPresent(postProcessorClassName, classLoader)) {
            postProcessors.add(0, instantiateEnvironmentPostProcessor(postProcessorClassName, classLoader));
        }
    }
}

AOT 生成的类名规则:{主应用类全名}__EnvironmentPostProcessor

例:com.example.MyApp__EnvironmentPostProcessor

postProcessors.add(0, ...)——插到最前面(最高优先级)。因为 AOT 生成的 processor 包含"编译期已算好的配置",应该先于运行时的 processor 执行。

ClassUtils.isPresent 检查——这个类可能不存在(没跑 AOT 编译时),此时静默跳过。

AOT_FEATURE_NAME = "EnvironmentPostProcessor"——一个常量,与生成代码的命名约定绑定。

对比第 04 章提到的 AOT 生成物命名:

AOT 生成物命名
ApplicationContextInitializer{Main}__ApplicationContextInitializer
EnvironmentPostProcessor{Main}__EnvironmentPostProcessor
BeanDefinition 注册器(其它约定,第 28 章)

规律:{主类名}__{特性名}。这是 Boot AOT 的统一命名约定。

B.6 两个工厂入口

public EnvironmentPostProcessorApplicationListener() {
    this(EnvironmentPostProcessorsFactory::fromSpringFactories);
}

public static EnvironmentPostProcessorApplicationListener with(
        EnvironmentPostProcessorsFactory postProcessorsFactory) {
    return new EnvironmentPostProcessorApplicationListener((classloader) -> postProcessorsFactory);
}
  • 默认:从 spring.factories 加载 EnvironmentPostProcessor
  • with(factory):用自定义工厂(测试用,或编程式注册)

Function<ClassLoader, EnvironmentPostProcessorsFactory> 而不是直接 EnvironmentPostProcessorsFactory——延迟到真正需要时才按 ClassLoader 创建。这支持"不同 ClassLoader 下有不同 processor 集合"的场景(比如 Spring Cloud 的多 ClassLoader 环境)。

B.7 它还是个 AOT processor

/**
 * Contribute a {@code <Application>__EnvironmentPostProcessor} class that stores AOT
 * optimizations.
 */
static class EnvironmentBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
    ...
}

(这个类我只读到了声明,未展开实现——标注为未精读。它的职责从注释看是"生成 __EnvironmentPostProcessor 类"。第 28 章会讲 AOT 代码生成。)


附录 C · 启动期扩展点选型矩阵

Boot 在启动期提供了五个看起来很像的扩展点。选错会导致"代码跑了但没生效"。

扩展点触发时机能拿到典型用途注册方式
SpringApplicationRunListener七个回调点视回调而定横切所有阶段;框架集成spring.factories 或 SpringApplicationHook
ApplicationListener(含 @EventListener)事件发布时事件对象观察、不改SpringApplication.addListeners / @Component
EnvironmentPostProcessorenvironmentPreparedEnvironment、bootstrapContext、DeferredLogFactory改 Environmentspring.factories
ApplicationContextInitializerprepareContext 内ApplicationContext改上下文配置spring.factories / addInitializers
BootstrapRegistryInitializercreateBootstrapContextBootstrapRegistry(只写)注册早期共享实例addBootstrapRegistryInitializers

C.1 决策树

要改 Environment(加 PropertySource / 改 profile)?
  └─ 是 → EnvironmentPostProcessor
  └─ 否 ↓

需要在 Environment 之前(或横跨多个阶段)做事?
  └─ 是 → SpringApplicationRunListener
  └─ 否 ↓

需要改 ApplicationContext 本身(不是 BeanDefinition)?
  └─ 是 → ApplicationContextInitializer
  └─ 否 ↓

需要改 BeanDefinition?
  └─ 是 → BeanFactoryPostProcessor(第 13 章)
  └─ 否 ↓

只是想观察?
  └─ 是 → ApplicationListener / @EventListener

C.2 三个高频错误

错误 1:在 ApplicationListener<ApplicationEnvironmentPreparedEvent> 里改 Environment

技术上可行(能拿到 ConfigurableEnvironment),但:

  • 顺序不确定(你的 listener 可能在 EnvironmentPostProcessorApplicationListener 之前跑,此时 ConfigData 还没加载)
  • 拿不到 DeferredLogFactory(日志会丢)
  • 拿不到 bootstrap context(除非从事件里取)

正确做法:写 EnvironmentPostProcessor。

错误 2:在 @Component 里监听 ApplicationStartingEvent

永远收不到。因为 @Component 的 @EventListener 需要 bean 被实例化,而 bean 实例化在 refresh() 里——那时 ApplicationStartingEvent 早发完了。

正确做法:用 spring.factories 注册 ApplicationListener,或 SpringApplication.addListeners(...)。

这个错误极其常见,值得单独记牢:

ApplicationStartingEvent / ApplicationEnvironmentPreparedEvent / ApplicationContextInitializedEvent / ApplicationPreparedEvent 这四个事件,无法用 @Component + @EventListener 接收。 只有 ApplicationStartedEvent / ApplicationReadyEvent / ApplicationFailedEvent 可以(它们发生在 refresh 之后)。

错误 3:在 BootstrapRegistryInitializer 里 get()

参数是 BootstrapRegistry,没有 get 方法(get 在 BootstrapContext 里)。这是刻意的 API 设计。

C.3 EnvironmentPostProcessor 的顺序

从 spring.factories:

org.springframework.boot.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.support.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.support.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.support.SystemEnvironmentPropertySourceEnvironmentPostProcessor

五者按声明顺序执行(EnvironmentPostProcessorsFactory 是否做排序,我未核实——标注待查)。但注意它们都实现了 Ordered(SystemEnvironmentPropertySourceEnvironmentPostProcessor 的 DEFAULT_ORDER = SpringApplicationJsonEnvironmentPostProcessor.DEFAULT_ORDER - 1),所以实际顺序由 order 决定。

SystemEnvironmentPropertySourceEnvironmentPostProcessor 必须早于 SpringApplicationJsonEnvironmentPostProcessor——因为后者要读 SPRING_APPLICATION_JSON 环境变量,需要前者已经把 OriginAwareSystemEnvironmentPropertySource 装好。

这个 order 差值(-1)是保证正确性的关键,改动它会导致 JSON 配置的 Origin 丢失。


附录 D · 事件监听器的四种注册途径

同一个事件,监听器可以从四个地方来。理解这点才能排查"为什么我的监听器没被调用"。

途径生效事件范围注册时机
① spring.factories 的 ApplicationListener全部七个SpringApplication 构造时(由 getSpringFactoriesInstances)
② SpringApplication.addListeners(...) / SpringApplicationBuilder.listeners(...)全部七个run() 之前
③ context.addApplicationListener(...)contextLoaded 之后prepareContext 内
④ @Component + @EventListener仅 started / ready / failedrefresh() 内

D.1 途径 ① 与 ② 的注入点

application.getListeners() 是这两种途径的合集。EventPublishingRunListener.refreshApplicationListeners() 每次发事件前都把它们加进 initialMulticaster:

private void refreshApplicationListeners() {
    this.application.getListeners().forEach(this.initialMulticaster::addApplicationListener);
}

"每次都重新加"——所以途径 ③(在 contextLoaded 时)之外,你在 contextPrepared 里动态 addListeners(...) 也能被后续事件看到。

D.2 途径 ③ 的特殊性

contextLoaded 时:

for (ApplicationListener<?> listener : this.application.getListeners()) {
    if (listener instanceof ApplicationContextAware contextAware) {
        contextAware.setApplicationContext(context);
    }
    context.addApplicationListener(listener);
}

注意这是把 ①+② 转发进 context,不是途径 ③ 本身。

途径 ③ 是用户代码直接 context.addApplicationListener(...)——比如在 ApplicationContextInitializer 里。这样注册的监听器从 refresh() 开始生效。

D.3 途径 ④ 的时间窗

@EventListener 由 EventListenerMethodProcessor(一个 SmartInitializingSingleton)在所有单例实例化完成后注册。

所以它能收到的最早事件是 ApplicationStartedEvent(在 refresh() 之后、callRunners 之前)。

这解释了为什么 @EventListener(ApplicationReadyEvent.class) 能工作,而 @EventListener(ApplicationEnvironmentPreparedEvent.class) 不能。

D.4 排查清单

监听器没被调用?按序检查:

  1. 是不是 @Component + 早期事件? → 改用途径 ①/②
  2. 是不是在 spring.factories 里写了但没被加载? → 检查文件路径是 META-INF/spring.factories(不是 *.imports)
  3. 事件类型匹配吗? → SmartApplicationListener.supportsEventType 是否正确实现
  4. 是不是被异常吞掉了? → 前六个事件用 initialMulticaster(默认 error handler 会抛异常);ApplicationFailedEvent 用 LoggingErrorHandler(只 warn,不抛)
  5. 是不是 @Order 太靠后? → EnvironmentPostProcessorApplicationListener 是 HIGHEST_PRECEDENCE + 10,你的监听器若想看完整环境必须更靠后

附录 E · 本章与前三卷的接口总览

前文章节本章如何呼应
第 04 章 run() 十二步本章把它从"事件视角"重画了一遍,并补出 Startup、KeepAlive、handleRunFailure
第 06 章 EnvironmentPostProcessor附录 B 给出触发它的完整链路(EnvironmentPostProcessorApplicationListener + DeferredLogs)
第 07 章 ConfigData本章给 ConfigData 的阶段划分找到了"事件锚点"(environmentPrepared);不动点迭代的三轮都在这个回调内完成
第 09 章 @ConfigurationProperties绑定发生在 refresh(),即 ApplicationPreparedEvent 之后
第 10 章 registerBootstrapBinder本章补全 BootstrapRegistry 的语义,解释了为什么能用 PROTOTYPE 反复重注册
第 11 章 ConfigData SPI本章说明自定义 resolver/loader 如何拿到 bootstrap 实例(context.getBootstrapContext())

一句话总结本章:

事件总线是"纵向"的(贯穿七个时点),BootstrapRegistry 是"横向"的(跨越上下文缺失的那段时间)。 二者共同让 Boot 能在 ApplicationContext 存在之前,完成一整套需要"容器"才能做的复杂工作。

本章未逐行核实 / 待你实测的项

  1. BootstrapRegistry 的完整生命周期(含关闭时机)未逐行跟踪。
  2. CRaC 相关 listener 的行为未实测(需要支持 CRaC 的 JDK)。
  3. SpringApplicationRunListener 的全部实现类未统计(含用户自定义与第三方)。
  4. 事件在 AOT 模式下是否全部照常发布未验证。
  5. EventPublishingRunListener 的异常处理分支未逐个读。