启动失败发生在哪一步,会改变清理结果

“Spring 启动时创建所有 Bean,失败就销毁全部对象”不足以解释实际故障。有些对象是后处理器,业务对象创建前就必须存在;有些业务对象设置为懒加载,启动成功时仍未创建;某些前置准备失败甚至没有进入统一销毁的异常捕获范围。

本篇检验的问题是:refresh() 中的阶段顺序怎样限制扩展点,异常落点又怎样影响已创建对象?实验采用 Spring Framework 6.2.11、JDK 21 的 GenericApplicationContext,不借用 Boot 启动日志推断原生 Framework 行为。实现引用固定到 4c134254642d88e058aa004bdaf44168e1be7bb2。

refresh准备阶段、内部初始化阶段及异常清理范围

图据 AbstractApplicationContext.refresh 绘制。内层异常分支覆盖图中第二组初始化阶段;外层 finally 负责恢复启动线程标记并释放锁,它不等同于销毁 Bean。

refresh 是阶段协议,不是对象创建循环

容器先执行 prepareRefresh,记录启动状态并准备环境;随后 obtainFreshBeanFactory 取得供本次启动使用的工厂,prepareBeanFactory 为工厂配置上下文相关设施。方法名称中的 “fresh” 不能机械解释成所有 Context 都新建一个工厂:具体行为由子类实现,GenericApplicationContext 使用预先持有的工厂,并限制多次 refresh。

之后才进入本篇关注的内层初始化范围。子类可先执行 postProcessBeanFactory;容器调用工厂后处理器,再注册 Bean 后处理器;消息源、事件广播器、子类刷新钩子与监听器注册随后执行;finishBeanFactoryInitialization 完成剩余普通单例的预实例化;最后 finishRefresh 完成生命周期处理并发布刷新事件。refresh 固定实现

这些步骤不能随意交换。配置类本身包含其他 Bean 的声明,如果先创建业务对象再解析配置,依赖候选可能还没注册。自动代理处理器如果安装过晚,已暴露的引用可能没有经过它。把全部逻辑压成一次“遍历 Bean 并 new”,无法保留元数据扩展与实例扩展各自的处理窗口。

阶段 主要输入与状态变化 此时能做什么
定义注册 配置进入注册表 增加类、工厂方法、依赖描述
Registry 后处理 注册表可能继续增长 根据已有元数据补充定义
Factory 后处理 定义内容最终调整 修改属性值等创建参数
BPP 注册 处理器实例进入处理链 为后续实例的初始化与包装做准备
单例预实例化 描述转为对象,缓存保存最终引用 完成依赖、初始化与后处理
刷新完成 生命周期启动、刷新事件发布 暴露容器启动结果

这里的“BPP 注册”并不是仅保存一个类名。PostProcessorRegistrationDelegate.registerBeanPostProcessors 会调用 getBean 创建后处理器,再把处理器对象加入工厂。因此在普通业务对象批量创建之前,容器里已经可能存在后处理器及其依赖。

后处理器的两个窗口

BeanDefinitionRegistryPostProcessor 可以增加定义,BeanFactoryPostProcessor 可以修改工厂中的定义配置,BeanPostProcessor 则作用于创建中的对象。前两者名称相近,但操作目标仍是元数据;最后一种才能看到某个实际 Bean 引用,并可能返回替代引用。

注册表后处理器还能注册新的注册表后处理器。固定版本的委托实现因此不是只获取一次列表:它处理已知优先级组,并继续查找尚未执行的注册表处理器,直到没有新项,再调用相应工厂后处理回调。源码保留多轮查找与多个列表,是为了避免过早实例化错误优先级的处理器。后处理器调度实现

对容器自动发现的后处理器,需要区分 PriorityOrdered、Ordered 与其他对象;通过上下文编程接口直接加入的工厂后处理器走显式列表路径,不能把同一排序口诀覆盖所有注册方式。本篇实验只用受控的少量处理器证明阶段关系,完整排序规则留在扩展顺序篇。

工厂后处理回调在类型上持有工厂,因而可以调用 getBean,但“能调用”不等于“适合在这里创建业务对象”。过早创建的对象可能赶不上尚未安装的全部 BPP,后续不会仅因处理器安装完成就自动重新创建。需要调整配置时修改定义,避免用 getBean 当成读取元数据的工具。否则实验虽能运行,代理、注解注入等效果可能因对象创建过早而缺失。

模式提炼:先安装执行规则,再处理业务对象

参数化表达为 收集{规则定义} → 实例化并安装{规则} → 处理{业务对象}。

场景 先安装 后处理
Spring 容器 Bean 后处理器 普通业务 Bean
HTTP 服务 路由和过滤器 请求
编译流程 转换阶段及其配置 业务代码的中间表示

遇到“同一种对象有的生效、有的不生效”,除了检查规则是否存在,还应检查对象进入流程时规则是否已经安装。创建时点本身就是行为差异的来源。

用事件序列检查正常启动

实验入口注册一个 Registry 后处理器、一个 Factory 后处理器和一个 BPP。业务 Target 记录构造、初始化、销毁事件;BPP 的前置回调记录 before,后置回调记录 after 并返回一个 JDK 接口代理。刷新监听器记录 refreshed。

代理实验只证明 BPP 可改变容器最终暴露的引用,不把这个手写代理等同于 Spring AOP 自动代理。断言同时检查代理类型和一次真实调用的返回值,关闭后检查原始目标的销毁事件,避免只凭类名里出现 Proxy 就宣称生命周期正确。

1
2
cd examples/spring-framework-lab
./mvnw -q compile exec:java -Dexec.mainClass=blog.spring.Chapter03

完整可运行入口在 src/main/java/blog/spring/Chapter03.java,共用断言在 Checks.java。以下是该入口代码,包含正常启动与故障控制组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package blog.spring;

import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.GenericApplicationContext;
import static blog.spring.Checks.*;

public class Chapter03 {
static final List<String> events = new ArrayList<>();
public interface Operation { String execute(); }
public static class Target implements Operation, InitializingBean, DisposableBean {
public Target() { events.add("construct"); }
public void afterPropertiesSet() { events.add("init"); }
public String execute() { return "order"; }
public void destroy() { events.add("destroy"); }
}
public static class Broken { public Broken() { throw new IllegalStateException("intentional bean creation failure"); } }
public static class PrepareFailure extends GenericApplicationContext {
@Override protected void prepareRefresh() {
super.prepareRefresh();
throw new IllegalStateException("intentional prepareRefresh failure");
}
}
private static void failureBoundary(String phase) {
events.clear();
try (var context = phase.equals("prepareRefresh") ? new PrepareFailure() : new GenericApplicationContext()) {
context.registerBean("precreated", Target.class);
// Deliberately create a disposable singleton before refresh, to observe cleanup boundaries.
context.getBeanFactory().getBean("precreated");
if (phase.equals("factory")) context.addBeanFactoryPostProcessor(factory -> {
throw new IllegalStateException("intentional factory post-processor failure");
});
if (phase.equals("BPP")) context.registerBean("brokenProcessor", BeanPostProcessor.class, () -> {
throw new IllegalStateException("intentional BPP creation failure");
});
fails(RuntimeException.class, context::refresh, "03 " + phase + " failure propagates");
boolean outsideCatch = phase.equals("prepareRefresh");
check(events.contains("destroy") != outsideCatch, "03 " + phase + " cleanup boundary events=" + events);
check(context.isActive() == outsideCatch, "03 " + phase + " active=" + context.isActive());
check(context.getBeanFactory().containsSingleton("precreated") == outsideCatch, "03 " + phase + " singleton retention=" + context.getBeanFactory().containsSingleton("precreated"));
}
check(events.getLast().equals("destroy"), "03 " + phase + " explicit close leaves disposable destroyed");
}
public static void main(String[] args) {
events.clear();
try (var context = new GenericApplicationContext()) {
context.addBeanFactoryPostProcessor((BeanDefinitionRegistryPostProcessor) registry -> events.add("registry"));
context.addBeanFactoryPostProcessor(factory -> events.add("factory"));
context.registerBean("postProcessor", BeanPostProcessor.class, () -> new BeanPostProcessor() {
public Object postProcessBeforeInitialization(Object bean, String name) {
if (name.equals("target")) events.add("before");
return bean;
}
public Object postProcessAfterInitialization(Object bean, String name) {
if (!name.equals("target")) return bean;
events.add("after");
return Proxy.newProxyInstance(Operation.class.getClassLoader(), new Class<?>[]{Operation.class},
(proxy, method, arguments) -> method.invoke(bean, arguments));
}
});
context.registerBean("target", Target.class);
context.addApplicationListener(event -> { if (event instanceof ContextRefreshedEvent) events.add("refreshed"); });
context.refresh();
check(events.equals(List.of("registry", "factory", "construct", "before", "init", "after", "refreshed")), "03 refresh phase order " + events);
var operation = context.getBean("target", Operation.class);
check(Proxy.isProxyClass(operation.getClass()) && operation.execute().equals("order"), "03 BPP wrapper preserves callable behavior");
}
check(events.getLast().equals("destroy"), "03 close destroys original target behind wrapper");
events.clear();
try (var context = new GenericApplicationContext()) {
context.registerBean("first", Target.class);
context.registerBean("broken", Broken.class, d -> d.setDependsOn("first"));
fails(BeanCreationException.class, context::refresh, "03 singleton creation failure cancels refresh");
check(events.equals(List.of("construct", "init", "destroy")), "03 failed refresh destroys already created disposable singleton " + events);
check(!context.isActive(), "03 failed context inactive");
}
failureBoundary("factory");
failureBoundary("BPP");
failureBoundary("prepareRefresh");
System.out.println("CHAPTER 03 PASS");
}
}

本地命令在 Amazon Corretto 21.0.11 / Spring 6.2.11 下退出 0,末行是 CHAPTER 03 PASS。原始输出在 evidence/03/local-20261002/run.txt;预期异常对应的 WARN 属于故障实验,不能单凭它判断命令失败。

场景 清理前后的实际观察 判定
正常刷新 registry, factory, construct, before, init, after, refreshed 阶段顺序断言成立
普通单例创建失败 construct, init, destroy;active=false 既有单例销毁
Factory 后处理失败 active=false;单例缓存不保留预创建对象 内层清理分支执行
BPP 创建失败 active=false;单例缓存不保留预创建对象 内层清理分支执行
父类准备完成后抛错 active=true;预创建单例仍保留;显式 close 后 destroy 此故障在内层清理范围之外

事件序列是本实验的断言,不是全框架的完整生命周期清单。比如记录到 factory 与 construct 相邻,不代表框架没有在中间创建 BPP;只表示日志没有给 BPP 构造器单独打点。源码调度范围负责解释没有记录的步骤,日志负责证明本次观察到的先后关系。

失败清理必须同时看异常位置与已有对象

固定版本 refresh 的内层 catch (RuntimeException | Error) 调用 destroyBeans(),接着 cancelRefresh(ex) 重置 active 状态,再向调用者传播原异常。普通构造器失败、工厂后处理失败以及 BPP 安装阶段失败,只要传播到这个范围,都会到达该分支。

这并不表示每次都能观察到销毁事件。若失败前没有成功创建可销毁的单例,销毁集合可能没有对应业务对象。因此实验故意预先通过内部工厂创建一个可销毁的 Target,再分别在 Factory 后处理与 BPP 创建阶段抛异常。这个预创建操作是为了固定“失败时已存在对象”的控制条件,不是推荐的应用初始化方式。

普通业务创建失败的实验则注册 first 与 broken,显式设置后者依赖前者,确保可销毁对象先完成初始化。broken 构造器抛异常后,断言检查先前对象的 destroy 事件以及 Context 的 inactive 状态。这里只验证已有且纳入容器销毁管理的对象。构造器若取得资源后抛出异常,资源尚未交付容器,就需要在构造器自身的异常路径中释放。

前置准备失败是反例。prepareRefresh、obtainFreshBeanFactory、prepareBeanFactory 位于上述内层 try 之前。从这些步骤抛出的异常不会经过同一个 destroyBeans/cancelRefresh 分支。实验在调用父类 prepareRefresh 后抛异常,让 active 标记已经被设置,并用预创建对象观测此时尚未自动销毁;随后显式 close,验证最终释放。这个结果不应扩展成“任何 Context 的任何准备失败都一定仍是 active”,因为具体抛出时点和子类实现会改变状态。

模式提炼:按失败边界计算清理集合

参数化表达为 清理对象 = 已成功登记的资源 ∩ 当前异常处理范围负责的资源。

场景 已成功登记 尚未登记或不在本范围
容器启动 已注册销毁回调的单例 失败构造器中的临时资源
批量文件处理 已打开并纳入关闭逻辑的文件 打开动作自身失败前的临时状态
事务操作 已加入事务的数据库修改 外部网络副作用与普通内存字段

“异常被捕获”不等于“所有副作用已撤回”。清理结果应由具体资源终态证明,而不是从一次 catch 或一条 close 日志推断。

单例预实例化也有边界

finishBeanFactoryInitialization 最终进入工厂的预实例化逻辑,普通路径处理非抽象且符合单例创建条件的定义。prototype 不因这个阶段自动变成单例;lazy Bean 如果被另一个非懒单例依赖,仍可能因依赖解析而创建。Framework 6.2.11 还支持标记后台初始化并配置相应执行器的分支,本实验没有启用它,因此单线程事件表不用于证明所有启动场景都由同一线程串行执行。

预实例化后还有 SmartInitializingSingleton.afterSingletonsInstantiated 回调。上游 GenericApplicationContextTests 分别测试普通 Bean 创建失败与该回调抛异常时,已创建对象被销毁。这说明“构造器都执行成功”并不等于 refresh 已经成功;后面的初始化回调、生命周期阶段和同步事件处理仍可能失败。上游故障清理测试

上述上游测试只做源码阅读;本地运行覆盖的是入口中的故障控制组,没有执行整套 Spring 上游 Gradle 测试。资源释放结论也限于实验记录的 DisposableBean,没有据此宣称真实连接池、后台线程或网络请求已经释放。

练习与解答

练习一。 一个 Factory 后处理器调用 getBean("service"),之后才安装一个包装 service 的 BPP。启动没有异常,能否推断 service 已被包装?

解答:不能。业务对象可能已经在处理链不完整时创建并缓存,后续安装 BPP 不会自动补做该实例的全套创建流程。先检查对象的第一次创建时点与最终引用,把元数据修改留在 Factory 后处理器,把实例包装留在 BPP。

练习二。 broken 构造器抛异常后,日志里没有 first.destroy,能否判定 refresh 清理失效?

解答:不能先下结论。要检查 first 是否在失败前创建成功、是否登记销毁回调、异常是否落在统一清理范围。使用显式依赖建立创建前后关系,比依靠两个定义的偶然注册顺序更适合故障实验。

模式速查

观察 对应模式 优先证据
某个 Bean 没经过完整后处理 规则安装早于业务创建 首次 getBean 时点与 BPP 安装时点
失败后仍有对象未销毁 按异常范围计算清理集合 抛出位置、资源登记与销毁终态
构造器全成功却启动失败 启动是多阶段协议 后续回调、生命周期与刷新事件
故障组没有销毁日志 先确认资源是否存在 显式依赖或预创建控制条件

参考资料

实验工程与环境准备见00:容器实验入口。继续阅读04:配置类解析与 Bean 方法调用。