小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
背景:📅
最近要用到这个定时任务,之前就简单使用注解的那种方式,需求一变化,就得重新修改。
就想到了动态定时任务,连接数据库来动态选择,这样确实解决了问题。
但是仍然有一个缺陷,就是没法设置任务的执行时间,无法做到像 QQ 发说说那样,给 xdm 祝福生日时,设定说说为晚上00:00发布。
本文就以上三点用自己的思路写了一个小Demo,希望对大家有所帮助。
👩💻
封面:来自于校园一角,秋意渐浓,思念渐深
。
倒计时拉倒计时拉,xdm,国庆就要来了
前言:
阅读完本文:🐱👓
- 知晓
SpringBoot
用注解如何实现定时任务 - 明白
SpringBoot
如何实现一个动态定时任务 (与数据库相关联实现) - 理解
SpringBoot
实现设置时间执行定时任务 (使用ThreadPoolTaskScheduler
实现)
一、注解实现定时任务
用注解实现是真的简单,只要会 cron 表达式就行。🧙♂️
第一步: 主启动类上加上 @EnableScheduling
注解
@EnableScheduling @SpringBootApplication public class SpringBootScheduled { public static void main(String[] args) { SpringApplication.run(SpringBootScheduled.class); } }
第二步:写一个类,注入到Spring,关键就是 @Scheduled
注解。 () 里就是 cron 表达式,用来说明这个方法的执行周期的。 🛌
我常常也记不住,通常是在线生成的: Cron 表达式在线生成
/** * 定时任务 静态定时任务 * * 第一位,表示秒,取值0-59 * 第二位,表示分,取值0-59 * 第三位,表示小时,取值0-23 * 第四位,日期天/日,取值1-31 * 第五位,日期月份,取值1-12 * 第六位,星期,取值1-7,1表示星期天,2表示星期一 * 第七位,年份,可以留空,取值1970-2099 * @author crush * @since 1.0.0 * @Date: 2021-07-27 21:13 */ @Component public class SchedulingTaskBasic { /** * 每五秒执行一次 */ @Scheduled(cron = "*/5 * * * * ?") private void printNowDate() { long nowDateTime = System.currentTimeMillis(); System.out.println("固定定时任务执行:--->"+nowDateTime+",此任务为每五秒执行一次"); } }
执行效果:
源码在文末。🏍
二、动态定时任务
其实也非常的简单。
2.1、建数据表
第一步:建个数据库表。
CREATE TABLE `tb_cron` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '动态定时任务时间表', `cron_expression` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '定时任务表达式', `cron_describe` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; INSERT INTO `tb_cron` VALUES (1, '0 0/1 * * * ?', '每分钟执行一次');
2.2、导入依赖,基础编码
第二步:导入数据库相关依赖,做到能从数据库查询数据。大家都会。🤸♂️
第三步: 编码
实体类:
@Data @TableName("tb_cron") public class Cron { private Long id; private String cronExpression; private String cronDescribe; }
mapper层:
@Repository public interface CronMapper extends BaseMapper<Cron> { @Select("select cron_expression from tb_cron where id=1") String getCron1(); }
2.3、主要实现代码
第四步:写一个类 实现 SchedulingConfigurer
🍻
实现 void configureTasks(ScheduledTaskRegistrar taskRegistrar);
方法,此方法的作用就是根据给定的 ScheduledTaskRegistrar 注册 TaskScheduler 和特定的Task实例
@Component public class CompleteScheduleConfig implements SchedulingConfigurer { @Autowired @SuppressWarnings("all") CronMapper cronMapper; /** * 执行定时任务. */ @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addTriggerTask( //1.添加任务内容(Runnable) () -> System.out.println("执行动态定时任务1: " + LocalDateTime.now().toLocalTime()+",此任务执行周期由数据库中的cron表达式决定"), //2.设置执行周期(Trigger) triggerContext -> { //2.1 从数据库获取执行周期 String cron = cronMapper.getCron1(); //2.2 合法性校验. if (cron!=null) { // Omitted Code .. } //2.3 返回执行周期(Date) return new CronTrigger(cron).nextExecutionTime(triggerContext); } ); } }
2.4、效果
注意:当你修改了任务执行周期后,生效时间为执行完最近一次任务后。这一点是需要注意的,用生活中的例子理解就是我们取消电话卡的套餐也要下个月生效,含义是一样的。
源码同样在文末。
三、实现设置时间定时任务
通常业务场景是我前言中说的那样,是一次性的定时任务。如:我设置了我写的这篇文章的发布时间为今天下午的两点,执行完就删除没有了。一次性的。
实现主要依靠于 TaskScheduler
的ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
方法来实现。其本质和动态定时任务的实现是一样的。
3.1、实现重点
代码中都含有注解,不多做阐述。
import cn.hutool.core.convert.ConverterRegistry; import com.crush.scheduled.entity.Task; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; /** * @author crush */ @Component @Slf4j public class DynamicTaskService { /** * 以下两个都是线程安全的集合类。 */ public Map<String, ScheduledFuture<?>> taskMap = new ConcurrentHashMap<>(); public List<String> taskList = new CopyOnWriteArrayList<String>(); private final ThreadPoolTaskScheduler syncScheduler; public DynamicTaskService(ThreadPoolTaskScheduler syncScheduler) { this.syncScheduler = syncScheduler; } /** * 查看已开启但还未执行的动态任务 * @return */ public List<String> getTaskList() { return taskList; } /** * 添加一个动态任务 * * @param task * @return */ public boolean add(Task task) { // 此处的逻辑是 ,如果当前已经有这个名字的任务存在,先删除之前的,再添加现在的。(即重复就覆盖) if (null != taskMap.get(task.getName())) { stop(task.getName()); } // hutool 工具包下的一个转换类型工具类 好用的很 ConverterRegistry converterRegistry = ConverterRegistry.getInstance(); Date startTime = converterRegistry.convert(Date.class, task.getStart()); // schedule :调度给定的Runnable ,在指定的执行时间调用它。 //一旦调度程序关闭或返回的ScheduledFuture被取消,执行将结束。 //参数: //任务 – 触发器触发时执行的 Runnable //startTime – 任务所需的执行时间(如果这是过去,则任务将立即执行,即尽快执行) ScheduledFuture<?> schedule = syncScheduler.schedule(getRunnable(task), startTime); taskMap.put(task.getName(), schedule); taskList.add(task.getName()); return true; } /** * 运行任务 * * @param task * @return */ public Runnable getRunnable(Task task) { return () -> { log.info("---动态定时任务运行---"); try { System.out.println("此时时间==>" + LocalDateTime.now()); System.out.println("task中设定的时间==>" + task); Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } log.info("---end--------"); }; } /** * 停止任务 * * @param name * @return */ public boolean stop(String name) { if (null == taskMap.get(name)) { return false; } ScheduledFuture<?> scheduledFuture = taskMap.get(name); scheduledFuture.cancel(true); taskMap.remove(name); taskList.remove(name); return true; } }
3.2、异步线程池的配置
/** * 异步线程池ThreadPoolExecutor 配置类 * * @Author: crush * @Date: 2021-07-23 14:14 */ @Configuration public class ThreadPoolTaskExecutorConfig { @Bean public ThreadPoolTaskScheduler syncScheduler() { ThreadPoolTaskScheduler syncScheduler = new ThreadPoolTaskScheduler(); syncScheduler.setPoolSize(5); // 这里给线程设置名字,主要是为了在项目能够更快速的定位错误。 syncScheduler.setThreadGroupName("syncTg"); syncScheduler.setThreadNamePrefix("syncThread-"); syncScheduler.initialize(); return syncScheduler; } }
3.3、业务代码
这里需要注意一个点,我给项目中的 LocalDateTime
做了类型转换。这里没贴出来(主要是复制以前的代码遗留下来的,源码中都有)
大家简单使用,可以直接用注解 标注在 LocalDateTime
属性上即可。
package com.crush.scheduled.controller; import com.crush.scheduled.entity.Task; import com.crush.scheduled.service.DynamicTaskService; import org.springframework.web.bind.annotation.*; import java.util.List; /** * @Author: crush * @Date: 2021-07-29 15:26 * version 1.0 */ @RestController @RequestMapping("/dynamicTask") public class DynamicTaskController { private final DynamicTaskService dynamicTask; public DynamicTaskController(DynamicTaskService dynamicTask) { this.dynamicTask = dynamicTask; } /** * 查看已开启但还未执行的动态任务 * @return */ @GetMapping public List<String> getStartingDynamicTask(){ return dynamicTask.getTaskList(); } /** * 开启一个动态任务 * @param task * @return */ @PostMapping("/dynamic") public String startDynamicTask(@RequestBody Task task){ // 将这个添加到动态定时任务中去 dynamicTask.add(task); return "动态任务:"+task.getName()+" 已开启"; } /** * 根据名称 停止一个动态任务 * @param name * @return */ @DeleteMapping("/{name}") public String stopDynamicTask(@PathVariable("name") String name){ // 将这个添加到动态定时任务中去 if(!dynamicTask.stop(name)){ return "停止失败,任务已在进行中."; } return "任务已停止"; } }
简单封装的一个实体类:
/** * @Author: crush * @Date: 2021-07-29 15:35 * version 1.0 */ @Data @Accessors(chain = true) // 方便链式编写 习惯所然 public class Task { /** * 动态任务名曾 */ private String name; /** * 设定动态任务开始时间 */ private LocalDateTime start; }
3.4、效果
💫💨
开启一个动态任务:
查看开启还未执行的动态任务:
执行结果:
和我们代码中是一模一样的。
停止任务:
再去查看就是已经停止的拉
四、自言自语
本文就是简单介绍了,具体使用时还需要根据具体情况具体分析啦。