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