Spring Boot 项目启动时,经常需要在容器就绪后立刻做一些初始化工作,比如预热点数据到缓存、校验外部服务连通性、注册定时任务、打印启动成功信息等。这些逻辑如果写在普通的构造方法或者静态代码块里,往往拿不到完整的 Spring 上下文,依赖注入也可能还没生效。Spring Boot 在启动流程中预留了多个扩展点,选对扩展点并理解它们的执行时机,是写出可靠初始化逻辑的关键。

一、先搞清楚 Spring Boot 启动流程中的扩展时机
Spring Boot 的启动入口是 SpringApplication.run(),整个流程大致分为创建应用上下文、准备环境、刷新容器、执行 Runner、发布就绪事件几个阶段。理解这个顺序非常重要,因为不同的扩展点在不同阶段被触发,能拿到的资源也不一样。
具体来说,InitializingBean 的 afterPropertiesSet 方法在 Bean 属性注入完成后立即调用,此时容器还没有完全刷新完毕,其他 Bean 可能还在创建中;而 ApplicationRunner 和 CommandLineRunner 则是在容器刷新完成之后执行,此时所有单例 Bean 都已就绪,是执行业务初始化的最佳时机;ApplicationReadyEvent 事件则更靠后一些,代表应用已经完全准备好对外提供服务。
如果把需要依赖其他 Bean 的初始化逻辑放在构造方法里,大概率会遇到空指针,因为构造方法执行时属性注入尚未发生。这是一个非常常见的误区,很多人以为 @PostConstruct 和构造方法差别不大,实际上前者发生在依赖注入完成之后,后者发生在依赖注入之前,两者能安全访问的资源完全不同。
二、用 ApplicationRunner 实现启动后自动执行逻辑
ApplicationRunner 是最推荐的启动扩展方式,它的 run 方法接收一个 ApplicationArguments 参数,可以方便地解析命令行参数。下面是一个完整的示例,演示启动时预热缓存并校验外部依赖:
@Component
public class CacheWarmUpRunner implements ApplicationRunner, Ordered {
private final UserService userService;
private final RedisTemplate<String, Object> redisTemplate;
// 构造器注入,避免字段注入带来的隐患
public CacheWarmUpRunner(UserService userService,
RedisTemplate<String, Object> redisTemplate) {
this.userService = userService;
this.redisTemplate = redisTemplate;
}
@Override
public void run(ApplicationArguments args) throws Exception {
List<User> hotUsers = userService.findHotUsers();
for (User user : hotUsers) {
redisTemplate.opsForValue().set(
"user:" + user.getId(), user, 30, TimeUnit.MINUTES);
}
System.out.println("缓存预热完成,共加载 " + hotUsers.size() + " 条数据");
}
@Override
public int getOrder() {
return 1; // 数值越小越先执行
}
}
当项目中存在多个 Runner 时,可以通过实现 Ordered 接口或者添加 @Order 注解来控制执行顺序。需要注意 ApplicationRunner 和 CommandLineRunner 是等价的,区别只在于参数形式:前者把命令行参数解析成键值对,后者直接给原始字符串数组。两者不要混用太多,保持团队风格统一即可。
另外一个细节是异常处理。如果 Runner 中的 run 方法抛出异常,整个应用启动会失败并退出。对于非关键性的初始化逻辑,建议在内部做好 try-catch,只记录日志不阻断启动;而对于数据库连接校验这类关键检查,则应该让异常抛出去,避免应用带着问题状态对外提供服务。
三、监听启动事件实现更细粒度的控制
除了 Runner,Spring Boot 还提供了丰富的事件机制。容器刷新完成后会依次发布 ApplicationStartedEvent 和 ApplicationReadyEvent,监听这些事件同样可以执行初始化逻辑:
@Component
public class StartupEventListener {
private static final Logger log =
LoggerFactory.getLogger(StartupEventListener.class);
@EventListener
public void handleReady(ApplicationReadyEvent event) {
long startTime = event.getTimestamp().getTime();
log.info("应用已就绪,开始执行延迟初始化任务");
// 适合放非阻塞的异步任务,比如指标上报
CompletableFuture.runAsync(() -> {
// 上报启动指标、注册服务发现等
});
}
}
事件监听的优势在于可以拿到 ApplicationReadyEvent 携带的完整上下文信息,包括启动耗时、环境变量等,适合做启动监控和链路上报。而 Runner 的优势是接口简单、顺序可控,适合业务层面的初始化。两者可以结合使用:Runner 处理核心数据准备,事件监听处理旁路任务。
四、封装成自定义 Starter 供其他工程复用
如果初始化逻辑需要在多个项目间共享,最佳做法是把它封装成自定义 Starter。核心思路是:提供一个自动配置类,在里面注册 Runner 或初始化 Bean,然后通过 spring.factories 文件让 Spring Boot 自动发现这个配置。
@Configuration
@ConditionalOnProperty(prefix = "app.warmup",
name = "enabled", havingValue = "true",
matchIfMissing = true)
public class WarmUpAutoConfiguration {
@Bean
public ApplicationRunner warmUpRunner(WarmUpProperties properties) {
return args -> {
System.out.println("预热开关:" + properties.getEnabled());
System.out.println("预热数据量:" + properties.getSize());
};
}
@ConfigurationProperties(prefix = "app.warmup")
@Bean
public WarmUpProperties warmUpProperties() {
return new WarmUpProperties();
}
}
然后在 Starter 工程的 src/main/resources/META-INF 目录下创建 spring.factories 文件,内容如下(Spring Boot 2.7 之后也可以使用 AutoConfiguration.imports 文件替代):
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.warmup.WarmUpAutoConfiguration
这样其他工程只需在 pom 中引入这个 Starter,并在配置文件中按需调整参数,初始化逻辑就会在启动时自动执行,完全不需要业务方写任何代码。@ConditionalOnProperty 注解还提供了开关能力,出问题时可以一键关闭预热逻辑,这在生产运维中非常实用。
总结一下,简单的启动初始化用 ApplicationRunner 就够了;需要异步或监控场景用事件监听;跨项目复用则封装成自定义 Starter。掌握这三个层次的扩展方式,基本可以应对 Spring Boot 启动阶段的各类初始化需求。
Spring Boot启动扩展ApplicationRunner自定义Starter修改时间:2026-09-10 17:38:41