SpringBoot初始化方法和执行顺序

SpringBoot 启动时到底谁先执行?@PostConstruct、InitializingBean、ApplicationListener、CommandLineRunner 和 ApplicationRunner 经常被放在一起比较,但它们所处阶段并不一样。这篇用时间线和代码示例讲清每种方式适合做什么、常见执行顺序,以及为什么 ApplicationListener 不能简单排成一个固定位置。

SpringBoot 项目启动时,经常需要提前做一些事情。

比如加载一批基础数据、校验配置、预热缓存、注册第三方客户端,或者启动一个定时任务。

于是我们会遇到一堆看起来相似的选择:

  • @PostConstruct
  • InitializingBean
  • ApplicationListener
  • CommandLineRunner
  • ApplicationRunner

它们都能在启动阶段执行代码,但执行时机并不一样。

如果不分清生命周期,很容易把数据库初始化放到 Bean 还没准备好的阶段,或者把本来应该启动完成后执行的任务塞进了 Bean 初始化方法。

这篇不追求把 Spring 源码每一行都抠出来,先用一条时间线把它们放到正确位置,再看每种方式适合干什么。

先看完整时间线

SpringBoot 启动可以先粗略理解成下面几个阶段:

SpringApplication.run
        │
        ▼
创建环境和 ApplicationContext
        │
        ▼
准备 BeanDefinition 和配置
        │
        ▼
刷新 ApplicationContext
        │
        ├── 创建 Bean
        │     ├── 依赖注入
        │     ├── @PostConstruct
        │     ├── InitializingBean
        │     └── 其他初始化回调
        │
        ├── ContextRefreshedEvent
        │
        ▼
ApplicationStartedEvent
        │
        ├── CommandLineRunner
        ├── ApplicationRunner
        │
        ▼
ApplicationReadyEvent
        │
        ▼
应用正式就绪
​

这张图里最重要的是分成两段:

  • Bean 初始化阶段:@PostConstruct、InitializingBean
  • 应用启动阶段:ApplicationListener 监听事件、CommandLineRunner、ApplicationRunner

不过 ApplicationListener 是一个范围很大的接口。

它可以监听启动早期事件,也可以监听容器刷新事件,还可以监听应用就绪事件,所以不能简单说它一定在某个方法前面或后面。

Bean 初始化阶段

@PostConstruct 和 InitializingBean 都发生在 Bean 创建过程中。

它们适合做当前 Bean 自己的初始化,不适合承担整个应用启动后的大任务。

@PostConstruct

@PostConstruct 是最常见的初始化方式。

它表示:依赖注入完成后,在 Bean 正式对外使用前,执行这个方法。

@Component
public class UserCache {

    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @PostConstruct
    public void init() {
        // 依赖已经注入,可以做当前 Bean 的准备工作
        loadBaseUsers();
    }

    private void loadBaseUsers() {
        // 初始化当前 Bean 需要的数据
    }
}
​

@PostConstruct 的特点:

  • 写法简单,业务代码里最常见
  • 依赖注入完成后执行
  • 一个 Bean 中通常只放一个
  • 方法不能依赖整个应用已经完全启动

SpringBoot 3 使用 jakarta.annotation.PostConstruct。

SpringBoot 2 常见的是 javax.annotation.PostConstruct。

这个包名差异,升级版本时很容易踩到。

InitializingBean

InitializingBean 是 Spring 提供的生命周期接口。

实现它之后,重写 afterPropertiesSet,Spring 会在属性设置完成后调用。

@Component
public class UserCache implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
        // Bean 属性已经设置完成
        loadBaseUsers();
    }

    private void loadBaseUsers() {
        // 当前 Bean 的初始化逻辑
    }
}
​

它和 @PostConstruct 做的事情很像。

同一个 Bean 里如果两者都存在,通常是 @PostConstruct 先执行,然后执行 afterPropertiesSet,之后才是配置的自定义 init-method。

实例化 Bean
   ↓
属性注入
   ↓
@PostConstruct
   ↓
InitializingBean.afterPropertiesSet
   ↓
自定义 init-method
   ↓
Bean 初始化完成
​

两者怎么选

我个人更喜欢优先使用 @PostConstruct。

它对业务代码侵入小,不需要为了一个初始化动作去实现 Spring 生命周期接口。

InitializingBean 也不是不能用,适合框架基础设施、公共组件,或者项目本身已经大量采用这种风格的场景。

不要在同一个 Bean 里同时堆很多种初始化回调。

这样出了问题,日志里会出现一串初始化方法,很难判断到底是哪一步改了状态。

ApplicationListener 监听事件

ApplicationListener 不是一个单独的固定时间点。

它的作用是监听 Spring 或 SpringBoot 发布的事件。

不同事件发生的时间不同,监听器的位置也就不同。

监听应用就绪事件

如果我们希望应用已经启动完成,再执行一些初始化任务,可以监听 ApplicationReadyEvent。

@Component
public class ReadyListener
        implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 应用已经完成启动流程,可以执行就绪后的任务
        warmUpCache();
    }

    private void warmUpCache() {
        // 预热缓存或通知其他系统
    }
}
​

ApplicationReadyEvent 通常在 CommandLineRunner 和 ApplicationRunner 执行完之后发布。

因此,监听这个事件的任务一般可以认为是在 Runner 之后执行。

监听应用开始事件

ApplicationStartedEvent 发布得更早一些。

此时 ApplicationContext 已经刷新完成,但 Runner 还没有执行。

@Component
public class StartedListener
        implements ApplicationListener<ApplicationStartedEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        // 容器已经启动,但 CommandLineRunner 还未开始
        recordStarted();
    }

    private void recordStarted() {
        // 记录启动阶段信息
    }
}
​

如果你要在 Runner 之前做一件事,可以考虑监听这个事件。

监听容器刷新事件

ContextRefreshedEvent 在 ApplicationContext 刷新完成时发布。

这个时间点已经晚于当前 Bean 的 @PostConstruct 和 InitializingBean,但通常早于 SpringBoot 的 Runner。

@Component
public class ContextListener
        implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 容器刷新完成,Bean 基本已经准备好
        validateContext();
    }

    private void validateContext() {
        // 做容器级检查
    }
}
​

ApplicationListener 没有统一顺序

下面这种说法是不严谨的:

ApplicationListener 一定在 @PostConstruct 前执行
​

因为你监听的事件不同,执行时间就不同。

更准确的说法是:

ApplicationStartingEvent       启动非常早期
ApplicationContextInitializedEvent  上下文初始化阶段
ContextRefreshedEvent          容器刷新完成
ApplicationStartedEvent        Runner 执行前
ApplicationReadyEvent          Runner 执行后
​

同一种事件有多个监听器时,可以使用 @Order 或实现 Ordered 控制它们之间的顺序。

但 @Order 主要控制同一事件监听器的处理顺序,不是用来控制所有 Bean 的创建顺序。

两种 Runner

CommandLineRunner 和 ApplicationRunner 都是在 SpringBoot 应用启动阶段执行一次。

它们都发生在 ApplicationContext 刷新完成之后,并且通常在 ApplicationReadyEvent 之前执行。

CommandLineRunner

CommandLineRunner 的参数是原始命令行字符串。

@Component
@Order(10)
public class DataInitRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // 适合执行简单的启动任务
        initData();
    }

    private void initData() {
        // 初始化基础数据
    }
}
​

它适合参数格式简单、只需要拿到字符串数组的场景。

ApplicationRunner

ApplicationRunner 的参数是 ApplicationArguments,SpringBoot 已经帮我们把命令行参数做了一层解析。

@Component
@Order(20)
public class ArgumentRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // 可以读取非选项参数和选项参数
        if (args.containsOption("profile")) {
            loadProfile(args.getOptionValues("profile"));
        }
    }

    private void loadProfile(List<String> values) {
        // 根据启动参数加载配置
    }
}
​

例如启动命令里传入 --profile=test,ApplicationRunner 可以通过 ApplicationArguments 读取到 profile 这个选项。

如果只是想执行一段无参数初始化逻辑,CommandLineRunner 更直接。

如果要解析 --key=value 这种启动参数,ApplicationRunner 更顺手。

Runner 之间怎么排序

SpringBoot 会收集容器中的 CommandLineRunner 和 ApplicationRunner,统一排序后执行。

它们不是两个互相独立的队列。

@Component
@Order(1)
public class FirstRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // 第一个执行
    }
}

@Component
@Order(2)
public class SecondRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // 第二个执行
    }
}
​

通常可以得到:

FirstRunner
    ↓
SecondRunner
    ↓
ApplicationReadyEvent
​

如果没有指定 @Order,不要依赖它们的偶然执行顺序。

启动任务之间有前后依赖时,明确加上 @Order,或者把它们合并成一个有清晰步骤的启动组件。

Runner 抛异常会怎样

Runner 发生未处理异常时,SpringBoot 启动流程可能失败,ApplicationReadyEvent 也不会正常发布。

因此启动任务里要区分两类错误:

  • 核心配置、数据库结构不满足要求:应该让启动失败,避免应用带病运行
  • 可选缓存预热、非核心通知失败:可以记录日志,让应用继续启动

不要所有异常都 catch 后打印一行日志就算了。

有些初始化失败必须让发布系统感知到,否则进程虽然活着,实际上已经不能正常提供服务。

五种方式怎么选

把它们放在一起对比:

方式 大致时机 适合做什么
@PostConstruct 当前 Bean 注入完成后 Bean 自身初始化
InitializingBean 当前 Bean 属性设置完成后 Spring 组件生命周期回调
ApplicationListener 对应事件发布时 监听容器或应用状态变化
CommandLineRunner Context 刷新后、Ready 前 简单启动任务和命令行参数
ApplicationRunner Context 刷新后、Ready 前 解析结构化启动参数

可以按下面的决策思路来选:

只是当前 Bean 自己准备一下?
    └── @PostConstruct

需要监听某个 SpringBoot 生命周期事件?
    └── ApplicationListener

应用启动后执行一次任务?
    ├── 参数简单:CommandLineRunner
    └── 需要解析参数:ApplicationRunner
​

如果任务必须等整个应用“可用”后再执行,监听 ApplicationReadyEvent 往往更合适。

如果任务必须在 Runner 之前完成,则考虑 ApplicationStartedEvent 或更明确的启动编排。

常见误区

把 @PostConstruct 当成应用启动完成

@PostConstruct 只代表当前 Bean 初始化完成。

它不代表所有 Bean 都创建完了,更不代表 Web 服务已经对外完全就绪。

所以不建议在这里做跨模块的大规模初始化。

用 @Order 控制 Bean 创建顺序

@Order 主要用于排序某些可排序对象,比如事件监听器和 Runner。

它不是通用的 Bean 创建顺序开关。

如果 Bean 之间存在硬依赖,使用构造器注入、@DependsOn 或调整设计,比到处添加 @Order 更靠谱。

同时使用多种方式初始化同一份数据

一个数据表既在 @PostConstruct 里初始化,又在 Runner 里初始化,最后经常变成重复插入或执行两遍。

初始化入口要集中,幂等也要做好。

把耗时任务堵在启动线程

Runner 默认会阻塞后续启动流程。

如果里面做大批量同步、长时间远程调用或全量缓存预热,应用可能长时间无法发布成功。

这种任务可以拆成启动前置检查和启动后异步任务,别把所有事情都塞进启动钩子。

小结

SpringBoot 初始化方法并不是一条简单的固定队列。

@PostConstruct 和 InitializingBean 属于 Bean 初始化阶段,解决当前 Bean 怎么准备。

ApplicationListener 取决于监听的事件,可能很早,也可能在应用就绪时执行。

CommandLineRunner 和 ApplicationRunner 都在容器刷新后、ApplicationReadyEvent 之前执行,并且可以通过 @Order 统一排序。

可以先记住这条主线:

Bean 注入完成
  -> @PostConstruct
  -> InitializingBean
  -> Context 刷新完成
  -> ApplicationStartedEvent
  -> CommandLineRunner / ApplicationRunner
  -> ApplicationReadyEvent
​

实际项目里,当前 Bean 的小初始化用 @PostConstruct,生命周期组件可以用 InitializingBean,应用级启动任务用 Runner,状态通知和事件编排用 ApplicationListener。

别只问“谁先执行”,先问这段代码属于 Bean 初始化、容器刷新,还是应用真正就绪。位置找对了,启动逻辑自然就顺了。

更多推荐

章节目录