小言_互联网的博客

Springboot定时调度任务动态管理

383人阅读  评论(0)

前言

        现在智能手表、手环,相信很多人都使用过,其中有一个功能,就是会有各种的提醒,如喝水提醒、运动提醒、远眺提醒,本质上根据用户的设置,间隔一定时间执行一个调度任务,提醒用户做某件事情。这篇文章将以这样的场景为便,和大家分享一下如何使用SprIngboot对定时调度任务进行动态管理。

1. 表结构设计

        创建提醒任务数据表(remind_task ),用来存储提醒任务的基本信息,如任务的名称、cron表达式、任务状态、提醒任务的实现类。


  
  1. create table if not exists remind_task
  2. (
  3. id int auto_increment,
  4. task_name varchar( 30) null comment '任务名称',
  5. cron varchar( 100) null comment 'cron表达式',
  6. task_status int null comment '任务状态,1、启动;0、停止;',
  7. bean_clazz varchar( 255) null comment '提醒任务的实现类',
  8. constraint remind_task_id_uindex
  9. unique (id)
  10. )
  11. comment '提醒任务数据表';
  12. alter table remind_task add primary key (id);
  13. INSERT INTO happy_home.remind_task (id, task_name, cron, task_status, bean_clazz)
  14. VALUES ( 1, '喝水提醒', '0/5 * * * * ?', 1, 'com.fanfu.task.remind.task.DrinkTask');
  15. INSERT INTO happy_home.remind_task (id, task_name, cron, task_status, bean_clazz)
  16. VALUES ( 2, '运动提醒', '0/5 * * * * ?', 1, 'com.fanfu.task.remind.task.SportTask');
  17. INSERT INTO happy_home.remind_task (id, task_name, cron, task_status, bean_clazz)
  18. VALUES ( 3, '吃饭提醒', '0/5 * * * * ?', 0, 'com.fanfu.task.remind.task.EatTask');

2. 实现方法

2.1 调度任务的抽象封装

        面向接口编程,把提醒任务抽象封装为一个接口RemindTask,定义execute()方法;


  
  1. public interface RemindTask {
  2. /**
  3. * 提醒任务执行逻辑
  4. */
  5. void execute();
  6. }

具体的提醒任务,如吃饭、喝水、运动,则实现RemindTask,在execute()方法中编写具体的提醒业务逻辑


  
  1. public class DrinkTask implements RemindTask {
  2. @Override
  3. public void execute( ) {
  4. System. out. println( "[喝水提醒]主人,记得喝水哦"+ LocalDateTime. now());
  5. }
  6. }

  
  1. public class SportTask implements RemindTask {
  2. @Override
  3. public void execute( ) {
  4. System. out. println( "[运动提醒]主人,站起来活动一下吧"+ LocalDateTime. now());
  5. }
  6. }

  
  1. public class EatTask implements RemindTask {
  2. @Override
  3. public void execute( ) {
  4. System. out. println( "[吃饭提醒]主人,吃点东西吧"+ LocalDateTime. now());
  5. }
  6. }

2.2 调度任务的注册

        完成调度任务的抽象封装后,就需要对这些提醒调度任务进行注册,具体的方法也很简单,在前面的文章中也已经分享过,主要逻辑是实现org.springframework.scheduling.annotation.SchedulingConfigurer接口,重写com.fanfu.task.DynamicScheduleTask#configureTasks方法,在重写方法内完成提醒任务的注册;重写方法的具体业务逻辑是:

  1. 从提醒任务数据表(remind_task)中查询出任务状态为启动的任务列表信息;
  2. 遍历提醒任务列表 ,根据提醒任务信息中提醒任务具体实现类、cron表达式构建一个触发器任务并进行注册
  3. 注册调度任务成功后,把返回结果进行缓存;
  4. 把Spring的调度任务注册管理中心注入到本类的属性中,方便后续使用它对调度任务进行动态管理

其中configureTasks()方法会在Spring容器启动完成时被调用,因此在项目启动后会完成调度任务的初次注册并开始执行。


  
  1. @Configuration
  2. public class DynamicScheduleTask implements SchedulingConfigurer {
  3. @Autowired
  4. private RemindTaskDao remindTaskDao;
  5. private ScheduledTaskRegistrar scheduledTaskRegistrar;
  6. //用于存储已注册过的调度任务
  7. private Map< String, ScheduledTask> triggerTaskMap = new ConcurrentHashMap<>();
  8. /**
  9. * spring容器启动完成时会执行这个方法,完成初次的调度任务注册
  10. * @param scheduledTaskRegistrar
  11. */
  12. @Override
  13. public void configureTasks( ScheduledTaskRegistrar scheduledTaskRegistrar) {
  14. //查询出任务状态为启动状态的调度任务
  15. List< RemindTaskBean> remindTaskBeans = remindTaskDao. queryAll( null);
  16. for ( RemindTaskBean remindTaskBean : remindTaskBeans) {
  17. //定义一个触发器类类型的调度任务
  18. TriggerTask triggerTask = new TriggerTask( new Runnable() {
  19. @SneakyThrows
  20. @Override
  21. public void run( ) {
  22. String beanClazz = remindTaskBean. getBeanClazz();
  23. Class<?> aClass = Class. forName(beanClazz);
  24. Method execute = aClass. getMethod( "execute");
  25. execute. invoke(aClass. newInstance(), null);
  26. }
  27. }, new CronTrigger(remindTaskBean. getCron()));
  28. //注册并立即开始调度任务
  29. ScheduledTask scheduledTask = scheduledTaskRegistrar. scheduleTriggerTask(triggerTask);
  30. //注册过的调度任务存储在自定义的容器里,方便后续对注册的调度任务进行动态管理
  31. triggerTaskMap. put(remindTaskBean. getTaskName(), scheduledTask);
  32. }
  33. //把Spring的调度任务注册管理中心注入到本类的属性中,方便后续使用它对调度任务进行动态管理
  34. this. scheduledTaskRegistrar = scheduledTaskRegistrar;
  35. }
  36. }

2.3 调度任务的动态管理        

        这里我通过定义三个接口(任务列表、启动接口、停止接口)来分享一下如何对调度任务进行动态管理,在实际开发中,可以做成一个单独的管理页面,前端页面通过调用后台接口完成调度任务的启动、停止、编辑操作。

在SchedulingConfigurer接口的实现类DynamicScheduleTask中定义三个方法,1、查询调度任务列表 ;2、启动调度任务;3、停止调度任务


  
  1. /**
  2. * 调度任务列表
  3. * @return
  4. */
  5. public List< RemindTaskBean> taskList( ){
  6. List< RemindTaskBean> remindTaskBeans = this. remindTaskDao. queryAll( null);
  7. return remindTaskBeans;
  8. }

  
  1. /**
  2. * 启动调度任务
  3. * @param taskName
  4. */
  5. public synchronized void addTask (String taskName) {
  6. //启动任务前,需要先判断启动任务的状态,如果是已启动,就不能重复启动,抛出异常提示
  7. RemindTaskBean remindTaskBean = this.remindTaskDao.queryByTaskName(taskName);
  8. if ( 1== remindTaskBean.getTaskStatus()) {
  9. throw new RuntimeException( "调度任务已启动,不能重复添加已启动的任务");
  10. }
  11. //如果待启动的调度任务处于停止状态,则定义一个触发器任务
  12. TriggerTask triggerTask = new TriggerTask( new Runnable() {
  13. @SneakyThrows
  14. @Override
  15. public void run () {
  16. String beanClazz = remindTaskBean.getBeanClazz();
  17. Class<?> aClass = Class.forName(beanClazz);
  18. Method execute = aClass.getMethod( "execute");
  19. execute.invoke(aClass.newInstance(), null);
  20. }
  21. }, new CronTrigger(remindTaskBean.getCron()));
  22. //注册并开始执行触发器调度任务
  23. ScheduledTask scheduledTask = this.scheduledTaskRegistrar.scheduleTriggerTask(triggerTask);
  24. //注册调度任务成功后,把执行结果缓存起来,用于调度任务的动态管理
  25. this.triggerTaskMap.put(taskName, scheduledTask);
  26. //调度任务注册成功后,更新调度任务状态为启动状态
  27. remindTaskBean.setTaskStatus( 1);
  28. this.remindTaskDao.updateByTaskName(remindTaskBean);
  29. }

  
  1. /**
  2. * 停止调度任务
  3. * @param taskName
  4. */
  5. public synchronized void cancel (String taskName) {
  6. //这个地方其实也需要加一些判断,比如只有启动状态的调度任务才能取消,不能取消处于停止状态的任务
  7. //取缓存结果中取出要取消的调度任务,然后取消执行
  8. ScheduledTask scheduledTask = this.triggerTaskMap.get(taskName);
  9. scheduledTask.cancel();
  10. //调度任务取消后,更新调度任务的状态为停止状态
  11. RemindTaskBean remindTaskBean = new RemindTaskBean();
  12. remindTaskBean.setTaskName(taskName);
  13. remindTaskBean.setTaskStatus( 0);
  14. this.remindTaskDao.updateByTaskName(remindTaskBean);
  15. }

定义一个提醒任务控制器,并定义三个接口(调度任务列表查询接口、停止调度任务接口、添加并启动调度任务接口),供前端调用;


  
  1. @RestController
  2. @RequestMapping( "/remindTask")
  3. public class RemindTaskController {
  4. @Autowired
  5. private DynamicScheduleTask dynamicScheduleTask;
  6. @GetMapping( "/list")
  7. public List< RemindTaskBean> list( ){
  8. List< RemindTaskBean> remindTasks = dynamicScheduleTask. taskList();
  9. return remindTasks;
  10. }
  11. /**
  12. * 停止调度任务
  13. * @param taskName
  14. */
  15. @GetMapping( "/cancel")
  16. public void cancel( String taskName){
  17. dynamicScheduleTask. cancel(taskName);
  18. }
  19. /**
  20. * 添加并启动调度任务
  21. * @param taskName
  22. */
  23. @GetMapping( "/add")
  24. public void add( String taskName){
  25. dynamicScheduleTask. addTask(taskName);
  26. }
  27. }

3. 基本实现原理

这里回顾一下Springboot实现定时调度任务的动态管理的基本原理:

  1. 创建一个提醒任务数据表,用于存储提醒调度任务的基本信息,如cron表达式、具体的实现类;
  2. 通过实现接口SchedulingConfigurer并重写configureTasks(),在spring容器启动完成时,完成提醒调度任务的初次注册并开始执行;
  3. 对前端暴露接口(任务列表接口、启动调度任务接口、停止调度任务接口),实现调度任务的动态管理;

4. 总结

        总体来说,实现原理还是比较简单、且容易理解的。但是这个实现方案也是有缺点的,实际开发过程中应该根据具体情况作出适当调整。具体的缺点就是初次完成调度任务注册后,会把执行结果缓存起来,以方便对调度任务进行动态管理,这里使用的是虚拟机级别的缓存(ConcurrentHashMap),所以如果实际业务场景中是分布式部署,就考虑使用分布式缓存。

文章内的源码示例可以从这里下载:https://download.csdn.net/download/fox9916/87354573


转载:https://blog.csdn.net/fox9916/article/details/128494957
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场