SpringBoot实现固定、动态定时任务和延迟任务

目录
  1. 1. 一、注解实现定时任务
  2. 2. 二、动态定时任务
    1. 2.1. 2.1、建数据表
    2. 2.2. 2.2、导入依赖,基础编码
    3. 2.3. 2.3、主要实现代码
    4. 2.4. 2.4、效果
  3. 3. 三、实现延迟任务
    1. 3.1. 具体实现
    2. 3.2. 第一步
    3. 3.3. 第二步
    4. 3.4. 第三步
    5. 3.5. 测试

背景

最近要用到这个定时任务,之前就简单使用注解的那种方式,需求一变化,就得重新修改。就想到了动态定时任务,连接数据库来动态选择,这样确实解决了问题。但是仍然有一个缺陷,就是没法设置任务的执行时间,比如超过半小时未付款,把订单状态改为取消。

前言

阅读完本文:

  1. 知晓 SpringBoot 用注解如何实现定时任务

  2. 明白 SpringBoot 如何实现一个动态定时任务 (与数据库相关联实现)

  3. 理解 SpringBoot 实现设置时间执行定时任务  (使用 ThreadPoolTaskScheduler实现)

一、注解实现定时任务

用注解实现是真的简单,只要会 cron 表达式就行。

第一步: 主启动类上加上 @EnableScheduling 注解

1
2
3
4
5
6
7
8
@EnableScheduling
@SpringBootApplication
public class SpringBootScheduled {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootScheduled.class);
    }
}

第二步:写一个类,注入到Spring,关键就是 @Scheduled 注解。 () 里就是 cron 表达式,用来说明这个方法的执行周期的。

我常常也记不住,通常是在线生成的: Cron 表达式在线生成

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
/**
 * 定时任务 静态定时任务
 *
 * 第一位,表示秒,取值0-59
 * 第二位,表示分,取值0-59
 * 第三位,表示小时,取值0-23
 * 第四位,日期天/日,取值1-31
 * 第五位,日期月份,取值1-12
 * 第六位,星期,取值1-7,1表示星期天,2表示星期一
 * 第七位,年份,可以留空,取值1970-2099
 * @author crush
 * @since 1.0.0
 * @Date: 2021-07-27 21:13
 */
@Component
public class SchedulingTaskBasic {

    /**
     * 每五秒执行一次
     */
    @Scheduled(cron = "*/5 * * * * ?")
    private void printNowDate() {
        long nowDateTime = System.currentTimeMillis();
        System.out.println("固定定时任务执行:--->"+nowDateTime+",此任务为每五秒执行一次");
    }
}

执行效果:

源码:springboot-scheduled

@Scheduled除去cron还有三种方式:fixedRate,fixedDelay,initialDelay

cron: 表达式可以定制化执行任务,但是执行的方式是与fixedDelay相近的,也是会按照上一次方法结束时间开始算起。

fixedDelay: 控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次。

1
2
3
4
5
6
7
8
9
@Configuration
@EnableScheduling //开启定时任务
public class ScheduleTask1 {
//每3秒执行一次
@Scheduled(fixedDelay = 3000)
private void myTasks() {
System.out.println("I do myself per third seconds");
}
}

fixedRate: 是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行。

1
2
3
4
5
6
7
8
9
@Component
@EnableScheduling //开启定时任务
public class ScheduleTask2 {
//每10秒执行一次
@Scheduled(fixedRate = 10000)
private void myTasks2() {
System.out.println("我是一个定时任务");
}
}

initialDelay: initialDelay = 10000 表示在容器启动后,延迟10秒后再执行一次定时器。

1
2
3
4
5
6
7
8
9
@Component
@EnableScheduling //开启定时任务
public class ScheduleTask {
//容器启动后,延迟10秒后再执行一次定时器,以后每10秒再执行一次该定时器。
@Scheduled(initialDelay = 10000, fixedRate = 10000)
private void myTasks3() {
System.out.println("我是一个定时任务3");
}
}

二、动态定时任务

其实也非常的简单。

2.1、建数据表

第一步:建个数据库表。

1
2
3
4
5
6
7
8
CREATE TABLE `tb_cron`  (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '动态定时任务时间表',
`cron_expression` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '定时任务表达式',
`cron_describe` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `tb_cron` VALUES (1, '0 0/1 * * * ?', '每分钟执行一次');

2.2、导入依赖,基础编码

实体类:

1
2
3
4
5
6
7
@Data
@TableName("tb_cron")
public class Cron {
    private Long id;
    private String cronExpression;
    private String cronDescribe;
}

mapper层:

1
2
3
4
5
@Repository
public interface CronMapper extends BaseMapper<Cron> {
    @Select("select cron_expression from tb_cron where id=1")
    String getCron1();
}

2.3、主要实现代码

写一个类 实现 SchedulingConfigurer

实现 void configureTasks(ScheduledTaskRegistrar taskRegistrar); 方法,此方法的作用就是根据给定的 ScheduledTaskRegistrar 注册 TaskScheduler 和特定的Task实例

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
28
29
@Component
public class CompleteScheduleConfig implements SchedulingConfigurer {

    @Autowired
    @SuppressWarnings("all")
    CronMapper cronMapper;

    /**
     * 执行定时任务.
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(
                //1.添加任务内容(Runnable)
                () -> System.out.println("执行动态定时任务1: " + LocalDateTime.now().toLocalTime()+",此任务执行周期由数据库中的cron表达式决定"),
                //2.设置执行周期(Trigger)
                triggerContext -> {
                    //2.1 从数据库获取执行周期
                    String cron = cronMapper.getCron1();
                    //2.2 合法性校验.
                    if (cron!=null) {
                        // Omitted Code ..
                    }
                    //2.3 返回执行周期(Date)
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                }
        );
    }
}

2.4、效果

注意:当你修改了任务执行周期后,生效时间为执行完最近一次任务后。这一点是需要注意的,用生活中的例子理解就是我们取消电话卡的套餐也要下个月生效,含义是一样的。

源码:springboot-scheduled

三、实现延迟任务

ThreadPoolTaskScheduler 这个类是在 org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler 这个包中。

ThreadPoolTaskScheduler 是 spring taskSchedule 接口的实现,可以用来做定时任务使用。

ThreadPoolTaskScheduler 四个版本定时任务方法:

  • schedule(Runnable task, Date stateTime),在指定时间执行一次定时任务(实现一次性延迟任务)
  • schedule(Runnable task, Trigger trigger),动态创建指定表达式cron的定时任务
  • scheduleAtFixedRate,指定间隔时间执行一次任务,间隔时间为前一次执行开始到下次任务开始时间
  • scheduleWithFixedDelay,指定间隔时间执行一次任务,间隔时间为前一次任务完成到下一次开始时间

具体实现

我们需要向 spring 容器中注入一个 ThreadPoolTaskScheduler 的 bean,用于调度定时任务,以及需要增加一个缓存用于存入当前执行任务的 scheduleFuture 对象,将 ScheduledFuture 对象缓存的原因是在于,为了方面于停止对应的任务。

我们点击 ThreadPoolTaskScheduler 类,看具体的源码的时候会发现有这么一段断码:


这里是表示初始化的线程池的大小为 1,也就是说线程调度器设置只有一个线程容量,如果存在多个任务被触发时,会等第一个任务执行完毕才会执行下一个任务。所以这里还是需要自己去定义线程的大小,避免因为默认的线程池导致出现奇奇怪怪的问题。

第一步

创建 ThreadPoolTaskScheduler Bean 以及 用于存放定时任务的 map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class ScheduleConfig {

// 用来存入线程执行情况, 方便于停止定时任务时使用
public static ConcurrentHashMap<String, ScheduledFuture> taskList= new ConcurrentHashMap<String, ScheduledFuture>();

@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler(){
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(10); // 线程池大小
threadPoolTaskScheduler.setThreadNamePrefix("taskExecutor-"); // 线程名称
threadPoolTaskScheduler.setAwaitTerminationSeconds(60); // 等待时长
threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true); // 调度器shutdown被调用时等待当前被调度的任务完成
return threadPoolTaskScheduler;
}
}

第二步

增加用于外部访问的接口 controller,等程序启动完成之后,我们需要调用对应的方法进行访问测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class TestController {

@Autowired
private DynamicTask task;

@RequestMapping("start")
public void startTask() {
com.demo.task.startCron();
}

// 测试访问: http://localhost:8080/stopById?taskId=任务一
@RequestMapping("stopById")
public void stopById(String taskId) {
com.demo.task.stop(taskId);
}

@RequestMapping("stopAll")
public void stopAll() {
com.demo.task.stopAll();
}
}

第三步

核心逻辑

在这里当在执行 threadPoolTaskScheduler.schedule() 时,会传入一个自定义的 com.demo.task,以及一个 trigger。
调用完成之后会返回一个 scheduledFuture,这个就是当前的任务调度器,停止的时候需要找到这个调度器,用这个调用器来终止。

  • threadPoolTaskScheduler.schedule(com.demo.task, cron) 用来调度任务
  • boolean cancelled = scheduledFuture.isCancelled(); 用来判断是否已经取消
  • scheduledFuture.cancel(true) 用来将当前的任务取消

下面是核心代码逻辑:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Component
public class DynamicTask {
private final static Logger logger = LoggerFactory.getLogger(DynamicTask.class);

@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler; // 注入线程池任务调度类

public void startCron(){

getTasks().forEach(customizeTask -> {
// 开始执行调度(CronTrigger是定时任务)
ScheduledFuture scheduledFuture =
threadPoolTaskScheduler.schedule(customizeTask, new CronTrigger(customizeTask.getCron()));
// 将 scheduledFuture 保存下来用于停止任务使用
ScheduleConfig.taskList.put(customizeTask.getName(), scheduledFuture);
});
}

public void stop(String taskId) {
if (ScheduleConfig.taskList.isEmpty()) return;
if (ScheduleConfig.taskList.get(taskId) == null) return;

ScheduledFuture scheduledFuture = ScheduleConfig.taskList.get(taskId);

if (scheduledFuture != null) {
scheduledFuture.cancel(true); // 这里需要使用指定的 scheduledFuture 来停止当前的线程
ScheduleConfig.taskList.remove(taskId); // 移除缓存
}
}

public void stopAll(){
if (ScheduleConfig.taskList.isEmpty()) return;
ScheduleConfig.taskList.values().forEach(scheduledFuture -> scheduledFuture.cancel(true) );
}

private List<CustomizeTask> getTasks(){
return Arrays.asList(
new CustomizeTask("任务一", "0/2 * * * * ?"),
new CustomizeTask("任务二", "0/3 * * * * ?")
);
}

// 自定义任务,这里用来对任务进行封装
private class CustomizeTask implements Runnable {
private String name; // 任务名字
private String cron; // 触发条件

CustomizeTask(String name, String cron) {
this.name = name;
this.cron = cron;
}

public String getCron(){
return this.cron;
}

public String getName(){
return this.name;
}

@Override
public void run() {
//schedule会定时执行run方法中的内容
logger.info("当前任务名称:{}", name );
}
}
}

对于上面的任务类 CustomizeTask 可以自定义进行封装,可以增加成员属性调用的 服务方法类 以及 调用参数,在 run 方法上面就可以去调用对应的服务方法了,详见:SpringBoot动态定时任务(完整版)

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
28
private class CustomizeTask implements Runnable {
private String name; // 任务名字
private String cron; // 触发条件
private String data; // 传输的数据参数
private String method; // 需要调用的方法

CustomizeTask(String name, String cron, String data, String method) {
this.name = name;
this.cron = cron;
this.data = data;
this.method = method;
}

public String getCron(){
return this.cron;
}

public String getName(){
return this.name;
}

@Override
public void run() {
// 通过反射获取到调用的方法,传入调用的参数 data
logger.info("当前任务名称:{}", name );
}
}

测试

当源码搭建完成之后,启动服务,我们测试下,首先我们先访问下启动任务

1
http://localhost:8080/start

这里可以看到控制台交替输出任务的名称


接着,我们再来测试下停止其中的某个服务,当访问下面的链接之后,会发现任务一就已经被停止了

1
http://localhost:8080/stopById?taskId=任务一


最后,我们访问下停止所有的任务:

1
http://localhost:8080/stopAll

就会发现控制台没有任务在执行了。

摘自:SpringBoot实现固定、动态定时任务的三种实现方式