持续创作,加速成长!这是我参与「掘金日新计划 · 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 吧!