本节目标:掌握 Bean 从实例化到销毁的完整流程,记住三种初始化回调的执行顺序,知道「启动后要跑一次」的逻辑该放在哪个时机,并理解销毁回调与优雅停机的关系。
适用版本:Spring Boot 4.1.x(Java 21)
Bean 生命周期全流程
一个 Bean 从被容器接管到最终销毁,要走过下面这些阶段。这是理解所有回调的骨架:
- 实例化:调用构造器创建对象。
- 属性填充:注入依赖(构造器参数、Setter、字段)。
- Aware 回调:如果 Bean 实现了
BeanNameAware、BeanFactoryAware、ApplicationContextAware,容器会把名字、工厂、上下文塞进来。 BeanPostProcessor.postProcessBeforeInitialization:所有后置处理器的「前置」回调。@PostConstruct:由CommonAnnotationBeanPostProcessor调用。InitializingBean.afterPropertiesSet():接口回调。- 自定义
init-method:@Bean(initMethod = "...")指定的方法。 BeanPostProcessor.postProcessAfterInitialization:「后置」回调,AOP 代理就在这一步生成。- Bean 就绪:可以被应用使用。
- 销毁(容器关闭时):
@PreDestroy→DisposableBean.destroy()→ 自定义destroyMethod。
记住一条主线:先注入依赖,再执行初始化回调。所有初始化回调(第 5~7 步)都发生在依赖注入(第 2 步)之后,这就是它们相对构造器的最大价值。
三种初始化回调的执行顺序实测
光看流程不够直观,直接跑一遍。准备一个图书缓存组件,同时实现三种初始化方式:
package com.example.book;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.InitializingBean;
public class BookCache implements InitializingBean {
public BookCache() {
System.out.println("[1] 构造器 BookCache()");
}
@PostConstruct
public void postConstruct() {
System.out.println("[2] @PostConstruct");
}
@Override
public void afterPropertiesSet() {
System.out.println("[3] InitializingBean.afterPropertiesSet()");
}
public void customInit() {
System.out.println("[4] 自定义 init-method");
}
@PreDestroy
public void preDestroy() {
System.out.println("[D] @PreDestroy");
}
}
注意 @PostConstruct / @PreDestroy 在 Spring Boot 4.x 下来自 jakarta.annotation 包(Jakarta EE 11 口径),不是老的 javax.annotation。
用 @Bean(initMethod = "...") 注册它,并指定自定义初始化方法:
@Configuration
public class BookConfig {
@Bean(initMethod = "customInit")
public BookCache bookCache() {
return new BookCache();
}
}
再加一个后置处理器,观察「前置 / 后置」回调的位置:
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof BookCache) {
System.out.println("[P-before] BeanPostProcessor 前置: " + beanName);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof BookCache) {
System.out.println("[P-after] BeanPostProcessor 后置: " + beanName);
}
return bean;
}
}
启动应用后,实测输出如下:
[1] 构造器 BookCache()
[P-before] BeanPostProcessor 前置: bookCache
[2] @PostConstruct
[3] InitializingBean.afterPropertiesSet()
[4] 自定义 init-method
[P-after] BeanPostProcessor 后置: bookCache
...(应用运行中)...
[D] @PreDestroy
顺序一目了然,可归纳成一条规律:
| 顺序 | 回调 | 由谁触发 |
|---|---|---|
| 1 | 构造器 | 容器实例化 |
| 2 | BeanPostProcessor 前置 | 后置处理器 |
| 3 | @PostConstruct | CommonAnnotationBeanPostProcessor |
| 4 | InitializingBean.afterPropertiesSet() | 容器 |
| 5 | 自定义 init-method | 容器 |
| 6 | BeanPostProcessor 后置 | 后置处理器(AOP 代理在此生成) |
也就是说:@PostConstruct 最早,afterPropertiesSet 居中,init-method 最后,三者被包在 BeanPostProcessor 的前置与后置之间。
三种方式怎么选:
| 方式 | 依赖 | 推荐度 |
|---|---|---|
@PostConstruct | 标准注解(Jakarta) | 首选,无侵入 |
InitializingBean | 需实现 Spring 接口 | 少用,与框架耦合 |
init-method | 无需改类,写注解 | 第三方类首选 |
结论:自己的类用 @PostConstruct,第三方类用 @Bean(initMethod = "...")。
@PostConstruct 与构造器的差别
两者都能「在对象刚建好时执行一段代码」,但差别关键:
| 对比项 | 构造器 | @PostConstruct |
|---|---|---|
| 依赖是否已全部注入 | 否,字段/Setter 依赖可能还没填 | 是,所有注入都已完成 |
| 调用次数 | 一次 | 一次 |
| 适合做什么 | 赋值、校验构造器参数 | 依赖就绪后的初始化(建索引、预热缓存) |
| 抛出异常 | 创建失败,启动中止 | 创建失败,启动中止 |
能否用 this 引用已注入字段 | 危险,可能 NPE | 安全 |
一个典型错误是在构造器里使用字段注入的依赖:
@Service
public class BookService {
@Autowired
private BookRepository repository;
public BookService() {
// 错误:此时 repository 还是 null,字段注入发生在构造器之后
System.out.println(repository.count());
}
}
正确做法是放进 @PostConstruct:
@Service
public class BookService {
@Autowired
private BookRepository repository;
@PostConstruct
public void warmUp() {
// 正确:此时 repository 已注入完毕
System.out.println("预热完成,当前图书数: " + repository.count());
}
}
Aware 回调
如果 Bean 需要知道「我在容器里的身份」,可以实现 Aware 系列接口,容器会在初始化回调之前回调它们:
| 接口 | 回调方法 | 能拿到什么 |
|---|---|---|
BeanNameAware | setBeanName(String) | Bean 的名字 |
BeanFactoryAware | setBeanFactory(BeanFactory) | 底层 BeanFactory |
ApplicationContextAware | setApplicationContext(ctx) | 完整上下文 |
EnvironmentAware | setEnvironment(Environment) | 环境与配置 |
绝大多数业务类不需要 Aware——需要什么依赖,注入进来即可。Aware 多用于框架级扩展。不要用它来「随手拿容器再 getBean」,那会把依赖关系藏起来,等于退回服务定位器反模式。
SmartInitializingSingleton
它有一个方法 afterSingletonsInstantiated(),在所有单例 Bean 都实例化完成之后调用,时机在上下文刷新接近结束、ApplicationRunner 之前。
适用场景:某段逻辑需要「等所有单例都就绪」才能跑,比如遍历容器中所有某类型 Bean 建立索引。如果只是单个 Bean 自己的初始化,用 @PostConstruct 就够了,不需要它。
ApplicationRunner / CommandLineRunner
「应用启动后要跑一次」的逻辑(加载示例数据、预热、打印统计),最合适的位置是这两个 Runner 接口。它们在上下文刷新完成后、启动日志打印之后、main 方法返回之前执行。
package com.example.book;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class BookDataLoader implements ApplicationRunner {
private final BookService bookService;
public BookDataLoader(BookService bookService) {
this.bookService = bookService;
}
@Override
public void run(ApplicationArguments args) {
bookService.save(new Book("978-7-111", "深入理解 Java 虚拟机", "周志明"));
System.out.println("示例图书已载入");
}
}
另一个接口 CommandLineRunner 几乎一样,只是参数形式不同:
@Component
public class SimpleRunner implements CommandLineRunner {
@Override
public void run(String... args) {
System.out.println("命令行参数: " + String.join(", ", args));
}
}
| 接口 | 参数 | 取参数方式 |
|---|---|---|
ApplicationRunner | ApplicationArguments | 可区分 option / non-option 参数 |
CommandLineRunner | String... | 原始命令行数组 |
选择建议:参数简单用 CommandLineRunner,需要按 --key=value 解析用 ApplicationRunner。多个 Runner 可用 @Order 控制顺序。
时机对照,把几个「初始化」放在一起看:
| 回调 | 触发时机 |
|---|---|
@PostConstruct | 单个 Bean 依赖注入完成后 |
afterSingletonsInstantiated() | 所有单例实例化完成后 |
ApplicationRunner.run() | 上下文刷新、启动日志打印之后 |
@EventListener(ApplicationReadyEvent.class) | 应用完全就绪后(与 Runner 很接近) |
销毁回调与优雅停机
销毁回调的执行顺序与初始化相反:@PreDestroy → DisposableBean.destroy() → 自定义 destroyMethod。它们只在容器正常关闭时触发,用于释放资源(线程池、连接、临时文件)。
@Bean(destroyMethod = "close")
public BookResource bookResource() {
return new BookResource();
}
Spring 对 @Bean 会自动推断 close() 或 shutdown() 方法作为销毁方法,但显式声明更稳妥。
优雅停机让「正在处理的请求」先处理完再关闭,避免请求被硬切断。开启方式:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
server.shutdown 默认是 immediate(立即关闭)。改成 graceful 后,容器停止接收新请求,等待在途请求完成,最多等 timeout-per-shutdown-phase。实测 4.1.1 的停机日志如下:
2026-10-09T15:42:08.968+08:00 INFO 43496 --- [ionShutdownHook] o.s.boot.tomcat.GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
2026-10-09T15:42:09.015+08:00 INFO 43496 --- [tomcat-shutdown] o.s.boot.tomcat.GracefulShutdown : Graceful shutdown complete
注意 4.x 的包名是 o.s.boot.tomcat.GracefulShutdown(3.x 是 o.s.b.w.e.tomcat.GracefulShutdown),这是 4.0 模块化重构的结果。优雅停机属于「生产可用性」的基本盘,第 18 章综合项目会再次用到。
常见坑
- 在构造器里使用注入的字段:字段/Setter 依赖此时还没注入,极易 NPE。改用
@PostConstruct。 - 在
@PostConstruct里做长耗时操作:会拖慢启动;网络请求、大批量预热要评估必要性。 - 把「全局启动后跑一次」的逻辑塞进
@PostConstruct:它是每个 Bean 各跑一次,且可能在依赖尚未全部就绪时执行。全局逻辑用ApplicationRunner。 - 依赖销毁回调来保存数据:容器被
kill -9时不会走销毁流程,别把关键持久化寄托在@PreDestroy。 - 忘记开启优雅停机:默认
immediate,滚动发布时在途请求会被切断。 destroyMethod写错方法名:不会报错,静默不执行,资源泄漏不易察觉。
小结
Bean 生命周期的主线是「先实例化并注入依赖,再执行初始化回调,最后在容器关闭时执行销毁回调」。三种初始化回调的执行顺序固定为 @PostConstruct → InitializingBean.afterPropertiesSet() → 自定义 init-method,且都被 BeanPostProcessor 的前置/后置回调包裹。选择上:自己的类用 @PostConstruct,第三方类用 @Bean(initMethod)。需要「全局就绪后跑一次」的逻辑交给 ApplicationRunner;资源释放交给 @PreDestroy 与 destroyMethod,并记得开启优雅停机。
阅读导航:上一节:5.2 注入方式与作用域 · 下一节:6.1 YAML 配置与 Profile 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。