如何在Spring Boot中使用ScheduledExecutorService实现每月1日执行定时任务
Spring Boot 基于 ScheduledExecutorService 实现每月1号定时任务方案
核心实现逻辑
ScheduledExecutorService 原生仅支持固定延迟、固定周期的调度能力,不直接支持月度级别的时间规则调度,因此我们采用「单次调度+动态计算下次执行时间」的逻辑实现需求:
- 每次提交单次调度任务,延迟时间为当前时间到下一个月1号0点的时间差
- 任务执行完成后,自动计算下一次执行的延迟时间,提交新的调度任务,循环往复
- 项目启动时自动触发第一次调度的提交
具体代码实现
1. 配置 ScheduledExecutorService 实例
首先在配置类中注册线程池Bean,避免每次手动创建线程池:
import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; @Configuration public class SchedulerConfig { @Bean public ScheduledExecutorService scheduledExecutorService() { // 单线程足够处理月度低频任务,可根据实际业务调整核心线程数 ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat("monthly-task-thread-%d") .setDaemon(false) .build(); return Executors.newScheduledThreadPool(1, threadFactory); } }
若项目未引入Guava依赖,可自行实现ThreadFactory接口自定义线程创建规则。
2. 实现定时任务调度逻辑
import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.Duration; import java.time.LocalDateTime; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @Service public class MonthlyTaskService implements InitializingBean { @Resource private ScheduledExecutorService scheduledExecutorService; /** * 自定义业务执行逻辑,替换为你的实际业务代码 */ private void runBusinessTask() { System.out.println("每月1号定时任务执行,当前时间:" + LocalDateTime.now()); } /** * 计算当前时间到下一个月1号0点的延迟毫秒数 */ private long calculateNextDelayMillis() { LocalDateTime now = LocalDateTime.now(); LocalDateTime nextExecuteTime = LocalDateTime.of( now.plusMonths(1).getYear(), now.plusMonths(1).getMonth(), 1, 0, 0, 0 ); return Duration.between(now, nextExecuteTime).toMillis(); } /** * 提交单次调度任务 */ private void submitScheduleTask() { scheduledExecutorService.schedule(() -> { try { // 执行业务逻辑 runBusinessTask(); } catch (Exception e) { // 必须捕获所有异常,避免任务抛出异常后终止后续调度 e.printStackTrace(); } finally { // 任务执行完成后提交下一次调度 submitScheduleTask(); } }, calculateNextDelayMillis(), TimeUnit.MILLISECONDS); } /** * Spring Bean初始化完成后自动触发第一次调度提交 */ @Override public void afterPropertiesSet() { submitScheduleTask(); } }
注意事项
- 必须在任务执行逻辑中捕获所有异常,未捕获的异常会导致当前调度任务终止,不会触发下一次调度提交
- 多实例部署场景下需要引入分布式锁(如Redis锁),任务执行前先抢锁,抢锁成功再执行业务,避免多实例重复执行任务
- 若需要支持当月1号已过的情况下启动服务补跑任务,可在首次提交调度前判断当前时间是否晚于当月1号0点,若晚于则先执行一次业务逻辑,再计算下一次调度延迟
- 如果没有强制要求使用ScheduledExecutorService,Spring自带的
@Scheduled注解直接配置cron表达式0 0 0 1 * ?即可实现每月1号0点执行,实现更简单
内容的提问来源于stack exchange,提问作者Amit Gairola
相关产品推荐
相关产品推荐

