IOC 容器与 Bean 生命周期
排查一次启动失败,先问:失败的是定义登记、实例构造、依赖填充,还是初始化?再问:谁已经取得资源,谁负责回收?本篇只讲容器阶段和对象生命周期;早期引用的身份协议见循环依赖,调用入口见AOP,应用 ready 见Boot。固定源码为 Framework v6.1.12,Java API 为 17,不把版本基线当作生产升级建议。
定义不是实例,单例不是全局锁
订单服务、仓储、数据源首先是三份 BeanDefinition:记录类或工厂方法、scope、构造参数、属性、init/destroy 等创建策略。登记定义不等于执行构造器。BeanFactory 负责按名称和类型获取、创建与解析依赖;ApplicationContext 在其上协调环境、资源、事件、后处理器及刷新关闭。因此“扫描到了类”和“getBean 成功”是两个证据。XML/注解/Java 三类定义来源与扫描导入的可启动对照见配置与候选解析。
还要区分定义、原始 target、对外 exposedObject。后处理器可以返回包装对象,最终引用不必等于构造器创建的引用。FactoryBean 又增加一种创建策略:getBean("mapper") 通常返回产物,getBean("&mapper") 返回工厂;MyBatis 当前正文解释 MapperFactoryBean,不在这里重复 Mapper 链。
singleton 是特定容器、beanName 下的共享对象,不是 JVM 全局唯一,也不承诺可变字段线程安全。两个 Context 可各有一个同类单例;单例字段保存“当前请求用户”仍可能串请求。scope 是获取策略,并发隔离要另有状态归属或同步协议。
refresh:定义处理先于普通对象
输入为已登记入口定义的 Context。prepareRefresh / obtainFreshBeanFactory / prepareBeanFactory 准备刷新与工厂,之后才进入下面的内层 try。Context 子类可以改变准备方式,例如不能把 XML Context 重建工厂的行为套在 GenericApplicationContext 上。
原码:AbstractApplicationContext.refresh,Framework v6.1.12,连续 601–649 行;只删除共同缩进,未拼接分支。固定来源。
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (RuntimeException | Error ex ) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
contextRefresh.end();
}这里有两种完全不同的处理器。BeanDefinitionRegistryPostProcessor 能增加定义,BeanFactoryPostProcessor(BFPP)能修改定义;ConfigurationClassPostProcessor 解析配置类、扫描、Import 和 Bean 方法,形成创建规则。BeanPostProcessor(BPP)则参与每个对象的创建。registerBeanPostProcessors 是发现、实例化并注册实例处理器,不是此处一次性执行完所有业务对象的 before/after。
PostProcessorRegistrationDelegate按 PriorityOrdered、Ordered、无排序组处理自动发现的扩展;注册表处理器还可能登记新处理器。程序化登记要看登记顺序,不能概括成“所有处理器按 order 全局排序”。
消息源和事件广播器准备后,onRefresh 为子类提供扩展点;注册监听器后,finishBeanFactoryInitialization 设置转换和值解析等服务、冻结定义配置、创建剩余非懒单例。finishRefresh 再处理生命周期启动和刷新事件。普通对象通常在完整 BPP 注册之后创建,但 BFPP 中提前 getBean 会打破这个前提。为构造 BPP 而提前拉起其业务依赖也有同样问题:后来加入的代理处理器不会自动把已发布对象重做一遍。
图中箭头表示阶段先后,不承诺所有 Bean 在同一阶段才第一次创建。定位“有注解但没增强”时,记录第一次创建栈、定义来源和 beanName,优先排除处理器尚未注册,而不是盲调 order。
实例创建与初始化的可见边界
常见获取路径为 doGetBean → createBean → doCreateBean。前者处理名称、共享对象、合并定义、dependsOn 和 scope;createBean 允许实例化前的处理器返回替代对象,以下描述的是未短路的普通路径。
createBeanInstance 可选择供应器、工厂方法或构造器。构造器依赖在这个阶段解析;属性/字段注入主要在随后的 populateBean。AutowiredAnnotationBeanPostProcessor 的 postProcessProperties 使用注入元数据完成注解字段/方法注入,并非 JVM 自动识别注解。因此构造器里读稍后注入的字段可能仍为 null;容器外 new 也不会自动获得这套过程。
保留原文真实摘录:AbstractAutowireCapableBeanFactory.initializeBean,v6.1.12,1793–1813 行。固定来源。
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null), beanName, ex.getMessage(), ex);
}
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}输入已经过依赖填充。BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 在 invokeAwareMethods 中调用;ApplicationContextAware 等由相应 BPP 参与,不能把所有 Aware 都归入这一行。注册了注解处理器时,PostConstruct 在 before 阶段触发,然后 InitializingBean.afterPropertiesSet,再调用不同名的自定义 init;同名回调存在去重,不可机械累加。
wrappedBean 的传递允许 BPP 改变对象身份;synthetic 定义跳过此处的 before/after。init 抛错会包装成带 beanName 和定义资源的 BeanCreationException,after 不再按正常路径继续。生命周期方法执行成功只说明这个阶段成功;它既不证明依赖图全部就绪,也不证明调用经过最终代理。初始化中的 this 调用边界回到 AOP 篇分析。
lazy 与 prototype:延迟和所有权
lazy 单例没有被其他对象需要时,不在预实例化阶段创建,首次 getBean 才付构造与初始化成本。但非懒单例若直接依赖它,依赖解析仍会提前创建;lazy 不是禁止创建的屏障。要真正延迟获取可以使用 Provider 等显式入口,同时承认首次使用才发现故障的代价。
prototype 每次从容器获取可得到新实例,创建与初始化仍受容器管理;交付后通常不负责完整销毁。将 prototype 直接注入 singleton 只解析一次,并不使单例每次工作自动换新对象。例如 ExportService 保存 Writer.buffer,第一次写 A,第二次写 B,未清理可能得到 A+B。每次从 ObjectProvider 获取 Writer 并 finally 关闭,才同时明确状态与资源所有权。request scope 还要求活动请求上下文,后台线程不能靠 scope 代理凭空创造一个请求。
refresh 失败:清理不是回滚
S1 的 catch 只包住 601 行开始的内层 try。它捕获 RuntimeException/Error 后调用 destroyBeans(默认销毁单例)、cancelRefresh,再抛出。前面的 prepareRefresh / obtainFreshBeanFactory / prepareBeanFactory 位于584–599 行,不在此 catch 范围;外层 finally 才负责线程标记与锁。不要说“refresh 任何位置失败都走同一清理”。
已创建并登记销毁的 Resource 会在后续 Probe.init 失败时关闭;Probe 自身初始化未完成,不能假设已经登记 destroy。资源分配到一半失败的对象必须在获取处释放半成品,不能只依赖 PreDestroy。构造器发出的消息、已提交的数据库写入或泄漏到其他线程的 this 也不会被容器撤销。
还要保留异常清理自身失败的边界:若自定义清理抛错,不能仅凭顺序代码宣称 cancelRefresh 和原异常重抛一定都执行。故障报告应分别说明初始异常、已完成清理、清理异常与外部残留,而不是把 active=false 当成全局恢复证明。
关闭顺序与外部副作用
正常 close 处理关闭事件、生命周期停止及单例销毁。DestructionAwareBeanPostProcessor、PreDestroy、DisposableBean、自定义 destroy 各有契约;DisposableBeanAdapter处理回调与重复方法。依赖登记使依赖方通常先于被依赖资源销毁。应先停接入并排空,再停工作者,最后关闭池;需要启动/停止协调的组件使用 Lifecycle/SmartLifecycle,而不是在构造器里启动不可追踪线程。
进程强杀不保证回调执行;排空还要有超时。多副本各有 Context,各自会执行初始化,单例不能提供集群互斥。初始化写入必须有独立的唯一约束、幂等与恢复状态。这里不展开数据库事务协议。
实验:真实回调与资源责任
从仓库根执行 JAVA_HOME=$(/usr/libexec/java_home -v17) bash labs/spring-senior/run.sh lifecycle,或用实验说明中的 Maven 单模式命令。输入是 GenericApplicationContext、可关闭 Resource、带属性的 Probe、容器发现的 BFPP/BPP、未被依赖的 lazy 和可关闭 prototype。
- normal 的断言检查真实事件:BFPP → BPP 构造 → Probe 构造 → populate → aware → before → init → after;不是硬编码输出一份“预期日志”。BPP 的构造和其首次回调共同表明发现注册发生在普通对象初始化前。
- refresh 返回时 lazy 不存在;第一次 getBean 创建一次,再次获取身份相同。两个 prototype 身份不同,close Context 后尚未关闭,调用方 finally 主动关闭。
- fail 让 Probe.init 抛出指定异常,验证 after/destroy 未执行、Resource.close 执行一次、Context 不再 active;所有路径关闭 Context。
本批已通过 normal/fail,实测事件与断言一致;原码来源 v6.1.12 与实际运行 Liberica 17.0.19+11 分开记录。未测注解 PostConstruct/PreDestroy、request scope、自定义清理抛错、进程强杀;正文对应结论来自契约或推演。唯一可运行源码内嵌如下,可复制,不提供生产下载伪链接。
package study.spring;
import java.util.*;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.config.*;
import org.springframework.context.support.GenericApplicationContext;
public class LifecycleLab {
static final List<String> events = new ArrayList<>();
public static class Resource implements AutoCloseable {
public void close() { events.add("resource.close"); }
}
public static class Worker implements AutoCloseable {
boolean closed;
public void close() { closed = true; events.add("worker.close"); }
}
public static class Probe implements BeanNameAware, InitializingBean, DisposableBean {
final Resource resource;
final boolean fail;
String label;
Probe(Resource resource, boolean fail) {
this.resource = resource; this.fail = fail; events.add("construct");
}
public void setLabel(String value) { label = value; events.add("populate"); }
public void setBeanName(String name) { events.add("aware"); }
public void afterPropertiesSet() {
if (!"configured".equals(label)) throw new AssertionError("property not populated");
events.add("init");
if (fail) throw new IllegalStateException("synthetic init failure");
}
public void destroy() { events.add("destroy"); }
}
public static class RecordingProcessor implements BeanPostProcessor {
public RecordingProcessor() { events.add("bpp.construct"); }
public Object postProcessBeforeInitialization(Object bean, String name) {
if (bean instanceof Probe) events.add("before");
return bean;
}
public Object postProcessAfterInitialization(Object bean, String name) {
if (bean instanceof Probe) events.add("after");
return bean;
}
}
public static void main(String[] args) {
events.clear();
String mode = args.length == 0 ? "normal" : args[0];
if (!List.of("normal", "fail").contains(mode)) throw new IllegalArgumentException(mode);
boolean fail = mode.equals("fail");
var c = new GenericApplicationContext();
var workers = new ArrayList<Worker>();
c.registerBean("resource", Resource.class, Resource::new, bd -> bd.setDestroyMethodName("close"));
c.registerBean("worker", Worker.class, Worker::new, bd -> {
bd.setScope("prototype"); bd.setDestroyMethodName("close");
});
c.registerBean("lazy", Object.class, () -> { events.add("lazy.construct"); return new Object(); }, bd -> bd.setLazyInit(true));
c.registerBean("probe", Probe.class, () -> new Probe(c.getBean(Resource.class), fail),
bd -> bd.getPropertyValues().add("label", "configured"));
c.registerBean("definitionProcessor", BeanFactoryPostProcessor.class, () -> factory -> {
if (factory.containsSingleton("probe") || factory.containsSingleton("recordingProcessor"))
throw new AssertionError("ordinary bean or BPP created before BFPP callback");
events.add("bfpp");
});
// Discovered and registered by the context, not manually inserted into its factory.
c.registerBean("recordingProcessor", RecordingProcessor.class);
try {
c.refresh();
if (fail) throw new AssertionError("expected init failure");
if (!events.equals(List.of("bfpp", "bpp.construct", "construct", "populate", "aware", "before", "init", "after")))
throw new AssertionError(events);
if (c.getBean("probe") != c.getBean("probe")) throw new AssertionError("singleton identity");
if (c.getBeanFactory().containsSingleton("lazy")) throw new AssertionError("lazy created during refresh");
Object lazy = c.getBean("lazy");
if (lazy != c.getBean("lazy") || Collections.frequency(events, "lazy.construct") != 1)
throw new AssertionError("lazy first lookup");
workers.add(c.getBean(Worker.class)); workers.add(c.getBean(Worker.class));
if (workers.get(0) == workers.get(1)) throw new AssertionError("prototype identity");
} catch (BeansException e) {
if (!fail || !(e.getMostSpecificCause() instanceof IllegalStateException)
|| !"synthetic init failure".equals(e.getMostSpecificCause().getMessage())) throw e;
if (!events.equals(List.of("bfpp", "bpp.construct", "construct", "populate", "aware", "before", "init", "resource.close")))
throw new AssertionError(events);
if (c.isActive()) throw new AssertionError("failed context still active");
} finally {
c.close();
try {
if (Collections.frequency(events, "resource.close") != 1) throw new AssertionError("resource cleanup");
if (fail == events.contains("destroy")) throw new AssertionError("probe destroy registration boundary");
if (!fail && events.indexOf("destroy") > events.indexOf("resource.close")) throw new AssertionError("dependent destruction order");
if (workers.stream().anyMatch(w -> w.closed)) throw new AssertionError("container destroyed prototype");
} finally {
workers.forEach(Worker::close); // Ownership transferred to caller; release even if an assertion fails.
}
}
if (workers.stream().anyMatch(w -> !w.closed)) throw new AssertionError("caller leaked prototype");
System.out.println("PASS lifecycle " + mode + " " + events);
}
}架构师推演:启动预算与迁移责任
给定 400 个单例串行初始化共 18 秒、平台要求 10 秒 ready。全局 lazy 推迟部分开销,但把错误和首次请求延迟搬到运行期;预热全部依赖仍需支付原成本。先用启动步骤区分网络 I/O、锁等待、纯计算,确定关键路径,不用“扫描慢”归因一切。
方案一保留关键依赖预创建,把可选报表/缓存延迟到 warming 状态,并对相关端点降级;成本是冷请求延迟与额外状态。方案二阻塞直到关键预热完成,再接流量;成本是启动窗口变长,优点是早失败。应用 ready 与异步工作完成的时序由 Boot 篇统一解释,此处决策的是哪些资源必须由生命周期负责。
多副本数据迁移不应藏进每个 Bean.init。用专门迁移工具或数据库协议控制重入、锁失效、回滚责任;验证重启不会重复写入、停止可排空、连接可归还。可口述主线是“定义图 → 创建规则 → 实例回调 → 管理资源的清理”,连续追问则改变首次创建时点、scope 和失败位置,不能只背方法名。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。