《Spring Boot 入门》5.3 生命周期与初始化回调

本节用图书缓存 BookCache 实测 Bean 从构造器到销毁的完整生命周期,用真实日志证明 @PostConstruct、InitializingBean、自定义 init-method 与 BeanPostProcessor 的执行顺序,并说明 SmartInitializingSingleton、ApplicationRunner 各自该用在什么时机。

本节目标:掌握 Bean 从实例化到销毁的完整流程,记住三种初始化回调的执行顺序,知道「启动后要跑一次」的逻辑该放在哪个时机,并理解销毁回调与优雅停机的关系。
适用版本:Spring Boot 4.1.x(Java 21)

Bean 生命周期全流程

一个 Bean 从被容器接管到最终销毁,要走过下面这些阶段。这是理解所有回调的骨架:

  1. 实例化:调用构造器创建对象。
  2. 属性填充:注入依赖(构造器参数、Setter、字段)。
  3. Aware 回调:如果 Bean 实现了 BeanNameAware、BeanFactoryAware、ApplicationContextAware,容器会把名字、工厂、上下文塞进来。
  4. BeanPostProcessor.postProcessBeforeInitialization:所有后置处理器的「前置」回调。
  5. @PostConstruct:由 CommonAnnotationBeanPostProcessor 调用。
  6. InitializingBean.afterPropertiesSet():接口回调。
  7. 自定义 init-method:@Bean(initMethod = "...") 指定的方法。
  8. BeanPostProcessor.postProcessAfterInitialization:「后置」回调,AOP 代理就在这一步生成。
  9. Bean 就绪:可以被应用使用。
  10. 销毁(容器关闭时):@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构造器容器实例化
2BeanPostProcessor 前置后置处理器
3@PostConstructCommonAnnotationBeanPostProcessor
4InitializingBean.afterPropertiesSet()容器
5自定义 init-method容器
6BeanPostProcessor 后置后置处理器(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 系列接口,容器会在初始化回调之前回调它们:

接口回调方法能拿到什么
BeanNameAwaresetBeanName(String)Bean 的名字
BeanFactoryAwaresetBeanFactory(BeanFactory)底层 BeanFactory
ApplicationContextAwaresetApplicationContext(ctx)完整上下文
EnvironmentAwaresetEnvironment(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));
    }
}
接口参数取参数方式
ApplicationRunnerApplicationArguments可区分 option / non-option 参数
CommandLineRunnerString...原始命令行数组

选择建议:参数简单用 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 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计