SpringBoot启动时执行一次定时任务的思考

Sabthever

在改一个定时任务的时候,业务同学提了个需求:项目启动的时候也跑一次同步,不然每次重启都得等到第二天凌晨才能看到最新数据。需求一句话,但落地的时候脑子里拐了好几道弯,记录一下。

一. 需求背景

项目里有个数据权限同步的定时任务,每天凌晨00:20执行,代码大概长这样:

1
2
3
4
5
6
7
8
@Scheduled(cron = "0 20 0 * * ?")
@Override
public void syncDataPermission() {
RedisCustomUtils.executeWithLock(redisson,
"scheduled:backend:inter:liveOrder:syncDataPermission",
0, 30, 5000L,
this::syncDataPermissionProcess);
}

需求很简单:启动一次 + 每天定时。但我没急着动手,先想了想。

二. 方案探索

(一) @PostConstruct

脑子里第一个跳出来的就是@PostConstruct,一行注解搞定:

1
2
3
4
@PostConstruct
public void init() {
syncDataPermission();
}

但手指悬在键盘上,没敲下去。

RedissonClient这时候真的”活”了吗?——@PostConstruct是当前Bean自己初始化完成后立即触发的。字段是注入了,但Redisson的连接池、Netty EventLoop这些底层资源,在我这个Bean init的时候,别的Bean可能还在排队初始化。要是Redis还没连上,分布式锁就抛异常,整个应用直接起不来。

一个同步任务把整个服务搞崩,这事儿要是发生在生产凌晨发布,运维能把我祭天。

启动会不会变慢?——@PostConstruct同步阻塞的,意味着Spring容器在跑完之前,根本不会进入Ready状态。K8s的readinessProbe等不到200,会以为服务挂了然后重启,重启了又跑一次同步,又超时……死循环。

万一以后加了@Transactional呢?——@PostConstruct阶段,AOP代理有时候还没织入完毕。那时候this::syncDataPermissionProcess调出去的是裸方法,事务直接失效。现在没事,不代表以后没事。

(二) CommandLineRunner

第二个想法。Spring Boot标配,启动完了跑一次,挺正经。但马上又被自己劝退了:

  • 新建一个类,或者让Service去implements CommandLineRunner职责不单一
  • 抛异常默认行为是让应用启动失败,和@PostConstruct一个毛病

(三) ApplicationReadyEvent

绕了一圈,想起了Spring Boot的事件体系:

1
2
3
4
5
6
7
8
9
10
11
12
13
ApplicationStartingEvent

ApplicationEnvironmentPreparedEvent

ApplicationContextInitializedEvent

ApplicationPreparedEvent

ContextRefreshedEvent ← 所有 Bean 装配完成

ApplicationStartedEvent

ApplicationReadyEvent ← 应用真正"可以接客"了 ✅

ApplicationReadyEvent触发的时机,是整个Spring Boot应用起飞完毕:所有Bean就位、AOP织入完成、Web容器开始接收请求、Redisson和数据源都连上了。这时候再去跑同步任务,没有任何后顾之忧。而且有个隐藏福利:异步、不阻塞启动,就算同步任务跑30秒,K8s探针该绿就绿。

三. 动手实践

最终代码长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;

/**
* 定时任务同步数据权限全集(每天 00:20 执行)
*/
@Scheduled(cron = "0 20 0 * * ?")
@Override
public void syncDataPermission() {
RedisCustomUtils.executeWithLock(redisson,
"scheduled:backend:inter:liveOrder:syncDataPermission",
0, 30, 5000L,
this::syncDataPermissionProcess);
}

/**
* 项目启动完成后执行一次数据权限同步
*/
@EventListener(ApplicationReadyEvent.class)
public void syncDataPermissionOnStartup() {
log.info("[InterCommon] 项目启动完成,触发一次数据权限同步");
try {
syncDataPermission();
} catch (Exception e) {
log.error("[InterCommon] 启动时同步数据权限失败", e);
}
}

几个细节:

细节 为什么这么写
try/catch兜底 启动同步失败不能影响应用本身,定时任务还能再补一次
复用syncDataPermission() Redisson分布式锁逻辑直接复用,多副本部署不会重复跑
log.info标记 出问题第一时间在启动日志里就能看到
不新增类、不引入Runner 改动最小,职责依然在Service自己手里

写完之后,顺便过了一遍方案对比:

方案 阻塞启动 异常会让服务起不来 AOP/事务可靠 依赖外部资源安全
@PostConstruct ⚠️
InitializingBean ⚠️
CommandLineRunner
ApplicationRunner
ApplicationReadyEvent ❌(可控)

注意是体感,不是绝对真理,但够用了。

四. 总结

  1. “能跑通”和”能放心睡觉”,差了好几层考虑。@PostConstruct一行能解决的事,不代表它适合解决。
  2. 生命周期注解的顺序,永远要想清楚再下手。 尤其是涉及Redis、DB、远程调用的,Ready之前都别轻举妄动。
  3. 启动逻辑要有兜底。 一次失败不致命,定时器会再帮你救一次场。
  4. 能用框架原生事件,就别自己造轮子。 ApplicationReadyEvent是Spring Boot给的”礼物”,用起来又稳又干净。

下次再有人问”启动时跑一次怎么搞”,我大概率不会再先想@PostConstruct了。

  • 标题: SpringBoot启动时执行一次定时任务的思考
  • 作者: Sabthever
  • 创建于 : 2026-06-09 15:30:00
  • 更新于 : 2026-06-09 15:15:23
  • 链接: https://sabthever.cn/2026/06/09/technology/java/SpringBoot启动时执行定时任务的思考/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。