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存在之前完成,但它们又需要"一个容器"来共享对象。
具体例子:
- ConfigData 阶段 :
ConfigDataLocationResolver可能需要读配置(比如 Vault 的地址),而此时Environment才刚建好、上下文还不存在。 - 配置中心客户端 :Spring Cloud Config / Consul 的客户端要在
environmentPrepared阶段拉取远程配置,它需要 HTTP 客户端、需要序列化器------这些都是"昂贵对象"。 - AOT / native:需要在早期记录运行时行为,供编译期使用。
- 测试 :
@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 接口全貌
java
// 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 |
备注 |
|---|---|---|
starting |
1.0.0(接口本身) | 无独立 @since,随接口 |
environmentPrepared |
同上 | |
contextPrepared |
同上 | |
contextLoaded |
同上 | |
started |
2.6.0 | 后来加的 |
ready |
2.6.0 | 后来加的 |
failed |
2.0.0 |
started 与 ready 是 2.6 才加的 ------在此之前只有 ApplicationStartedEvent,无法区分"ApplicationContext 刷新完"与"CommandLineRunner 跑完"。2.6 引入 ready 之后,K8s 的 readiness probe 才能精确地在"真正可以接流量"时变绿。
1.2 七个回调的精确语义
| 回调 | 触发时机(在 run() 里的位置) |
此时可用 | 典型用途 |
|---|---|---|---|
starting |
run() 最开始,prepareEnvironment 之前 |
bootstrapContext(无 Environment) | 最早期的初始化;注册 bootstrap 实例 |
environmentPrepared |
Environment 建好、ConfigurationPropertySources.attach 之后 |
Environment(未绑定 spring.main) | EnvironmentPostProcessor、日志系统初始化 |
contextPrepared |
ApplicationContext 创建 + applyInitializers 之后 |
context(未加载 BeanDefinition) | 最后的上下文定制 |
contextLoaded |
load() 之后、refresh() 之前 |
context(已加载未刷新) | 注册 listener、改 BeanDefinition |
started |
refresh() + afterRefresh 之后,callRunners 之前 |
完整 context | 「启动完成但还没跑业务代码」 |
ready |
callRunners 之后 |
完整 context | readiness 探针、预热完成通知 |
failed |
任意步骤抛异常时 | context 可能为 null | 错误上报、资源清理 |
关键分界点:
startedvsready:中间夹着callRunners。所以ApplicationRunner/CommandLineRunner抛异常时,started已发、ready不发。contextPreparedvscontextLoaded:中间夹着load()(BeanDefinition 加载)和bootstrapContext.close(context)。environmentPrepared之后还有bindToSpringApplication------所以在这个回调里读SpringApplication的属性是旧值 (spring.main.*还没绑定)。
最后一条极其重要,值得展开:
java
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读。
另外注意这个断言:
java
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 theSpringFactoriesLoaderand should declare a public constructor that accepts aSpringApplicationinstance and aString[]of arguments. A newSpringApplicationRunListenerinstance will be created for each run.
而实际实现用的是 4.x 的 ArgumentResolver:
java
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);
}
两点:
ArgumentResolver显式声明SpringApplication与String[]两个可注入类型(对比第 11 章 §1.4 的 ConfigData resolver 注入六种类型)。SpringApplicationHook追加在最后(§10 详述)。
"每次 run 都新建实例"------所以 listener 里不能有跨 run 的静态状态(或者要小心处理)。
1.4 SpringApplicationRunListeners:门面与可观测
java
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 名 |
|---|---|
starting |
spring.boot.application.starting |
environmentPrepared |
spring.boot.application.environment-prepared |
contextPrepared |
spring.boot.application.context-prepared |
contextLoaded |
spring.boot.application.context-loaded |
started |
spring.boot.application.started |
ready |
spring.boot.application.ready |
failed |
spring.boot.application.failed |
这些是 ApplicationStartup 的埋点 ------配合 BufferingApplicationStartup(spring.main.application-startup=buffering)可以在 /actuator/startup 看到每一步耗时。
两个带 tag 的 step:
java
// 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:异常隔离
java
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 生命周期的对照
把两条时间线对齐:
scss
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)
三个"诞生"节点 与一个"死亡"节点:
| 对象 | 诞生 | 死亡 |
|---|---|---|
DefaultBootstrapContext |
createBootstrapContext() |
bootstrapContext.close(context) |
ConfigurableEnvironment |
getOrCreateEnvironment() |
随 context |
ConfigurableApplicationContext |
createApplicationContext() |
context.close() |
BootstrapContext 的存活窗口 是:从 run() 开头,到 contextPrepared 之后、load() 之前。
为什么在这个点关闭? 因为:
contextPrepared已经发过,ApplicationContextInitializer已经跑完------它们可能需要 bootstrap 实例。load()之后 BeanDefinition 就进 context 了,再需要共享对象就应该用 Spring bean 而不是 bootstrap 实例。BootstrapContextClosedEvent让 bootstrap 实例有机会把自己注册成 Spring bean(见 §7.3)。
2. EventPublishingRunListener:从回调到 ApplicationEvent
2.1 它是什么
java
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:
properties
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
它的职责 :把七个回调翻译成七个 ApplicationEvent,并分发给 SpringApplication.getListeners() 里注册的监听器。
getOrder() 返回 0 ------它在 listener 列表中的位置由 SpringApplicationRunListeners 的遍历顺序决定。因为只有一个内置实现,顺序不敏感;但第三方 listener 追加进来时,0 意味着"中间位置"(@Order 更小者在前)。
2.2 七个回调 → 七个事件
| 回调 | 发布的事件 |
|---|---|
starting |
ApplicationStartingEvent |
environmentPrepared |
ApplicationEnvironmentPreparedEvent |
contextPrepared |
ApplicationContextInitializedEvent |
contextLoaded |
ApplicationPreparedEvent |
started |
ApplicationStartedEvent(+ AvailabilityChangeEvent(LivenessState.CORRECT)) |
ready |
ApplicationReadyEvent(+ AvailabilityChangeEvent(ReadinessState.ACCEPTING_TRAFFIC)) |
failed |
ApplicationFailedEvent |
2.3 initialMulticaster:为什么需要自己的多播器
java
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
java
@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));
}
做了两件事,顺序很重要:
- 把
SpringApplication.getListeners()的所有 listener 注册进ApplicationContext- 若 listener 实现了
ApplicationContextAware,先注入 context
- 若 listener 实现了
- 然后 发布
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(...):
java
@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→initialMulticasterstarted/ready→context.publishEventfailed→ 视情况(见 2.5)
2.5 failed 的双路径
java
@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 借过来 |
"借过来"这一步很巧妙:
java
if (context instanceof AbstractApplicationContext abstractApplicationContext) {
for (ApplicationListener<?> listener : abstractApplicationContext.getApplicationListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
}
即:context 已经 addApplicationListener 过(在 contextLoaded 里),但它的多播器还没初始化(因为没 refresh)。此时把那些 listener 拷到 initialMulticaster 里,用后者发事件。
为什么还要 setErrorHandler(new LoggingErrorHandler())? 因为失败处理期间,监听器自己抛异常不应该掩盖原始异常:
java
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:
java
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()。
| 事件 | 额外提供 |
|---|---|
ApplicationStartingEvent |
getBootstrapContext() |
ApplicationEnvironmentPreparedEvent |
getBootstrapContext()、getEnvironment() |
ApplicationContextInitializedEvent |
getApplicationContext() |
ApplicationPreparedEvent |
getApplicationContext() |
ApplicationStartedEvent |
getApplicationContext()、getTimeTaken() |
ApplicationReadyEvent |
getApplicationContext()、getTimeTaken() |
ApplicationFailedEvent |
getApplicationContext()(可 null)、getException() |
ApplicationFailedEvent 的 context 可能为 null------失败发生在 context 创建之前时。
2.7 事件的典型消费者
Boot 内部谁在听这些事件?
| 监听器 | 监听的事件 | 干什么 |
|---|---|---|
EnvironmentPostProcessorApplicationListener |
ApplicationEnvironmentPreparedEvent |
跑全部 EnvironmentPostProcessor(第 06 章) |
LoggingApplicationListener |
ApplicationEnvironmentPreparedEvent 等 |
初始化日志系统(第 24 章) |
AnsiOutputApplicationListener |
ApplicationEnvironmentPreparedEvent |
配置 ANSI |
FileEncodingApplicationListener |
ApplicationEnvironmentPreparedEvent |
校验 file.encoding |
ClearCachesApplicationListener |
ContextRefreshedEvent |
清理反射缓存 |
ParentContextCloseApplicationListener |
多个 | 父上下文联动关闭 |
从 spring.factories 可以看到完整列表:
properties
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 同时发布可用性事件
java
@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
java
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的实现类型(LivenessStatevsReadinessState),只通知关心该类型的监听器 - 如果状态没变化,可能跳过(具体实现我没读
AvailabilityChangeEvent全文,标注为未核实)
实践用法(用户代码):
java
@Component
public class MyReadinessListener {
@EventListener
public void onReady(AvailabilityChangeEvent<ReadinessState> event) {
if (event.getPayload() == ReadinessState.ACCEPTING_TRAFFIC) {
// 可以接流量了
}
}
}
4. BootstrapRegistry:上下文之前的临时容器
4.1 为什么需要它
回到 §0 的困境。Boot 需要一个地方放"早期共享对象",要求是:
- 比
ApplicationContext早 - 能表达"延迟创建"(不一定都要实例化)
- 能表达"作用域"(单例 or 每次新建)
- 能在上下文就绪后优雅交接(把自己注册成 bean)
BootstrapRegistry 就是这四个要求的答案。
4.2 接口方法
java
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
java
@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 枚举:
java
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:读取侧
java
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 是两者的合并:
java
public interface ConfigurableBootstrapContext extends BootstrapRegistry, BootstrapContext {
}
一个空接口,纯粹是"注册 + 读取"的组合视图。run() 里用的就是它。
4.5 DefaultBootstrapContext 的实现细节
java
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) "已实例化就不能再注册"
java
Assert.state(!this.instances.containsKey(type), () -> type.getName() + " has already been created");
这是一个强约束 :一旦某个类型被 get() 过(且是 SINGLETON),就不能再 register 替换它。
为什么? 因为可能已经有代码持有旧实例的引用了。此时替换会造成"同一个类型有两个不同实例"的混乱状态。
这条约束的可观测后果:
java
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() 过:
java
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 的原因!
java
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 的用法
java
synchronized (this.instanceSuppliers) { ... }
所有访问都锁 instanceSuppliers------包括 instances 的访问 (getInstance 在 getOrElseThrow 的同步块里被调用)。
不一致之处 :instances 本身不是锁对象,但因为所有访问路径都在 instanceSuppliers 的锁内,所以逻辑上是安全的。
为什么需要锁? 因为 ConfigData 的处理可能涉及多线程(尤其是自定义 resolver 做并发 IO)。
(4) getInstance 的懒加载
java
@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
java
public void close(ConfigurableApplicationContext applicationContext) {
this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext));
}
只是发一个事件 ,不做任何清理(instanceSuppliers / instances 不清空)。
为什么不清空? 因为 close 之后可能还有代码持有 bootstrapContext 引用并 get()。Boot 选择"软关闭"------发事件通知交接,但不切断访问。
4.6 BootstrapContextClosedEvent
java
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:
less
* @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.x | 4.x |
|---|---|
org.springframework.boot.BootstrapRegistry |
org.springframework.boot.bootstrap.BootstrapRegistry |
org.springframework.boot.BootstrapContext |
org.springframework.boot.bootstrap.BootstrapContext |
org.springframework.boot.BootstrapRegistryInitializer |
org.springframework.boot.bootstrap.BootstrapRegistryInitializer |
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener |
org.springframework.boot.support.* |
4.8 BootstrapRegistryInitializer
java
@FunctionalInterface
public interface BootstrapRegistryInitializer {
void initialize(BootstrapRegistry registry);
}
用途 :在 BootstrapContext 创建后立刻做初始化注册。
调用点:
java
private DefaultBootstrapContext createBootstrapContext() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
return bootstrapContext;
}
注入方式 :SpringApplication.addBootstrapRegistryInitializer(...)。
注意它拿到的是 BootstrapRegistry(写视图),不是 ConfigurableBootstrapContext ------所以在 initializer 里不能 get(),只能注册。
这是一个刻意的设计:初始化阶段只允许注册,不允许读取,避免初始化顺序依赖。
5. 完整时序:BootstrapContext 的一生
scss
① 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)
关键观察:
-
BootstrapContext 的"活跃期"覆盖了整个 Environment 构建阶段 ------包括 ConfigData 的全部三轮处理(第 10 章)。这让自定义
ConfigDataLocationResolver能拿到早期共享实例(比如 HTTP 客户端)。 -
Binder.class在 bootstrap 里被反复重注册(第 10 章 §4.4),每次都是最新的 contributors 快照 + PROTOTYPE 作用域。 -
关闭点在
contextPrepared之后 ------ApplicationContextInitializer还能用 bootstrap 实例(如果它持有引用),但load()就不行了。
6. 实战:用 BootstrapRegistry 做早期共享实例
Boot 的 smoke test 里有一个完整的例子:spring-boot-smoke-test-bootstrap-registry,模拟"从 Subversion 拉取配置"。
6.1 场景
bash
配置位置语法:svn:host/path
需要从 Subversion 服务器拉取配置文件
→ 需要在 ConfigData 阶段有一个可用的 SVN 客户端
→ SVN 客户端的创建需要证书等资源,不应该每次都新建
→ 但 ApplicationContext 还不存在,没法用 Spring bean
6.2 代码走读
Step 1:定义"昂贵的"客户端
java
// 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 注册它
java
// 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 里用它
java
// 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 里用它
java
// 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
java
// 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(可选)
java
// 在 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 客户端) |
| 交接为 bean | addCloseListener + registerSingleton |
应用代码也要用 |
| 懒加载 | 用 InstanceSupplier.from(Supplier) 而不是 of(instance) |
创建昂贵,且可能不需要 |
6.4 陷阱清单
-
initialize里不能get()------参数是BootstrapRegistry,没有get方法。想在注册时就实例化,只能自己调 supplier。 -
register的顺序无关 ------因为是懒加载。register(A, ctx -> new A(ctx.get(B)))可以在register(B, ...)之前写。 -
"已实例化不可替换" (§4.5)------注册顺序在第一次
get()之后就固定了。 -
close不清空 ------BootstrapContextClosedEvent之后仍可get()。别依赖"关闭后不可用"的假设。 -
线程安全 ------
DefaultBootstrapContext用synchronized,但你的InstanceSupplier实现本身可能不是线程安全的。SINGLETON 只调一次,PROTOTYPE 可能被并发调用。
7. 七个事件的详细规格
7.1 ApplicationStartingEvent
java
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() 里的判断:
java
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 项目)。
核心思想:
- 应用启动完成后,对 JVM 做一次 checkpoint(快照到磁盘)
- 之后启动新实例时,直接 restore(恢复)这个快照,跳过所有初始化
- 启动时间从秒级降到毫秒级
代价:应用必须"协调"------在 checkpoint 前关闭不能序列化的资源(网络连接、文件句柄),在 restore 后重新打开。这就是 "Coordinated" 的含义。
8.2 Boot 的支持:Startup 抽象
java
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() 的双条件探测:
java
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
java
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
java
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 要展示的价值。
日志输出差异:
arduino
# 正常启动
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
java
@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 环境下才需要的配置"。
java
@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 的应对 (我的观察,部分为推断):
Startup抽象(§8.2)------解决"启动耗时"的语义,让日志正确。@ConditionalOnCheckpointRestore------让用户能写"只在 CRaC 下的配置"。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
java
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();
}
}
安装点:
java
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 五步
java
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):
scss
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
java
if (exception instanceof AbandonedRunException abandonedRunException) {
return abandonedRunException;
}
直接返回,不做任何处理 。语义:"这次 run 是被主动放弃的(比如 SpringApplication 在 environmentPrepared 阶段被外部代码终止),不需要报错"。
典型场景 :SpringApplicationBuilder 的某些用法,或者测试框架主动中止。
AbandonedRunException 的具体定义我未在本章阅读------标注待查。
9.3 handleExitCode
遍历 context 里的 ExitCodeGenerator bean,取最大退出码。第 04 章讲过。
9.4 reportFailure 与 FailureAnalyzer
java
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
java
if (context != null) {
context.close();
shutdownHook.deregisterFailedApplicationContext(context);
}
失败时也要关 context------释放资源(连接池、文件句柄)。
deregisterFailedApplicationContext ------ 从 shutdown hook 里移除,避免重复关闭。
10. SpringApplicationHook:4.x 的测试友好能力
java
@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,互不干扰。
使用:
java
try {
SpringApplication.withHook(myHook, () -> {
SpringApplication.run(MyApp.class, args);
});
}
(withHook 的确切签名我未核实------Javadoc 里提到了 @see SpringApplication#withHook。)
hook listener 被追加在最后:
java
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 观察七个事件的完整时序
java
@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 的时机
java
@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 的间隔
java
@Component
public class SlowRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
Thread.sleep(5000);
}
}
再监听两个事件打时间戳。期望相差 ≥ 5 秒。
12.4 验证 BootstrapContext 的"已实例化不可替换"
java
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
对照:
java
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 规避:
java
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
java
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() 里:
java
MyClient client = context.getBootstrapContext().get(MyClient.class);
System.out.println("got client in resolver: " + client);
若 MyClient 未注册,应抛 IllegalStateException("... has not been registered")。
12.7 验证 KeepAlive
bash
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 依赖。
bash
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 抛异常,观察:
ApplicationFailedEvent是否被收到FailureAnalyzer的诊断报告是否输出(即使你在failed监听器里抛异常)- 退出码是否正确
13. 本章源码索引
13.1 核心
| 文件 | 关注点 |
|---|---|
SpringApplication.java |
run()(304 起)、createBootstrapContext()、prepareEnvironment()、prepareContext()、getRunListeners()、handleRunFailure()、Startup 抽象类与两个实现、KeepAlive |
SpringApplicationRunListener.java |
七个 default 方法 |
SpringApplicationRunListeners.java |
doWithListeners、七个 step 名、callFailedListener |
context/event/EventPublishingRunListener.java |
initialMulticaster、七个事件映射、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.java |
publish 静态方法 |
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 章讲:
BeanDefinitionLoader:五种 source 类型(Class/Resource/Package/CharSequence/AnnotatedBeanDefinitionReader直通)- 组件扫描 :
ClassPathBeanDefinitionScanner的过滤链、TypeExcludeFilter、AutoConfigurationExcludeFilter - Bean 命名 :
AnnotationBeanNameGenerator、BeanNameGenerator的两种策略、为什么同名 bean 会被覆盖(或报错) - 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
java
/**
* 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 的两条警告:
-
"before the Environment or ApplicationContext is available, but after the ApplicationListeners have been registered"
精确的时间窗:listener 已注册(
refreshApplicationListeners()已跑),但 Environment 还没创建。 -
"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
java
/**
* 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
java
/**
* 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 明确了三个已完成状态:
ApplicationContextis preparedApplicationContextInitializers have been called- before any bean definitions are loaded
第三条是它与 ApplicationPreparedEvent 的唯一区别。
典型用途 :在 BeanDefinition 加载前,调整 ClassPathBeanDefinitionScanner 的过滤器,或注册自定义的 BeanDefinitionRegistryPostProcessor。
A.4 ApplicationPreparedEvent
java
/**
* 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
java
/**
* 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),来源:
java
Duration timeTakenToStarted = startup.started();
...
listeners.started(context, timeTakenToStarted);
Startup.started() 总是返回非 null 的 Duration。但 SpringApplicationRunListener.started 的声明允许 null(给第三方实现用)。
A.6 ApplicationReadyEvent
java
/**
* 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() 里的条件:
java
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
}
ApplicationReadyEvent 不是必然发出的。
A.7 ApplicationFailedEvent
java
/**
* 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 | 多播器 |
|---|---|---|---|---|
ApplicationStartingEvent |
1.5.0 | Phillip Webb, Madhura Bhave | getBootstrapContext()(2.4.0) |
initial |
ApplicationEnvironmentPreparedEvent |
1.0.0 | Dave Syer | getBootstrapContext()(2.4.0)、getEnvironment() |
initial |
ApplicationContextInitializedEvent |
2.1.0 | Artsiom Yudovin | getApplicationContext() |
initial |
ApplicationPreparedEvent |
1.0.0 | Dave Syer | getApplicationContext() |
initial |
ApplicationStartedEvent |
2.0.0 | Andy Wilkinson | getApplicationContext()、getTimeTaken()(2.6.0) |
context |
ApplicationReadyEvent |
1.3.0 | Stephane Nicoll, Chris Bono | getApplicationContext()、getTimeTaken()(2.6.0) |
context |
ApplicationFailedEvent |
1.0.0 | Dave Syer | getApplicationContext()(nullable)、getException() |
视情况 |
附录 B · EnvironmentPostProcessorApplicationListener:事件到 SPI 的桥
这是"事件总线"如何驱动"S.PI"的最佳范例------它把 ApplicationEnvironmentPreparedEvent 转成 EnvironmentPostProcessor 的调用。第 06 章讲过后者,这里补全前者。
B.1 类定位
java
/**
* 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;
...
}
四点:
-
@since 4.0.0------虽然是 4.0 才"新建"的类,但它的职责(触发EnvironmentPostProcessor)从 1.x 就有(旧实现在ConfigFileApplicationListener里)。4.0 把它拆出来独立成类。 -
实现
SmartApplicationListener(不是ApplicationListener<...>) ------因为它要监听三种事件类型,用泛型表达不了。 -
Ordered,DEFAULT_ORDER = HIGHEST_PRECEDENCE + 10Integer.MIN_VALUE + 10------几乎是最高优先级。它必须在其它监听ApplicationEnvironmentPreparedEvent的监听器之前 跑,因为LoggingApplicationListener等需要EnvironmentPostProcessor处理完的环境。 -
包名
org.springframework.boot.support(4.x 迁移,3.x 是org.springframework.boot.context.config或org.springframework.boot.context)。
B.2 监听三种事件
java
@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?
java
private void onApplicationPreparedEvent() {
finish();
}
private void onApplicationFailedEvent() {
finish();
}
private void finish() {
this.deferredLogs.switchOverAll();
}
答案:DeferredLogs 的回放。
B.3 DeferredLogs:日志系统的先有鸡还是先有蛋
问题 :EnvironmentPostProcessor 在 ApplicationEnvironmentPreparedEvent 阶段跑,此时日志系统还没初始化 (LoggingApplicationListener 也是在这个阶段跑,有顺序依赖)。
解决 :DeferredLogs------一个"延迟日志"收集器。
java
private final DeferredLogs deferredLogs = new DeferredLogs();
EnvironmentPostProcessor 拿到的 DeferredLogFactory 会创建 DeferredLog,把消息缓存在内存里 。等日志系统就绪后,switchOverAll() 把缓存的消息回放到真正的 logger。
三个切换时机:
| 事件 | 调用 | 含义 |
|---|---|---|
ApplicationEnvironmentPreparedEvent |
(不切换,跑 processor) | 收集 |
ApplicationPreparedEvent |
finish() |
正常路径:上下文准备好,日志系统已就绪 → 回放 |
ApplicationFailedEvent |
finish() |
失败路径:也要回放,否则诊断信息丢失 |
为什么失败路径也要回放? 因为失败往往发生在日志系统就绪之前。如果不回放,"Config data location not found" 这类关键信息就永远看不到了。
这是一个非常实用主义的细节 ------Boot 宁可多写两个 if,也不让错误日志丢失。
B.4 主逻辑
java
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 的东西:
DeferredLogFactory(来自this.deferredLogs)ConfigurableBootstrapContext(来自事件的event.getBootstrapContext())ClassLoader(来自application.getResourceLoader())
ConfigurableBootstrapContext 出现在这里很关键 ------这就是 ConfigDataEnvironmentPostProcessor 能拿到 bootstrap context、进而 registerBootstrapBinder 的通道(第 10 章 §4.4)。
B.5 AOT 支持
java
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 两个工厂入口
java
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
java
/**
* 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 |
EnvironmentPostProcessor |
environmentPrepared |
Environment、bootstrapContext、DeferredLogFactory | 改 Environment | spring.factories |
ApplicationContextInitializer |
prepareContext 内 |
ApplicationContext | 改上下文配置 | spring.factories / addInitializers |
BootstrapRegistryInitializer |
createBootstrapContext |
BootstrapRegistry(只写) | 注册早期共享实例 | addBootstrapRegistryInitializers |
C.1 决策树
css
要改 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:
properties
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 / failed |
refresh() 内 |
D.1 途径 ① 与 ② 的注入点
application.getListeners() 是这两种途径的合集。EventPublishingRunListener.refreshApplicationListeners() 每次发事件前都把它们加进 initialMulticaster:
java
private void refreshApplicationListeners() {
this.application.getListeners().forEach(this.initialMulticaster::addApplicationListener);
}
"每次都重新加" ------所以途径 ③(在 contextLoaded 时)之外,你在 contextPrepared 里动态 addListeners(...) 也能被后续事件看到。
D.2 途径 ③ 的特殊性
contextLoaded 时:
java
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 排查清单
监听器没被调用?按序检查:
- 是不是
@Component+ 早期事件? → 改用途径 ①/② - 是不是在
spring.factories里写了但没被加载? → 检查文件路径是META-INF/spring.factories(不是*.imports) - 事件类型匹配吗? →
SmartApplicationListener.supportsEventType是否正确实现 - 是不是被异常吞掉了? → 前六个事件用
initialMulticaster(默认 error handler 会抛异常);ApplicationFailedEvent用LoggingErrorHandler(只 warn,不抛) - 是不是
@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存在之前,完成一整套需要"容器"才能做的复杂工作。
本章未逐行核实 / 待你实测的项
BootstrapRegistry的完整生命周期(含关闭时机)未逐行跟踪。- CRaC 相关 listener 的行为未实测(需要支持 CRaC 的 JDK)。
SpringApplicationRunListener的全部实现类未统计(含用户自定义与第三方)。- 事件在 AOT 模式下是否全部照常发布未验证。
EventPublishingRunListener的异常处理分支未逐个读。