Bean 生命周期与 BeanPostProcessor
Bean 的创建包含几个可以分别失败的阶段:构造器产生实例,属性和注解处理器填入依赖,初始化方法准备本对象,后处理器可能把目标包装成代理,容器最后向调用者提供引用。理解这些阶段,才能判断某个回调中有哪些依赖可用,以及谁应该释放已经取得的资源。
实例怎样成为容器中的 Bean
普通创建路径与提前短路
普通路径由 AbstractAutowireCapableBeanFactory 协调。它先得到合并定义;实例化前处理器如果返回替代对象,普通创建会被短路。没有替代对象时进入 doCreateBean:
createBeanInstance:构造器 / 工厂方法 / supplier
→ MergedBeanDefinitionPostProcessor:处理、缓存注入等元数据
→ 条件满足时登记 early-reference factory
→ populateBean:属性填充与注解依赖注入
→ initializeBean
├── BeanNameAware / BeanClassLoaderAware / BeanFactoryAware
├── BPP beforeInitialization(包括注解初始化处理器)
├── InitializingBean.afterPropertiesSet
├── 自定义 initMethod
└── BPP afterInitialization(可能返回代理)
→ 核对提前引用,登记必要销毁动作
→ 单例创建调用者发布最终引用@PostConstruct 由初始化注解处理器在 beforeInitialization 链中执行,不是所有 before 回调执行完之后才额外插入的一步。其他 Aware 接口如 ApplicationContextAware 也由相应处理器参与,不能把全部 Aware 接口都压成一个固定回调。7.0.9 创建实现
构造器中只有构造参数和自己建立的状态可用。字段注入尚未发生;在构造器里读取自动注入字段得到 null 属于顺序问题。初始化回调适合验证配置、建立本地数据结构;容器创建期间进行无界外部 I/O,会把整条依赖子图一起阻塞。初始化回调
完整程序观察回调
下载生命周期实验。运行环境为 Linux Docker,镜像 Maven 3.9.12 / Temurin 25,Spring 7.0.9、Jakarta Annotation 3.0.0,Java 编译目标 17。使用有 Docker 权限的普通账号,实验不连接外部服务。
mkdir spring-lifecycle-lab
unzip spring-framework-bean-lifecycle-processors-lab.zip -d spring-lifecycle-lab
cd spring-lifecycle-lab
mkdir -p .m2
IMAGE=maven:3.9.12-eclipse-temurin-25
docker pull "$IMAGE"src/main/java/example/FirstLifecycle.java 同时设置多个不同名称的回调,方便观察顺序。实际组件选择一种足够的初始化和关闭形式即可:
package example;
import java.util.*;
import jakarta.annotation.*;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.*;
public class FirstLifecycle {
static final List<String> events = new ArrayList<>();
public static class Worker implements BeanNameAware, InitializingBean, DisposableBean {
public Worker() { events.add("construct"); }
public void setBeanName(String name) { events.add("aware:" + name); }
@PostConstruct public void prepare() { events.add("postConstruct"); }
public void afterPropertiesSet() { events.add("afterPropertiesSet"); }
public void init() { events.add("customInit"); }
@PreDestroy public void release() { events.add("preDestroy"); }
public void destroy() { events.add("destroy"); }
public void close() { events.add("customClose"); }
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean(initMethod="init", destroyMethod="close") Worker worker() { return new Worker(); }
@Bean static BeanPostProcessor observer() {
return new BeanPostProcessor() {
public Object postProcessBeforeInitialization(Object bean, String name) {
if(name.equals("worker")) events.add("observerBefore");
return bean;
}
public Object postProcessAfterInitialization(Object bean, String name) {
if(name.equals("worker")) events.add("observerAfter");
return bean;
}
};
}
}
public static void main(String[] args) {
try(var context = new AnnotationConfigApplicationContext(Config.class)) {
if(!context.containsBean("worker")) throw new AssertionError();
System.out.println("ready=" + events);
}
System.out.println("closed=" + events);
}
}observer() 使用 static 工厂,创建该处理器无需先构造配置类。程序中的列表只在同一启动线程中使用,不用于保存并发业务状态。
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId><artifactId>lifecycle-lab</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>7.0.9</version></dependency>
<dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>3.0.0</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version><configuration><parameters>true</parameters></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version></plugin>
</plugins></build>
</project>docker run --rm --user "$(id -u):$(id -g)" -e MAVEN_CONFIG=/tmp/maven \
-v "$PWD:/work" -v "$PWD/.m2:/cache" -w /work "$IMAGE" \
sh -ec 'mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/cache \
clean package dependency:build-classpath -Dmdep.outputFile=target/classpath.txt
java -cp "target/classes:$(cat target/classpath.txt)" example.FirstLifecycle'构建容器采用宿主 UID/GID,输出和缓存必须可写;这与 Docker daemon 的执行权限是两件事。依赖无法下载时配置组织批准的 Maven settings,不要改用来源未知的 JAR。已准备好镜像和完整缓存的离线环境可用 mvn -o。Maven settings
应用输出的顺序为:
ready=[construct, aware:worker, observerBefore, postConstruct,
afterPropertiesSet, customInit, observerAfter]
closed=[construct, aware:worker, observerBefore, postConstruct,
afterPropertiesSet, customInit, observerAfter,
preDestroy, destroy, customClose]这里为便于查阅折行,程序实际每个列表输出一行。观察器的 before 发生在此配置下的 PostConstruct 之前;改变处理器注册和排序后,两者相对位置可能变化。保持稳定的关系是:注解初始化在对应 before 处理器中执行,随后才调用接口和自定义初始化方法。
初始化正常返回后,Context 才继续完成普通单例创建。关闭时,这三个不同方法按 @PreDestroy → DisposableBean → customClose 执行;相同方法若通过多个机制登记,框架会避免简单重复调用。资源自身的 close 仍应幂等,允许部分初始化后安全释放。注解生命周期
后处理器怎样改变对象
两条链均按正序执行
BeanPostProcessor 的 before 与 after 是两次独立遍历。每次把上一个处理器返回的对象交给下一个;它们不是 Filter 那种一进一出、逆序退栈模型。
before:原始对象 → A.before → B.before → 当前对象
init: 当前对象执行初始化回调
after: 当前对象 → A.after → B.after → 最终对象任一处理器返回 null,会终止当前那一条处理器遍历并保留之前的结果;不是把 Bean 注册为 null,也不自动取消后续所有生命周期阶段。包装器需要保持调用者要求的可赋值类型,否则最终注入会失败。BeanPostProcessor API
扩展实验手工注册 A、B 两个处理器,保证仅观察注册顺序:
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.LifecycleLab order'输出 [before:A, before:B, after:A, after:B],程序也对完整列表断言。由 Context 自动发现的处理器按 PriorityOrdered、Ordered、普通处理器分组;直接 addBeanPostProcessor 的处理器按注册顺序,不能期待 Order 注解自动重排。处理器登记
MergedBeanDefinitionPostProcessor 常保存注入元数据;它与处理每个实例的普通 BPP 职责不同。自定义缓存若强引用业务 Class,应考虑容器关闭、定义重置和类加载器释放,不能只用类名作为跨应用缓存键。
短路与阻止属性填充
InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation 可以直接返回替代对象。框架随后执行 afterInitialization 处理,但通常不再走普通构造、属性填充和目标初始化。postProcessAfterInstantiation 返回 false 则会跳过正常属性填充;postProcessProperties 用于处理或调整属性。
实例化前返回对象?
├── 是:替代对象 → afterInitialization → 返回
└── 否:创建实例 → 实例化后判断
├── 允许:填充属性
└── 拒绝:跳过填充
→ 初始化 → afterInitialization这些接口适合框架集成,不适合为了少写一个工厂方法而接管生命周期。返回自建代理之前,必须明确目标何时创建、依赖怎样注入、失败资源怎样释放。实例化感知处理器
自动代理创建器通常在初始化之后包装目标。因此在 PostConstruct 中通过 this 调用事务方法,仍然是目标内部调用。应由另一个 Bean 在适当生命周期阶段经代理调用,或者把初始化内容改为纯本地准备;AOP 调用路径决定增强是否执行。
初始化失败、运行启动与关闭
失败对象不能等待正常 destroy 补救
若 afterPropertiesSet 抛异常,创建尚未完成,销毁适配器可能还未登记。Context 刷新失败会清理已经创建并被管理的单例,但这不能替代失败组件释放自己刚取得的资源。
实验 FailInit 的核心做法是取得资源后,在初始化失败分支局部回收:
public void afterPropertiesSet() {
live.incrementAndGet();
try {
throw new IllegalStateException("bad config");
} catch (RuntimeException failure) {
live.decrementAndGet();
throw failure;
}
}计数器表示待管理资源数量,真实代码替换为已取得的连接、文件或执行器引用。不要吞掉异常后留下未就绪 Bean。
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.LifecycleLab failure'日志先报告 Context 刷新取消,程序输出 initFailed=true locallyReleased=true destroyWasNotFallback=true。它检查资源数回到零,且没有靠 DisposableBean 回调完成回收。若改动导致资源仍未释放,程序非零退出;应修正获取点的异常处理,再创建全新的 Context 复测。
初始化完成与运行组件启动
SmartInitializingSingleton.afterSingletonsInstantiated 在常规单例预实例化完成后回调,可做跨 Bean 检查;后来才创建的懒单例不自动进入此前已经结束的回调轮次。
持续运行的消费者、调度器等适合 Lifecycle 或 SmartLifecycle。isAutoStartup() 决定自动启动,phase 较低者先启动、较高者先停止;显式依赖关系还会影响依赖组件的顺序。phase 表达启动协调,不替代远端可用性检查。SmartLifecycle
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.LifecycleLab lifecycle'输出 [start, stop, callback]。实验同步完成 stop 后调用回调;真正异步关闭时,必须在停止动作结束后再调用该 callback。过早回调会让容器继续销毁仍被使用的依赖,漏调则可能等到关闭阶段超时。
prototype 通常只得到创建和初始化处理,使用者负责后续关闭。强制终止进程也无法保证任何回调完整执行;持久业务结果应在业务执行中提交,而不是留到 PreDestroy。
根据阶段定位扩展故障
not eligible for getting processed by all BeanPostProcessors 指向过早创建:某个处理器依赖业务 Bean,或者定义级处理器为了取数据调用 getBean,导致对象在完整 BPP 链登记前已经产生。诊断先检查该 Bean 的创建依赖和处理器的工厂方法,不要先调整业务代理开关。
| 现象 | 首查位置 | 处理与复测 |
|---|---|---|
| 构造器读到 null 字段 | 字段注入发生在构造之后 | 必需依赖改为构造参数,直接 new 与 Context 两种创建分别测试 |
| init 中增强不执行 | 调用者是否已经拿到代理 | 把需要代理的操作交给协作 Bean,断言增强实际执行 |
| BPP 包装后类型不兼容 | 注入点类型和最终接口 | 使用公开接口或兼容代理,重测非命中 Bean |
| init 失败后线程仍存活 | 失败前的资源获取点 | 局部 catch 释放、保留 cause,新建 Context 再验证 |
| close 长时间等待 | SmartLifecycle phase 与 callback | 找到未完成停止动作,限制操作时长并正确回调 |
| 关闭后仍有资源 | prototype、自建对象或静态引用 | 由创建/持有方显式关闭,取消静态缓存引用 |
对自定义处理器临时记录 Bean 名、是否命中和耗时即可;不要在每次回调里遍历整个容器。运行在每个 Bean 上的 I/O 会随对象数量放大。定位启动慢时,将处理器总耗时与最慢单个 Bean 分开统计,一个组件阻塞与普遍扫描过宽需要不同修复。
完成实验后所有 Context 均已 close。退出码非零时保留最深 cause 和模式名;修复后重跑该模式及正常关闭模式。target 和 .m2 只属于当前实验目录,可以保留供离线复测。
权威资料与规范地址
可按接口、注解和实现类查阅详细契约。
