SpringBoot+SchedulingConfigurer实现动态定时任务

2,869 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情


哈喽,大家好,我是一条。

上一节我们用实现 SpringBoot + @Scheduled 实现了定时任务。但是也存在很多问题:

  • 在一个线程内执行,那么任务多了就可能被阻塞,导致任务延迟执行。
  • 每次修改执行频率都要改代码,重启服务。
  • 无法提供定时任务的启停、修改接口。

所以本篇文章就聊聊怎么解决上述问题,实现一个类似 Quartz 的动态定时任务功能。

话不多说,开干。

SchedulingConfigurer 接口

相信WebMvcConfigurer这个接口大家都用过,在MVC里面,可以通过实现该类注册拦截器、转换器包括跨域等等。

类似的,通过实现SchedulingConfigurer我们也可以实现动态的添加定时任务。

@Component
@Slf4j
public class ReportSchedulerManager implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        
    }
}

任务配置表

既然是动态配置,我们就需要一张配置表,有同学可能会说写在配置文件里,一方面配置文件的热更新不稳定,另一方面不能做到通过接口修改。

所以我们在数据库建一张表:

create table t_alert_time
(
    id          varchar(10)  not null comment 'key'primary key,
    
    alert_cron  varchar(32)  not null comment 'corn表达式'
)comment '提报提醒时间表';

先不靠路多余的字段,只是简单的实现定时任务配置。

相关Mapper方法

建完表就需要一个方法查出全部数据,不啰嗦。我习惯上使用mybatis-plus。

这段是直从项目里摘取的,需要根据你的实际需求做修改。

    public List<AlertTimePO> getAlertTimeList(String month) {
        LambdaQueryWrapper<AlertTimePO> queryWrapper = Wrappers.lambdaQuery(AlertTimePO.class);
        if (StringUtils.isNotBlank(month)) {
            queryWrapper.eq(AlertTimePO::getId, month);
        }
        return alertTimeMapper.selectList(queryWrapper);
    }

项目启动添加全部任务

这里要提前说一下 @EnableScheduling这个注解,类似的还有 @EnableFeignClients@EnableAsync这种,都是开启某种功能。

这些@Enable*注解的源码可以看出,所有注解里面都有一个@Import注解,而@Import是用来导入配置类的,所以@Enable*自动开启的实现原理其实就是导入了一些自动配置的Bean。即在容器一启动就将这些bean实例化。

所以,我们就可以通过SchedulingConfigurer添加任务了。

    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
      // 配置线程池
        scheduledTaskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
        this.registrar = scheduledTaskRegistrar;
      alertTimeService.getAlertTimeList(StringUtils.EMPTY).forEach(this::addTask);
    }
​
// 核心代码
    public void addAlertTask(AlertTimePO alert) {
      // 将任务实现 Runnable 接口,并传入 cron 表达式
        CronTask cronTask = new CronTask(() -> storeOperatorService.getOperatorList()
                .forEach(
                        larkId -> sendReportCardMsg(larkId, 0, buildContent(alert.getId()))
                ), alert.getAlertCron());
      // 添加任务到线程池
        ScheduledFuture<?> future = Objects.requireNonNull(registrar.getScheduler()).schedule(cronTask.getRunnable(), cronTask.getTrigger());
        assert future != null;
        log.info("添加提报提醒定时任务 - 提报月:{} Cron:{}",alert.getId(),alert.getAlertCron());
    }

通过接口添加

这个就简单了,我们只需要补全service 和 Controller 就好。这里简单举个例子。

通过接口新增一条配置,同时添加定时任务,不需要重启服务

@Override
    public void addReportTime(ReportTimeDTO reportTimeDTO) {
      // 通过接口新增一条配置,同时添加定时任务,不需要重启服务
        validateReportTimeDTO(reportTimeDTO);
        AlertTimePO alertTimePO = buildAlertTimePO(reportTimeDTO);
        alertTimeMapper.insert(alertTimePO);
        schedulerManager.addTask(alertTimePO);
    }
    @PostMapping("/add")
    public Result addReportTime(@RequestBody ReportTimeDTO reportTimeDTO){
        alertTimeService.addReportTime(reportTimeDTO);
        return Result.ok();
    }

取消任务

修改任务也可以通过取消任务再添加实现。

首先需要一个map来存储任务和ScheduledFuture。

private final ConcurrentHashMap<String, ScheduledFuture<?>> schedulerFutureMap = new ConcurrentHashMap<>();

每次新增任务时需要加入到map里:

schedulerFutureMap.put(ALERT_TASK_KEY+alert.getId(),future);

取消任务

    public void cancelTestTask(String key) {
      // false 代表如果正在执行,不强制取消
        schedulerFutureMap.get(key).cancel(false);
        schedulerFutureMap.remove(key);
        log.info("移除定时任务:{}",key);
    }

总结

综上,我们就实现了一个类似 Quartz的比较灵活的定时任务框架,不过这里还有个坑,就是cron表达式不支持指定某年,也是很烦了,复杂的需求,还是直接上 Quartz 吧!