Springboot实现动态定时任务流程详解

作者:CzRger 时间:2022-09-28 02:26:55 

一、静态

静态的定时任务可以直接使用注解@Scheduled,并在启动类上配置@EnableScheduling即可

@PostMapping("/list/test1")
 @Async
 @Scheduled(cron = "0 * * * * ?")
 public void test1() throws Exception {
   Object obj = this.getClass().newInstance();
   log.info("执行静态定时任务时间/test1:param:{}", new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
 }

二、动态

动态的定时任务需要根据ScheduledTaskRegistrar的addTriggerTask方法进行实现,网上大多数资料是在Trigger中动态查询cron表达式来实现,但存在一个问题就是只有在下一次执行的时候才会刷新,比如一开始设置的每天12点执行一次,如果项目启动后,修改为每小时执行一次的话,需要等到下一次12点执行之后,才会刷新cron表达式,所以通过对ScheduledTaskRegistrar的源码分析,构建了以下解决方案,可以实现对定时任务的额单次额外执行、停止、启动三个基本功能。在此只列出关键代码,关于项目启动、数据库连接的代码等就不过多说明了。

1、基本代码

首先新建定时任务信息的库表及实体类

Springboot实现动态定时任务流程详解

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("cron")
public class Cron implements Serializable {
   private static final long serialVersionUID = 1L;
   @TableId(value = "id", type = IdType.ASSIGN_UUID)
   private String id;
   private String cronFlag;
   private String cronExpression;
   private String className;
   private String methodName;
}

用于获取bean实例的工具类

@Component
public class BeansUtils implements ApplicationContextAware {
 private static ApplicationContext context;
 public static <T> T getBean(Class<T> bean) {
   return context.getBean(bean);
 }
 public static <T> T getBean(String var1, @Nullable Class<T> var2){
   return context.getBean(var1, var2);
 }
 public static ApplicationContext getContext() {
   return context;
 }
 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
   context = applicationContext;
 }
}

实现定时任务类

@Configuration
@EnableScheduling
@EnableAsync
@Slf4j
public class RCScheduleTask implements SchedulingConfigurer {
 @Autowired
 private ICronService iCronService;
 private static ScheduledTaskRegistrar scheduledTaskRegistrar;
 public static Map<String, TriggerTask> triggerTaskMap;
 @Override
 public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
   scheduledTaskRegistrar = taskRegistrar;
   Cron cron = new Cron();
   cron.setCronFlag("1");
   List<Cron> list = iCronService.getList(cron);
   if (list != null) {
     initTriggerTask(list);
   }
 }
 public void initTriggerTask(List<Cron> list) {
   triggerTaskMap = new HashMap<>();
   for (Cron cron : list) {
     TriggerTask triggerTask = new TriggerTask(getRunnable(cron), getTrigger(cron));
     scheduledTaskRegistrar.addTriggerTask(triggerTask);
     triggerTaskMap.put(cron.getId(), triggerTask);
   }
 }
 private static Runnable getRunnable(Cron cron) {
   return new Runnable() {
     @Override
     public void run() {
       Class<?> clazz;
       try {
         clazz = Class.forName(cron.getClassName());
         Object bean = BeansUtils.getBean(clazz);
         Method method = ReflectionUtils.findMethod(bean.getClass(), cron.getMethodName());
         ReflectionUtils.invokeMethod(method, bean);
       } catch (Exception e) {
         e.printStackTrace();
       }
     }
   };
 }
 private static Trigger getTrigger(Cron cron) {
   return new Trigger() {
     @Override
     public Date nextExecutionTime(TriggerContext triggerContext) {
       CronTrigger trigger = new CronTrigger(cron.getCronExpression());
       Date nextExec = trigger.nextExecutionTime(triggerContext);
       return nextExec;
     }
   };
 }
 public static boolean run(String id) {
   TriggerTask tt = triggerTaskMap.get(id);
   if (tt != null) {
     tt.getRunnable().run();
     return true;
   }
   return false;
 }
 public static boolean stop(String id) {
   TriggerTask tt = triggerTaskMap.get(id);
   if (tt != null) {
     Set<ScheduledTask> scheduledTasks = scheduledTaskRegistrar.getScheduledTasks();
     for (ScheduledTask st:scheduledTasks) {
       boolean b = st.getTask().getRunnable() == tt.getRunnable();
       if (b) {
         st.cancel();
         return true;
       }
     }
   }
   return false;
 }
 public static boolean start(Cron cron) throws Exception {
   try {
     triggerTaskMap.remove(cron.getId());
     TriggerTask tt = new TriggerTask(getRunnable(cron), getTrigger(cron));
     triggerTaskMap.put(cron.getId(), tt);
     scheduledTaskRegistrar.scheduleTriggerTask(tt);
     return true;
   } catch (Exception e) {
     e.printStackTrace();
   }
   return false;
 }
}

控制调用类,RCResult为自定义封装的返回结果类,可自行定义

@RestController
@RequestMapping("/cron")
@Slf4j
public class CronController {
 @Autowired
 private ICronService iCronService;
 @Autowired
 private RCScheduleTask rcScheduleTask;
 @GetMapping("/task/1")
 @Async
 public void task1() {
   log.info("模拟任务1-执行时间:{}", new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
 }
 @GetMapping("/task/2")
 @Async
 public void task2() {
   log.info("模拟任务2-执行时间:{}", new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
 }
 @GetMapping("/task/3")
 @Async
 public void task3() {
   log.info("模拟任务3-执行时间:{}", new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
 }
 @GetMapping("/run/{id}")
 public RCResult run(@PathVariable("id") String id) {
   Boolean b = RCScheduleTask.run(id);
   if (b) {
     return RCResult.ok("执行任务" + id + "成功");
   }
   return RCResult.error("执行任务" + id + "失败");
 }
 @GetMapping("/stop/{id}")
 public RCResult stop(@PathVariable("id") String id) {
   Boolean b = RCScheduleTask.stop(id);
   if (b) {
     return RCResult.ok("停止任务" + id + "成功");
   }
   return RCResult.error("停止任务" + id + "失败");
 }
 @GetMapping("/start/{id}")
 public RCResult start(@PathVariable("id") String id) throws Exception {
   Cron cron = iCronService.getById(id);
   if (cron != null) {
     Boolean b = RCScheduleTask.start(cron);
     if (b) {
       return RCResult.ok("开始任务" + id + "成功");
     }
   }
   return RCResult.error("开始任务" + id + "失败");
 }
}

2、方案详解

2.1 初始化

通过@EnableScheduling和@EnableAsync两个标签,开启定时任务和多线程

@Configuration
@EnableScheduling
@EnableAsync
@Slf4j
public class RCScheduleTask implements SchedulingConfigurer {

重写SchedulingConfigurer的configureTasks方法,可以在项目启动时自动加载状态为启用(cron_flag为1)定时任务列表

@Override
 public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
   scheduledTaskRegistrar = taskRegistrar;
   Cron cron = new Cron();
   cron.setCronFlag("1");
   List<Cron> list = iCronService.getList(cron);
   if (list != null) {
     initTriggerTask(list);
   }
 }

TriggerTask创建的两个参数Runnable和Trigger可以理解为实现的操作和执行的周期,在Runnable中我们通过反射的方式,从库中取到调用方法的类名和方法名,来执行接口操作,在Trigger中我们根据库中的cron表达式来设置执行周期

public void initTriggerTask(List<Cron> list) {
   triggerTaskMap = new HashMap<>();
   for (Cron cron : list) {
     TriggerTask triggerTask = new TriggerTask(getRunnable(cron), getTrigger(cron));
     scheduledTaskRegistrar.addTriggerTask(triggerTask);
     triggerTaskMap.put(cron.getId(), triggerTask);
   }
 }
 private static Runnable getRunnable(Cron cron) {
   return new Runnable() {
     @Override
     public void run() {
       Class<?> clazz;
       try {
         clazz = Class.forName(cron.getClassName());
         Object bean = BeansUtils.getBean(clazz);
         Method method = ReflectionUtils.findMethod(bean.getClass(), cron.getMethodName());
         ReflectionUtils.invokeMethod(method, bean);
       } catch (Exception e) {
         e.printStackTrace();
       }
     }
   };
 }
 private static Trigger getTrigger(Cron cron) {
   return new Trigger() {
     @Override
     public Date nextExecutionTime(TriggerContext triggerContext) {
       CronTrigger trigger = new CronTrigger(cron.getCronExpression());
       Date nextExec = trigger.nextExecutionTime(triggerContext);
       return nextExec;
     }
   };
 }

库表数据

Springboot实现动态定时任务流程详解

从执行结果中可以看到状态为已启用的task1和task2执行了,而task3并没有执行

Springboot实现动态定时任务流程详解

2.2 单次执行

我们可以通过获取TriggerTask的Runnable来执行run()方法,来单次执行定时任务

任务类中定义run方法

public static boolean run(String id) {
   TriggerTask tt = triggerTaskMap.get(id);
   if (tt != null) {
     tt.getRunnable().run();
     return true;
   }
   return false;
 }

接口调用

@GetMapping("/run/{id}")
 public RCResult run(@PathVariable("id") String id) {
   Boolean b = RCScheduleTask.run(id);
   if (b) {
     return RCResult.ok("执行任务" + id + "成功");
   }
   return RCResult.error("执行任务" + id + "失败");
 }

模拟接口调用

Springboot实现动态定时任务流程详解

从调用结果中可以看到,task1在06秒的时候单独执行了一次,并且没有影响后续执行

Springboot实现动态定时任务流程详解

2.3 停止任务

停止任务需要执行ScheduledTask类的cancel()方法,由于在初始化时通过addTriggerTask方法并不会立刻加入到ScheduledTask列表中,所以需要在调用时通过ScheduledTaskRegistrar获取ScheduledTask列表,然后与TriggerTask的Runnable进行比较判断是否一致

任务类中定义stop

public static boolean stop(String id) {
   TriggerTask tt = triggerTaskMap.get(id);
   if (tt != null) {
     Set<ScheduledTask> scheduledTasks = scheduledTaskRegistrar.getScheduledTasks();
     for (ScheduledTask st:scheduledTasks) {
       boolean b = st.getTask().getRunnable() == tt.getRunnable();
       if (b) {
         st.cancel();
         return true;
       }
     }
   }
   return false;
 }

调用

@GetMapping("/stop/{id}")
 public RCResult stop(@PathVariable("id") String id) {
   Boolean b = RCScheduleTask.stop(id);
   if (b) {
     return RCResult.ok("停止任务" + id + "成功");
   }
   return RCResult.error("停止任务" + id + "失败");
 }

Springboot实现动态定时任务流程详解

在调用结果中可以看到,task1不再执行了

Springboot实现动态定时任务流程详解

2.4 启用任务

启用任务需要通过ScheduledTaskRegistrar的scheduleTriggerTask()方法进行调用,由于源码中并未提供针对Runnable或Trigger的单独修改方法,所以在这里我们通过新建实例进行替换,在执行前先停止任务

public static boolean start(Cron cron) throws Exception {
   try {
     stop(cron.getId());
     triggerTaskMap.remove(cron.getId());
     TriggerTask tt = new TriggerTask(getRunnable(cron), getTrigger(cron));
     triggerTaskMap.put(cron.getId(), tt);
     scheduledTaskRegistrar.scheduleTriggerTask(tt);
     return true;
   } catch (Exception e) {
     e.printStackTrace();
   }
   return false;
 }

调用

@GetMapping("/start/{id}")
 public RCResult start(@PathVariable("id") String id) throws Exception {
   Cron cron = iCronService.getById(id);
   if (cron != null) {
     Boolean b = RCScheduleTask.start(cron);
     if (b) {
       return RCResult.ok("开始任务" + id + "成功");
     }
   }
   return RCResult.error("开始任务" + id + "失败");
 }

Springboot实现动态定时任务流程详解

通过调用结果,可以看到在启用后,task1重新加入到定时任务队列中了

Springboot实现动态定时任务流程详解

在修改task1的执行周期后再次调用start方法

Springboot实现动态定时任务流程详解

从调用结果中可以看到,task1的周期已被更改

Springboot实现动态定时任务流程详解

三、小结

昨天刚接触定时任务,一开始也是在网上搜索资料,后来发现案例都无法满足自身需求,或者只是讲解怎么使用,根本无法映射到实际业务上,所以通过对源码的分析,一层层的梳理,模拟了此篇关于定时任务的解决方案,由于时间原因,在此省略了前端界面的编写,但大多数实际需求无非也就是通过一个开关或按钮来对任务进行启用、停止操作,或者额外需要单次执行任务,在此对动态定时任务的方案进行记录,同时也希望能帮助到遇到同样问题的同学们

来源:https://blog.csdn.net/CzRger/article/details/126782706

标签:Springboot,动态,定时任务
0
投稿

猜你喜欢

  • redisson分布式限流RRateLimiter源码解析

    2021-05-29 13:10:15
  • C#实现合并多个word文档的方法

    2022-10-02 08:32:39
  • Java基于外观模式实现美食天下食谱功能实例详解

    2022-08-22 22:59:51
  • Android定时器Timer的停止和重启实现代码

    2022-10-03 23:25:43
  • Java MyBatis可视化代码生成工具使用教程

    2022-12-05 15:23:39
  • Android开发之HttpClient异步请求数据的方法详解【附demo源码下载】

    2023-01-09 11:08:31
  • Java 字符终端上获取输入三种的方式分享

    2021-12-31 04:52:45
  • 浅谈一下SpringCloud中Hystrix服务熔断和降级原理

    2021-10-02 08:46:41
  • C#提取PPT文本和图片的实现方法

    2022-10-28 08:02:59
  • Java基础之容器Vector详解

    2023-11-25 13:10:07
  • C#利用缓存分块读写大文件

    2022-10-20 11:18:22
  • Jmeter环境搭建及安装步骤

    2021-11-03 21:06:10
  • SpringBoot整合Mybatis,解决TypeAliases配置失败的问题

    2023-11-28 14:59:24
  • 使用SharedPreferences在Android存储对象详细代码

    2022-06-14 12:50:29
  • Java 深拷贝与浅拷贝的分析

    2023-07-30 14:13:13
  • 使用Spring的拦截器监测每个Controller或方法的执行时长

    2021-12-19 16:36:01
  • Android编程常用技巧实例总结

    2022-11-30 20:27:44
  • Java swing读取txt文件实现学生考试系统

    2021-06-13 17:41:02
  • Java 获取Web项目相对webapp地址的实例

    2022-07-03 17:46:00
  • Struts2+Hibernate实现数据分页的方法

    2022-10-28 05:31:13
  • asp之家 软件编程 m.aspxhome.com