SpringBoot初始化方法和执行顺序
SpringBoot 启动时到底谁先执行?@PostConstruct、InitializingBean、ApplicationListener、CommandLineRunner 和 ApplicationRunner 经常被放在一起比较,但它们所处阶段并不一样。这篇用时间线和代码示例讲清每种方式适合做什么、常见执行顺序,以及为什么 ApplicationListener 不能简单排成一个固定位置。
SpringBoot 项目启动时,经常需要提前做一些事情。
比如加载一批基础数据、校验配置、预热缓存、注册第三方客户端,或者启动一个定时任务。
于是我们会遇到一堆看起来相似的选择:
@PostConstructInitializingBeanApplicationListenerCommandLineRunnerApplicationRunner
它们都能在启动阶段执行代码,但执行时机并不一样。
如果不分清生命周期,很容易把数据库初始化放到 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 初始化、容器刷新,还是应用真正就绪。位置找对了,启动逻辑自然就顺了。
