头图

关注公众号 不爱总结的麦穗 将不定期推送技术好文

最近在做一个项目,需要用到动态定时任务,现在比较普遍的做法是集成第三方框架(例如Quartz、XXL-JOB),我自己在做这个项目的时候也考虑过去集成Quartz实现,但是基于项目本身的复杂度和使用场景放弃了

本文主要分享在不依赖过多的其他框架,使用springBoot自身带有的定时任务框架来实现动态定时任务

注解实现定时任务

具体实现

主要基于@EnableScheduling@Scheduled注解

  • 主启动类上加上 @EnableScheduling 注解
  • 写一个类,注入到容器中,在方法上加上 @Scheduled 注解
@Slf4j
@Component
public class TimeTask {
    
    @Scheduled(cron = "0 0/31 * * * ?")
    public void refresh(){
        log.info("//// 定时刷新");
        
      //业务代码
    }

}

这样就实现了定时任务,是不是很简单?不难发现,这种方式定时执行时间是固定的,但是大部分业务的定时执行时间是经常在变化的,这时候我们就需要通过动态定时任务实现

实现动态定时任务

Spring实现动态定时任务的核心就是其提供的任务调度类ThreadPoolTaskSchedulerThreadPoolTaskScheduler基于线程池来执行任务,可以按照固定的时间间隔或者指定的Cron表达式来调度任务的执行。

image.png

在我的项目中主要用到了ScheduledFuture<?> schedule(Runnable task, Trigger trigger)方法,指定的Cron表达式来调度任务的执行

具体实现

  • 创建ThreadPoolTaskScheduler配置类

    @Configuration
    public class SchedulingConfig {
    
      @Bean
      public TaskScheduler taskScheduler() {
          // 获取系统处理器个数, 作为线程池数量
          int corePoolSize = Runtime.getRuntime().availableProcessors();
          ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
          // 定时任务执行线程池核心线程数
          taskScheduler.setPoolSize(corePoolSize);
          taskScheduler.setRemoveOnCancelPolicy(true);
          taskScheduler.setThreadNamePrefix("AntiFraudSchedulerThreadPool-");
          return taskScheduler;
      }
    
    }
  • 创建ScheduledTask包装类
    ScheduledFutureScheduledFuture<?> schedule(Runnable task, Trigger trigger)方法的返回值。

ScheduledFuture继承了Future接口,Future接口提供一组辅助方法,比如:

  • cancel():取消任务
  • isCancelled():任务是不是取消了
  • isDone():任务是不是已经完成了
  • get():用来获取执行结果

当我们调用cancel方法时,会将我们的任务从workQueue中移除

public final class ScheduledTask {

    public volatile ScheduledFuture<?> future;

    /**
     * 取消定时任务
     */
    public void cancel() {
        ScheduledFuture<?> scheduledFuture = this.future;
        if (Objects.nonNull(scheduledFuture)) {
            scheduledFuture.cancel(true);
        }
    }
}
  • 创建CronTaskRegistrar

实现了DisposableBean接口的类,用于注册定时任务。它具有添加、删除和调度定时任务的方法。在销毁时,会取消所有定时任务。

@Slf4j
@Component
@SuppressWarnings("all")
public class CronTaskRegistrar implements DisposableBean {

    @Resource
    private TaskScheduler taskScheduler;

    // 保存任务Id和定时任务
    private final Map<String, ScheduledTask> scheduledTaskMap = new ConcurrentHashMap<>(64);

    // 添加任务
    public void addTask(Runnable task, String cronExpression,String jobId) {
        addTask(new CronTask(task, cronExpression),jobId);
    }

    public void addTask(CronTask cronTask,String jobId) {
        if (Objects.nonNull(cronTask)) {
            Runnable task = cronTask.getRunnable();
            if (this.scheduledTaskMap.containsKey(task)) {
                removeTask(jobId);
            }
            // 保存任务Id和定时任务
            this.scheduledTaskMap.put(jobId, scheduleTask(cronTask));
        }
    }

    // 通过任务Id,取消定时任务
    public void removeTask(String jobId) {
        ScheduledTask scheduledTask = this.scheduledTaskMap.remove(jobId);
        if (Objects.nonNull(scheduledTask)) {
            scheduledTask.cancel();
        }
    }

    public ScheduledTask scheduleTask(CronTask cronTask) {
        ScheduledTask scheduledTask = new ScheduledTask();
        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
        return scheduledTask;
    }

    // 销毁
    @Override
    public void destroy() {
        this.scheduledTaskMap.values().forEach(ScheduledTask::cancel);
        this.scheduledTaskMap.clear();
    }
}

动态定时任务的核心逻辑到这基本就已经完成了,具体的Service类代码,这里就不贴出来了,因为里面基本上都是业务逻辑和CronTaskRegistrar类的编排(ps:需要的也可以私聊demo)

  • 定时任务表设计

    create table schedule_setting
    (
      id               varchar(32)  not null comment '唯一id'
          primary key,
      job_id           varchar(64)  null comment '任务ID',
      cron_expression  varchar(255) null comment 'cron表达式',
      job_result       varchar(32)  null comment '任务结果(通过 复议 拒绝)',
      create_date      datetime     null comment '创建时间',
      status           varchar(4)   null,
      create_by        varchar(64)  null comment '创建人账号',
      creator          varchar(64)  null comment '创建人',
      version          bigint(19)   null comment '版本号'
    )
      comment '定时任务表';
  • 项目启动时加载所有任务

    @Component
    @RequiredArgsConstructor
    @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
    public class ScheduleRunner implements CommandLineRunner {
    
    
      ScheduleSettingRepository scheduleSettingRepository;
    
      CronTaskRegistrar scheduledTaskRegistrar;
    
      @Override
      public void run(String... args) throws Exception {
          // 查询所有定时任务
          List<ScheduleSetting> scheduleSettingList=scheduleSettingRepository.findByStatus(EnableFlag.Y.name());
          for (ScheduleSetting scheduleSetting : scheduleSettingList) {
              //调用CronTaskRegistrar添加任务方法
              scheduledTaskRegistrar.addCronTask(() -> {
                  ···
                  任务执行方法
                  ...
              }, scheduleSetting.getCronExpression(), scheduleSetting.getJobId());
          }
    
      }
    }

    至此,动态定时任务就说完啦

GitHup demo dynamic-timing-task


不爱总结的麦穗
1 声望0 粉丝