0%

Spring Boot 实现动态定时任务

最近在项目中碰到一些问题,一个定时任务在运行过程中崩溃,导致定时任务没有执行完成,业务那边想能够在不重启任务的情况下,重启任务继续跑,而且希望在不重启服务的情况下,能够动态的修改定时任务时间。所以有了这一篇文章。

一般情况下,在Spring Boot 项目中,想使用定时任务,只需要使用 @EnableScheduling 注解开启定时任务即可,然后在定时任务调度的任务上,添加@Scheduled,修改自己需要的任务周期。

普通定时任务

下面是一般情况下,在开启@EnableScheduling注解情况下,一个简单的示例:

1
2
3
4
5
6
7
8
9
10
@Slf4j
@Component
public class SampleJob {

@Scheduled(cron = "* * * * * ?")
public void printLog() {
long time = System.currentTimeMillis();
log.info("当前时间:{}", time);
}
}

动态修改定时任务

方法一:仅修改任务周期

实现 SchedulingConfigurer 方法,重写 configureTasks 方法,重新制定 Trigger。下面上代码。

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
@Slf4j
@Component
public class DynamicCronJob implements SchedulingConfigurer {

/**
* 默认定时任务执行时间
*/
private String taskCron = GlobalConstants.TASK_DEFAULT_CRON;

@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.addTriggerTask(() -> {
long time = System.currentTimeMillis();
log.info("执行任务,当前时间:{}", time);
}, triggerContext -> {
// 刷新cron时间
CronTrigger cronTrigger = new CronTrigger(taskCron);
Date nextExecuteTime = cronTrigger.nextExecutionTime(triggerContext);
return nextExecuteTime;
});
}

/**
* 修改默认的定时任务时间
* */
public void setTaskCron(String taskCron) {
this.taskCron = taskCron;
}
}

这里的核心方法是 scheduledTaskRegistrar.addTriggerTask,它只接收两个参数,分别是调度任务实例(Runable实例),Trigger实例。如果想修改定时任务的时间,其实修改的就是这里的nextExecutionTime,返回下次执行时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@RestController
@RequestMapping(value = "/dynamic/job")
public class DynamicJobController {

@Autowired
private DynamicCronJob dynamicCronJob;
@Autowired
private DynamicTimedTask dynamicTimedTask;

@GetMapping(value = "/execute")
public NewResponseModel<?> execute(@RequestParam(value = "cron", defaultValue = "0/10 * * * * ?") String cron) {
log.info("修改任务执行时间,cron={}", cron);
dynamicCronJob.setTaskCron(cron);
return NewResponseModel.Success();
}

这里我们的一个测试类,是通过请求,修改定时任务时间。

下面是测试结果,一开始默认每个5秒打印一次任务,然后将任务周期改为10秒。

缺陷

这种方法存在一种缺陷,就是修改周期后,需要下一次执行后才能生效,比如一开始的定时任务是每隔5分钟执行一次,但是现在你想修改执行频率为10秒执行一次,修改后的执行频率,并不会马上生效,需要在最近一次执行后,才会生效。下面是我做的一个测试。我们可以看到,修改后的,是在下一次执行后才生效。

方法二:动态提交任务并修改任务执行周期

使用 ThreadPoolTaskScheduler 可以实现动态添加删除功能,实现动态修改任务执行周期。

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
@Slf4j
@Component
public class DynamicTimedTask {

/**表示是否存在上一次任务*/
private static boolean start = false;

/**
* 接受任务的返回结果
*/
private ScheduledFuture<?> future;

@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;

/**
* 启动定时任务
*
* @param task 执行任务
* @param cron 执行频率
* @return
*/
public boolean startCron(Runnable task, String cron) {
// 停止上一次定时任务
stopCron();
//从数据库动态获取执行周期
future = threadPoolTaskScheduler.schedule(task, new CronTrigger(cron));
if (future != null) {
log.info("start cron task success");
start = true;
return true;
}
return false;
}

/**
* 停止定时任务
*
* @return
*/
public boolean stopCron() {
if (!start) {
log.info("没有上一次任务");
return false;
}
boolean cancel = future.cancel(true);
if (cancel) {
log.info("start cron task success");
start = false;
return true;
}
return false;
}
}

这个类主要通过 startCron 提交了一个新的任务。在开始任务之前,先停止执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@EnableAsync
@EnableScheduling
@EnableJpaAuditing
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class SimpleSampleApplication {

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


@Bean
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-test-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
}

在启动类里,主要是开启 @EnableScheduling 定时任务,和自定义 ThreadPoolTaskScheduler

编写测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@RestController
@RequestMapping(value = "/dynamic/job")
public class DynamicJobController {

@Autowired
private DynamicCronJob dynamicCronJob;
@Autowired
private DynamicTimedTask dynamicTimedTask;

@GetMapping(value = "/execute/task")
public NewResponseModel<?> executeTask(@RequestParam(value = "cron", defaultValue = "0/10 * * * * ?") String cron) {
log.info("修改执行任务,并执行时间,cron={}", cron);
dynamicTimedTask.startCron(() -> {
log.info("模拟执行任务,cron={}", cron);
},
cron);
return NewResponseModel.Success();
}
}

测试结果:

进一步优化,比如我们把所有的任务,存储到 ConcurrentHashMap<String, ScheduledFuture>,我们只需要调用对应的key,然后通过map去除对应的定时任务,取消任务。

这种方法,并不存在方法一所存在的问题。在修改cron后,在最近的一次就立刻生效了。

从上图中可以看到,一开始是每个10秒一次执行,然后修改成每个3秒执行一次,cron 直接生效了。

客官,赏一杯coffee嘛~~~~