Appearance
在Spring Boot中实现定时任务
本文主要介绍如何在在Spring Boot中实现定时任务。通过注解和代码两种方式实现。
1. 基础使用
首先在任意的配置类上使用注解@EnableScheduling开启定时任务支持,这里我们选择在主类上开启:
java
@SpringBootApplication
@EnableScheduling
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
}然后,通过注解@Scheduled标注在组件里的方法,使得该方法成为定时任务:
java
@Component
public class ScheduleTask {
@Scheduled(fixedRate = 1000)
public void run() {
System.out.println("ScheduleTask---The time is now " +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}关键点:
- 使用
@Component将类放入容器中,使得组件可以被管理调度; - 使用
@Scheduled(fixedRate = 1000)指定方法为定时任务(以下都称被@Schedule标注的方法为任务),必须指定调度模式;
之后,启动程序就可以看到每秒在控制台上打印提示信息了。
2. 定时任务的调度模式
在@Schedule注解中,存在几个调度模式,决定了任务以什么频率执行。
2.1 initialDelay
initialDelay决定了任务第一次调度执行前需要等待的时间,单位为毫秒。如果只指定该模式,那么任务只会执行一次。
java
@Scheduled(initialDelay = 1000)2.2 fixedRate
fixedRate决定了两次任务开始执行的间隔时间,单位为毫秒。如:
java
@Scheduled(fixedRate = 1000)
public void run() {
System.out.println("ScheduleTask---The time is now " +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}即假设第一次任务执行的时间在10:10:01,那么第二次任务的执行时间在10:10:02。
假设任务的执行时间超过了fixedRate所指定的间隔时间,那么第二次任务该如何执行呢?我们可以做个实验,让任务休眠3秒:
java
@Scheduled(fixedRate = 1000)
public void run() throws InterruptedException {
Thread.sleep(3000);
System.out.println("ScheduleTask---The time is now " +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}再次运行程序,发现提示信息是以3秒的间隔时间打印的。
所以如果任务的执行时间超过了调度的间隔时间,那么下一次任务调度会等到上一次任务结束。
2.4 fixedDelay
fixedDelay指定了下一次的任务开始时间,距离上一次任务的结束时间的时间间隔,单位为毫秒。我们以下面的例子做实验:
java
@Scheduled(fixedDelay = 1000)
public void run() throws InterruptedException {
Thread.sleep(3000);
System.out.println("ScheduleTask---The time is now " +
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}可以看到提示信息是以4秒的间隔时间打印的。因为任务本身执行需要耗时3秒,然后fixedDelay指定了下一次任务调度需要延时1秒,所以总共4秒打印一次。
2.5 cron
cron表示通过cron表达式控制任务的调度时间。例如:
java
@Scheduled(cron = "* * * * * *")
public void run() throws InterruptedException {
System.out.println("begin ScheduleTask---The time is now " + LocalDateTime.now());
Thread.sleep(3000);
System.out.println("end ScheduleTask---The time is now " + LocalDateTime.now());
}在上面的例子中,cron表达式是* * * * * *,表示每秒执行一次。注意,任务调度存在延迟,即假如第一次任务开始于20:01:01,任务本身耗时3秒,那么下一次任务应该开始于20:01:04,但下一次任务时间开始于20:01:05。
什么是cron表达式?参考:https://docs.spring.io/spring-framework/reference/integration/scheduling.html#scheduling-cron-expression
Cron表达式 是一种用于描述时间间隔的字符串,它在Unix系统上广泛用于调度任务。通过Cron表达式,你可以精确地定义一个任务在何时执行,比如每天的某个时间,每周的某一天,或者每月的一定日期。
Cron表达式的组成
一个Cron表达式通常由6个字段组成,分别表示:
- 秒(Seconds):0-59之间的整数
- 分(Minutes):0-59之间的整数
- 小时(Hours):0-23之间的整数
- 日(Day of month):1-31之间的整数
- 月(Month):1-12之间的整数或JAN-DEC
- 星期(Day of week):0-7之间的整数(0或7表示星期日)或SUN-SAT
这些字段之间用空格分隔。

Cron表达式中可以使用一些特殊的字符来表示范围、列表、步长等:
- ***或?:**表示匹配该字段的任意值。
- -: 表示一个范围。
- ,: 表示枚举多个值。
- /: 表示步长。
示例:
- 每秒钟执行一次:
* * * * * * - 每天凌晨2点执行一次:
0 0 2 * * * - 每月第一天凌晨0点执行一次:
0 0 0 1 * * - 每周一、三、五的10点执行一次:
0 0 10 * * 1,3,5 - 每隔5秒钟执行一次:
*/5 * * * * *
我们可以使用spring提供的CronExpression.isValidExpression()静态方法来校验字符串是否为正确的cron表达式:
java
@Test
void test01(){
// 下面是正确的
System.out.println(CronExpression.isValidExpression("* * * * * *"));
System.out.println(CronExpression.isValidExpression("0 0 2 * * *"));
System.out.println(CronExpression.isValidExpression("0 0 0 1 * *"));
System.out.println(CronExpression.isValidExpression("0 0 10 * * 1,3,5,7"));
System.out.println(CronExpression.isValidExpression("*/5 * * * * 0,2,4,6"));
System.out.println(CronExpression.isValidExpression("0-5 * * * * *"));
// 下面是错误的
System.out.println(CronExpression.isValidExpression("* * * * *"));
System.out.println(CronExpression.isValidExpression("* * * * * * *"));
System.out.println(CronExpression.isValidExpression("60 * * * * *"));
System.out.println(CronExpression.isValidExpression("* 60 * * * *"));
System.out.println(CronExpression.isValidExpression("* * 25 * * *"));
System.out.println(CronExpression.isValidExpression("* * * 32 * *"));
System.out.println(CronExpression.isValidExpression("* * * * 13 *"));
System.out.println(CronExpression.isValidExpression("* * * * * 8"));
}3. 并发执行定时任务
在spring中,默认只使用一个线程来调度任务执行。我们可以通过如下案例演示:
java
@Component
public class ScheduleTask {
@Scheduled(fixedDelay = 1000)
public void run1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " begin ScheduleTask---The time is now " + LocalDateTime.now());
Thread.sleep(5000);
System.out.println(threadName + " end ScheduleTask---The time is now " + LocalDateTime.now());
}
@Scheduled(fixedRate = 2000)
public void run2() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " ScheduleTask2---The time is now " + LocalDateTime.now());
}
}任务一每秒钟执行一次,但是需要耗时5秒。任务二每两秒执行一次,耗时忽略不计。如果是并发执行,那么在任务一执行期间,应该能看到任务二打印的信息。但是结果如下:
txt
scheduling-1 begin ScheduleTask---The time is now 2024-12-23T19:20:11.020438
scheduling-1 end ScheduleTask---The time is now 2024-12-23T19:20:16.024205
scheduling-1 ScheduleTask2---The time is now 2024-12-23T19:20:16.024657
scheduling-1 ScheduleTask2---The time is now 2024-12-23T19:20:16.024811
scheduling-1 ScheduleTask2---The time is now 2024-12-23T19:20:17.017654
----这条线是手动加的,方便观看分析---
scheduling-1 begin ScheduleTask---The time is now 2024-12-23T19:20:17.026388
scheduling-1 end ScheduleTask---The time is now 2024-12-23T19:20:22.028724
scheduling-1 ScheduleTask2---The time is now 2024-12-23T19:20:22.029476
scheduling-1 ScheduleTask2---The time is now 2024-12-23T19:20:22.029620
scheduling-1 ScheduleTask2---The time is now 2024-12-23T19:20:23.015557可以看到在任务一执行期间,任务二并没有执行,反而积压到任务一执行完后才执行。并且,线程的名字相同。
如果我们想让多个任务并行执行,那么我们可以设置调度线程池大小。
方式一 修改配置文件:
yaml
spring:
task:
scheduling:
pool:
size: 5方式二 注册组件:
java
@Configuration
@EnableScheduling // 启用定时任务
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean
public Executor taskExecutor() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 设置线程池大小
scheduler.setThreadNamePrefix("MyScheduled-"); // 设置线程名前缀
return scheduler;
}
}两种方式均可,现在我们通过方式二的方式,再次运行上面的测试程序,结果如下:
txt
MyScheduled-1 ScheduleTask2---The time is now 2024-12-23T19:32:43.199881
MyScheduled-4 begin ScheduleTask---The time is now 2024-12-23T19:32:43.203226
MyScheduled-3 ScheduleTask2---The time is now 2024-12-23T19:32:45.199786
MyScheduled-3 ScheduleTask2---The time is now 2024-12-23T19:32:47.199859
MyScheduled-4 end ScheduleTask---The time is now 2024-12-23T19:32:48.208159
MyScheduled-3 ScheduleTask2---The time is now 2024-12-23T19:32:49.194922
MyScheduled-1 begin ScheduleTask---The time is now 2024-12-23T19:32:49.209076
MyScheduled-5 ScheduleTask2---The time is now 2024-12-23T19:32:51.195891
MyScheduled-5 ScheduleTask2---The time is now 2024-12-23T19:32:53.194892
MyScheduled-1 end ScheduleTask---The time is now 2024-12-23T19:32:54.212709可以看到,在任务一的执行期间,任务二也执行了,并且线程名不同,说明使用了不同的线程。
4. 如何使用代码执行定时任务
我们可以抛弃注解的方式,使用代码的方式来手动指定定时任务,spring提供的ThreadPoolTaskScheduler提供了一些方法。
首先我们在程序主类中设置ThreadPoolTaskScheduler,注意使用代码的方式不开启@EnableScheduling:
java
@SpringBootApplication
//@EnableScheduling
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(5);
threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
return threadPoolTaskScheduler;
}
}在ThreadPoolTaskScheduler中,提供了以schedule起始的相关方法,其入参分别是定时任务和调度模式,例如:
schedule(Runnable task, Date startTime):在某个时间点调度任务执行;schedule(Runnable task, Trigger trigger):通过指定CRON表达式调度任务执行,见CronTrigger实现;scheduleAtFixedRate(Runnable task, long period):如@Scheduled(fixedRate = xxx);scheduleWithFixedDelay(Runnable task, long delay):如@Scheduled(fixedDelay = xxx);
我们可以创建一个控制器Controller,通过调接口的方式手动调度任务执行:
java
@RestController
public class TaskSchedulerController {
@Resource
private TaskScheduler threadPoolTaskScheduler;
@GetMapping("/start")
String start(){
threadPoolTaskScheduler.scheduleAtFixedRate(()->System.out.println(LocalDateTime.now()), 1000);
return "ok";
}
}注意,第5行类型应声明为TaskScheduler。
然后调接口就能看到控制台以每秒中的频率输出时间信息了。
5. 如何关闭和启动定时任务
如果我们通过ThreadPoolTaskScheduler来调度定时任务,那么这些方法会返回ScheduledFuture<?>对象,我们可以使用该对象来关闭定时任务,如下:
java
@RestController
public class TaskSchedulerController {
@Resource
private TaskScheduler threadPoolTaskScheduler;
private ScheduledFuture<?> scheduledFuture;
@GetMapping("/start")
String start(){
if(scheduledFuture == null || scheduledFuture.isCancelled()) {
scheduledFuture = threadPoolTaskScheduler.scheduleAtFixedRate(() -> System.out.println(LocalDateTime.now()), 1000);
}
return "ok";
}
@GetMapping("/stop")
String stop(){
if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
scheduledFuture.cancel(false); // 参数为 false,不会中断正在执行的任务
System.out.println("Task has been canceled.");
}
return "ok";
}
}然后我们就可以通过/start接口启动定时任务,通过/stop接口关闭定时任务了。
6. 定时任务的管理
首先定义一个定时任务抽象类ScheduledTask,其中包括开启定时任务和关闭定时任务的方法,以及抽象的定时任务(待子类实现):
java
/**
* 定时任务抽象类
*/
public abstract class ScheduledTask {
@Resource
private ThreadPoolTaskScheduler taskScheduler;
private ScheduledFuture<?> scheduledFuture;
public void startTask(Date date) {
if(scheduledFuture == null || scheduledFuture.isCancelled() || scheduledFuture.isDone()) {
scheduledFuture = taskScheduler.schedule(this::run, date);
}
}
public void startTaskAtFixedRate(long period) {
if(scheduledFuture == null || scheduledFuture.isCancelled() || scheduledFuture.isDone()) {
scheduledFuture = taskScheduler.scheduleAtFixedRate(this::run, period);
}
}
public void startTaskWithFixedDelay(long delay) {
if(scheduledFuture == null || scheduledFuture.isCancelled() || scheduledFuture.isDone()) {
scheduledFuture = taskScheduler.scheduleWithFixedDelay(this::run, delay);
}
}
public void startTaskWithCron(String cron) {
if(scheduledFuture == null || scheduledFuture.isCancelled() || scheduledFuture.isDone()) {
scheduledFuture = taskScheduler.schedule(this::run, new CronTrigger(cron));
}
}
/**
* 定义实际的定时任务方法
*/
public abstract void run();
/**
* 取消任务
*/
public void cancelTask() {
if (scheduledFuture != null && !scheduledFuture.isCancelled()) {
scheduledFuture.cancel(false); // 参数为 false,不会中断正在执行的任务
System.out.println("Task has been canceled.");
}
}
}然后子类继承抽象类ScheduledTask,定义具体的定时任务:
java
@Component
public class ScheduledTaskManagerOne extends ScheduledTask {
@Override
public void run() {
System.out.println(LocalDateTime.now()+ "-------");
}
}
@Component
public class ScheduledTaskManagerTwo extends ScheduledTask {
@Override
public void run() {
System.out.println("第二个任务开始:" + LocalDateTime.now());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第二个任务结束:"+ LocalDateTime.now());
}
}新增一个定时任务配置类,为每个定时任务取个名字并以Map的形式放入容器:
java
@Configuration
public class ScheduleConfig {
@Resource
private ScheduledTaskManagerOne scheduledTaskManagerOne;
@Resource
private ScheduledTaskManagerTwo scheduledTaskManagerTwo;
@Bean
Map<String, ScheduledTask> scheduledTaskMap(){
return new HashMap<String, ScheduledTask>(){{
put("taskOne", scheduledTaskManagerOne);
put("taskTwo", scheduledTaskManagerTwo);
}};
}
}此处后续可考虑以反射的方式自动发现定时任务类,不用手动声明。
然后我们即可定义接口来动态管理定时任务的开启和关闭了:
java
@RestController
public class ScheduleController {
@Resource
private Map<String, ScheduledTask> scheduledTaskMap;
@GetMapping("/startAnother")
public String start(@Valid StartScheduledTaskRequest startScheduledTaskRequest){
ScheduledTask scheduledTaskManager = scheduledTaskMap.get(startScheduledTaskRequest.getName());
if(scheduledTaskManager == null)
return "fail";
switch (startScheduledTaskRequest.getMode()) {
case 1:
scheduledTaskManager.startTask(startScheduledTaskRequest.getDate());
break;
case 2:
scheduledTaskManager.startTaskAtFixedRate(startScheduledTaskRequest.getPeriod());
break;
case 3:
scheduledTaskManager.startTaskWithFixedDelay(startScheduledTaskRequest.getDelay());
break;
case 4:
scheduledTaskManager.startTaskWithCron(startScheduledTaskRequest.getCron());
break;
}
return "success";
}
@GetMapping("/stopAnother")
public String stop(@RequestParam("name") String name){
ScheduledTask scheduledTaskManager = scheduledTaskMap.get(name);
if(scheduledTaskManager == null)
return "fail";
scheduledTaskManager.cancelTask();
return "success";
}
}java
@Data
public class StartScheduledTaskRequest {
@NotNull(message = "任务名为空!")
private String name;
@Min(value = 1, message = "调度模式应为 1-定时,2-fixedRate,3-delay,4-cron")
@Max(value = 4, message = "调度模式应为 1-定时,2-fixedRate,3-delay,4-cron")
@NotNull(message = "调度模式应为 1-定时,2-fixedRate,3-delay,4-cron")
private Integer mode;
private Long period;
private Long delay;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date date;
private String cron;
}