环境:SpringBoot3.2.5
1. TaskScheduler接口
Spring 提供了一个TaskSchedulerSPI,其中包含各种用于调度任务的方法,以便在未来某个时间点运行。下面的列表显示了 TaskScheduler 接口定义:
public interface TaskScheduler {
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
}
上面接口只列出了核心方法。
在SpringBoot环境中,我们可以直接注入该接口,如下示例:
@Resource
private TaskScheduler taskScheduler ;
@PostConstruct
public void initScheduler() {
this.taskScheduler.schedule(() -> {
System.out.println("执行任务") ;
}, new CronTrigger("*/2 * * * * *")) ;
}
上面通过cron表达式控制任务执行周期。你也可以设置固定的执行速率。
this.taskScheduler.scheduleAtFixedRate(() -> {
System.out.println("固定周期指定任务") ;
}, Duration.ofSeconds(2)) ; // 每2s执行
在默认情况下SpringBoot创建的是ThreadPoolTaskScheduler
坑点:默认情况创建的ThreadPoolTaskScheduler只有一个线程,如果你当前有多个定时任务,如果出现重合那么任务会排队执行。通过如下参数修改线程池大小
spring:
task:
scheduling:
thread-name-prefix: pack-task
pool:
size: 2
这里的spring.task.scheduling.pool.size默认值为:1
2. 基于注解任务调用
基于注解方式实现任务的调用应该是我们工作中应用的主要方式,非常简单方便,如下示例:
// 以固定的时间间隔执行带有注释的方法,该时间间隔从上一次调用结束到下一次调用开始。
@Scheduled(fixedDelay = 2000)
public void fixedDelayTask() throws Exception {
System.err.printf("Current Time: %s, Current Thread: %s%n", new SimpleDateFormat("HH:mm:ss").format(new Date()), Thread.currentThread().getName()) ;
TimeUnit.SECONDS.sleep(1) ;
}
输出结果
虽然间隔设置为2s,但是实际输出是3s。
使用corn表达式
@Scheduled(cron = "*/3 * * * * *")
public void task1() {
// TODO
}
每隔3s执行
响应式支持
从Spring Framework 6.1开始,几种类型的反应式方法也支持@Scheduled方法:
@Scheduled(fixedRate = 2, timeUnit = TimeUnit.SECONDS)
public Flux<Integer> reactiveTask() {
return Flux.just(1, 2, 3).doOnNext(System.err::println) ;
}
在上面的示例中,会每隔2s打印1,2,3。
3. Cron表达式
一个格式良好的 cron 表达式(如 * * * * * *)由六个空格分隔的时间和日期字段组成,每个字段都有自己的有效值范围:
特别说明:
- 在范围(或 *)后面加上 /,表示数字值在范围内的间隔
- 用连字符 (-) 分隔的两个数字表示一个数字范围。指定的范围是包含在内的。如(10-15 * * * * *):每分钟内的10,11,12,13,14,15秒时都会运行
- 逗号 (,) 用于分隔列表中的项目;如(0 0 6,19 * * *)每天上午 6:00 和下午 7:00
写cron表达式不太好理解。为了提高可读性,Spring 支持以下表示常用序列的宏。因此,可以使用这些宏来代替六位数值:@Scheduled(cron = "@hourly")。
宏 | 描述 |
@yearly (or @annually) | 每年一次 (0 0 0 1 1 *) |
@monthly | 每月一次 (0 0 0 1 * *) |
@weekly | 每周一次 (0 0 0 * * 0) |
@daily (or @midnight) | 每天一次(0 0 0 * * *) |
@hourly | 每小时一次 (0 0 * * *) |
如下示例:
@Scheduled(cron = "@hourly")
public void task2() {
System.out.println("宏指令执行任务") ;
}
这样写简单多了。
4. 虚拟线程支持
从Spring6.1开始,支持虚拟线程(JDK21)执行任务的调用。在SpringBoot环境下你需要开启功能:
spring:
threads:
virtual:
enabled: true
如下示例:
@Scheduled(cron = "*/3 * * * * *")
public void scheduler1() throws Exception {
System.err.printf("当前时间: %s, 当前线程: %s, 是否虚拟线程: %b%n", new SimpleDateFormat("HH:mm:ss").format(new Date()), Thread.currentThread().getName(), Thread.currentThread().isVirtual()) ;
}
输出结果
图片
如果使用了虚拟线程,那么下面的配置将没有任何的意义。
spring:
task:
scheduling:
pool:
size: 10 #无意义
使用虚拟线程后,任务调用将使用单个调度线程,但每次执行计划任务时都会启动一个新线程(虚拟线程)。
5. 自定义任务调度配置
我们可以通过实现SchedulingConfigurer接口来自定义相关任务调度的设置,这通常用于设置在执行计划任务时使用的特定TaskScheduler bean,或者以编程方式注册计划任务。如下示例:
@Component
public class PackSchedulingConfigurer implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler() ;
taskScheduler.setThreadNamePrefix("my-task-") ;
taskScheduler.afterPropertiesSet();
taskRegistrar.setTaskScheduler(taskScheduler );
}
}
上面的示例修改任务调度执行的线程池对象。
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> {
System.out.println("动态注册调度任务...") ;
}, "*/2 * * * * *");
}
上面示例,通过编程的方式动态注册调度任务。