【资料图】
springBoot自定义cron表达式注册定时任务一、原理1、使用Spring自带的TaskScheduler注册任务2、注册后返回:ScheduledFuture,用于取消定时任务3、注册任务后不会马上取消任务,所以将任务缓存。在需要取消任务的时候调用取消接口取消4、cron表达式可以由前端或者后端生成。实现中会校验cron表达式public class TestScheduled { /** * 1、使用Spring自带的TaskScheduler注册任务 * 2、注册后返回:ScheduledFuture,用于取消定时任务 */ @Resource private TaskScheduler taskScheduler; public void registrarTask() { //具体的任务Runnable(一般使用类实现Runnable接口) Runnable taskRunnable = new Runnable() { @Override public void run() { } }; //cron表达式触发器 CronTrigger trigger = new CronTrigger("0/5 * * * * ?"); //开启定时任务的真正方法 ScheduledFuture> future = this.taskScheduler.schedule(taskRunnable, trigger); //取消定时任务 future.cancel(true); }}
二、具体实现1、配置任务调度器作用:设置:核心线程数:可同时执行任务数;设置线程名称前缀可以不配置。不配置就默认使用spring自带的package com.cc.ssd.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.TaskScheduler;import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;/** TaskScheduler任务调度器配置类 * @since 2023/4/21 0021 * @author CC **/@Configurationpublic class CronTaskConfig { /** * 任务调度器自定义配置 */ @Bean(name = "taskScheduler") public TaskScheduler taskScheduler() { // 任务调度线程池 ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); // 定时任务执行线程池核心线程数:可同时执行4个任务 taskScheduler.setPoolSize(4); taskScheduler.setRemoveOnCancelPolicy(true); // 线程名称前缀 taskScheduler.setThreadNamePrefix("Cs-ThreadPool-"); return taskScheduler; }}
2、定时任务注册类作用:缓存、注册定时任务;还可以查询、删除定时任务package com.cc.ssd.registrar;import com.cc.ssd.task.CronTaskFuture;import com.cc.ssd.task.CronTaskRunnable;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.DisposableBean;import org.springframework.scheduling.TaskScheduler;import org.springframework.scheduling.config.CronTask;import org.springframework.scheduling.support.CronExpression;import org.springframework.stereotype.Component;import org.springframework.util.Assert;import javax.annotation.Resource;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.*;import java.util.concurrent.ConcurrentHashMap;import java.util.stream.Collectors;/** 注册定时任务:缓存定时任务、注册定时任务到调度中心 * @author CC **/@Componentpublic class CronTaskRegistrar implements DisposableBean { private static final Logger log = LoggerFactory.getLogger(CronTaskRegistrar.class); /** * 缓存任务 * key:具体的任务 * value:注册定时任务后返回的ScheduledFuture */ private final Map scheduledTasks = new ConcurrentHashMap<>(16); /** * 使用自定义的任务调度配置 */ @Resource(name = "taskScheduler") private TaskScheduler taskScheduler; /** 获取任务调度配置 * @return 任务调度配置 */ public TaskScheduler getTaskScheduler() { return this.taskScheduler; } /** 新增定时任务1 * 存在任务:删除此任务,重新新增这个任务 * @param taskRunnable 执行的具体任务定义:taskRunnable 实现Runnable * @param cronExpression cron表达式 */ public void addCronTask(Runnable taskRunnable, String cronExpression) { //验证cron表达式是否正确 boolean validExpression = CronExpression.isValidExpression(cronExpression); if (!validExpression) { throw new RuntimeException("cron表达式验证失败!"); } //获取下次执行时间 CronExpression parse = CronExpression.parse(cronExpression); LocalDateTime next = parse.next(LocalDateTime.now()); if (Objects.nonNull(next)) { //定时任务下次执行的时间 String format = next.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); log.info("定时任务下次执行的时间:{}", format); } //封装成 CronTask(cron任务) CronTask cronTask = new CronTask(taskRunnable, cronExpression); this.addCronTask(cronTask); } /** 新增定时任务2 * @param cronTask :CronTask用于在指定时间间隔内执行定时任务。
* 它是通过CronTrigger来实现的,CronTrigger是一个基于cron表达式的触发器,
* 可以在指定的时间间隔内触发任务执行。
* @since 2023/4/21 0021 * @author CC **/ private void addCronTask(CronTask cronTask) { if (Objects.nonNull(cronTask)) { //1有这个任务,先删除这个任务。再新增 Runnable task = cronTask.getRunnable(); String taskId = null; if (task instanceof CronTaskRunnable) { taskId = ((CronTaskRunnable) task).getTaskId(); } //通过任务id获取缓存的任务,如果包含则删除,然后新增任务 Runnable taskCache = this.getTaskByTaskId(taskId); if (Objects.nonNull(taskCache) && this.scheduledTasks.containsKey(taskCache)) { this.removeCronTaskByTaskId(taskId); } //2注册定时任务到调度中心 CronTaskFuture scheduledFutureTask = this.scheduleCronTask(cronTask); //3缓存定时任务 this.scheduledTasks.put(task, scheduledFutureTask); //todo cc 4可以将任务保存到数据库中……重新启动程序然后加载数据库中的任务到缓存中…… } } /** 注册 ScheduledTask 定时任务 * @param cronTask cronTask * @return 注册定时任务后返回的 ScheduledFutureTask */ private CronTaskFuture scheduleCronTask(CronTask cronTask) { //注册定时任务后记录的Future CronTaskFuture scheduledTask = new CronTaskFuture(); //开启定时任务的真正方法 scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());// scheduledTask.setThreadLocal(this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger())); return scheduledTask; } /** 获取任务列表 * @return */ public List getScheduledTasks() { List tasks = new ArrayList<>(); Set keySet = scheduledTasks.keySet(); keySet.forEach(key -> { CronTaskRunnable task = new CronTaskRunnable(); if (key instanceof CronTaskRunnable) { CronTaskRunnable taskParent = (CronTaskRunnable) key; BeanUtils.copyProperties(taskParent, task); } tasks.add(task); }); return tasks.stream() .sorted(Comparator.comparing(CronTaskRunnable::getTaskId)) .collect(Collectors.toList()); } /** 根据任务id删除单个定时任务 * @param taskId 任务id */ public void removeCronTaskByTaskId(String taskId) { //通过任务id获取任务 Runnable task = this.getTaskByTaskId(taskId); //需要通过任务id获取任务,然后再移除 CronTaskFuture cronTaskFuture = this.scheduledTasks.remove(task); if (Objects.nonNull(cronTaskFuture)) { cronTaskFuture.cancel(); } } /** 通过任务id获取任务。未查询到返回null * @param taskId 任务id * @return java.lang.Runnable * @since 2023/4/21 0021 * @author CC **/ private Runnable getTaskByTaskId(String taskId) { Assert.notNull(taskId, "任务id不能为空!"); Set> entries = scheduledTasks.entrySet(); //根据任务id获取该任务缓存 Map.Entry rcf = entries.stream().filter(rf -> { Runnable key = rf.getKey(); String taskId1 = null; if (key instanceof CronTaskRunnable) { taskId1 = ((CronTaskRunnable) key).getTaskId(); } return taskId.equals(taskId1); }).findAny().orElse(null); if (Objects.nonNull(rcf)) { return rcf.getKey(); } return null; } /** 删除所有的定时任务 * DisposableBean是Spring框架中的一个接口,它定义了一个destroy()方法, * 用于在Bean销毁时执行清理工作。 * 当一个Bean实现了DisposableBean接口时, * Spring容器会在该Bean销毁时自动调用destroy()方法, * 以便进行一些清理工作,例如释放资源等。 * 如果您的Bean需要在销毁时执行一些清理工作, * 那么实现DisposableBean接口是一个很好的选择。 */ @Override public void destroy() { //关闭所有定时任务 for (CronTaskFuture task : this.scheduledTasks.values()) { task.cancel(); } //清空缓存 this.scheduledTasks.clear(); log.info("取消所有定时任务!"); //todo cc 修改或删除数据库的任务 }}
3、定时任务的执行结果ScheduledFuture作用:CronTaskFuture类中使用的是ScheduledFuture对象来表示定时任务的执行结果。package com.cc.ssd.task;import java.util.Objects;import java.util.concurrent.ScheduledFuture;/** CronTaskFuture类中使用的是ScheduledFuture对象来表示定时任务的执行结果。 * ——最后ps:也可以不要这个记录类,直接缓存ScheduledFuture对象。 * 用来记录单独的Future、定时任务注册任务后产生的 * @author CC **/public final class CronTaskFuture { /** 每个线程一个副本 * 经过测试这里不能使用ThreadLocal */// private static final ThreadLocal> THREAD_LOCAL = new ThreadLocal<>(); /** 最后ps:由于ScheduledFuture是线程安全的。这里不用 volatile 或者 ThreadLocal * 注册任务后返回的:ScheduledFuture 用于记录并取消任务 * 这两个都可以不使用。直接给future赋值 * volatile:线程之间可见:volatile用于实现多线程之间的可见性和一致性,保证数据的正确性。 * ThreadLocal:用于实现线程封闭,保证线程安全 * 使用建议: * CronTaskFuture类中使用的是ScheduledFuture对象来表示定时任务的执行结果。 * ScheduledFuture对象是线程安全的,因此不需要使用volatile关键字来保证多线程同步。 * 如果需要在多线程中使用线程本地变量,可以使用ThreadLocal。 * 因此,建议在CronTaskFuture类中使用ScheduledFuture对象,而不是使用volatile或ThreadLocal。 * 另外,如果需要在Spring容器销毁时执行一些清理操作,可以实现DisposableBean接口,并在destroy()方法中进行清理操作。 */ public ScheduledFuture> future;// public volatile ScheduledFuture> future;// public void setThreadLocal(ScheduledFuture> future){// THREAD_LOCAL.set(future);// } /** * 取消当前定时任务 */ public void cancel() { try {// ScheduledFuture> future = THREAD_LOCAL.get(); ScheduledFuture> future = this.future; if (Objects.nonNull(future)) { future.cancel(true); } } catch (Exception e) { throw new RuntimeException("销毁定时任务失败!"); } finally {// THREAD_LOCAL.remove(); } }}
4、具体的任务。实现Runable接口任务处理的方式按照自己的需求去实现即可package com.cc.ssd.task;import lombok.Data;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;/** 具体任务实现 * @author CC * @since 2023/4/21 0021 */@Datapublic class CronTaskRunnable implements Runnable { private static final Logger log = LoggerFactory.getLogger(CronTaskRunnable.class); /** * 任务id(必须唯一) */ private String taskId; /** * 任务类型:自定义 */ private Integer taskType; /** * 任务名字 */ private String taskName; /** * 任务参数 */ private Object[] params; public CronTaskRunnable() { } public CronTaskRunnable(String taskId, Integer taskType, String taskName, Object... params) { this.taskId = taskId; this.taskType = taskType; this.taskName = taskName; this.params = params; } /** 执行任务 * @since 2023/4/21 0021 * @author CC **/ @Override public void run() { long start = System.currentTimeMillis(); //具体的任务。 log.info("\n\t {}号.定时任务开始执行 - taskId:{},taskName:{},taskType:{},params:{}", taskType, taskId, taskName, taskType, params); //任务处理的方式: //todo cc 1就在这里执行:模拟任务 //todo cc 2开启策略模式,根据任务类型 调度不同的任务 //todo cc 3使用反射:传来bean名字,方法名字,调用不同的任务 //todo cc 4开启队列,把要执行的任务放到队列中,然后执行 —— 使用场景:每个任务执行很耗时的情况下使用 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } log.info("\n\t {}号.任务执行完成 - 耗时:{},taskId:{},taskType:{}", taskType, System.currentTimeMillis() - start, taskId, taskType); }}
5、测试Controllerpackage com.cc.ssd.web.controller;import com.cc.ssd.registrar.CronTaskRegistrar;import com.cc.ssd.task.CronTaskRunnable;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import java.util.List;import java.util.Map;/** * @author CC * @since 2023/4/21 0021 */@RestController@RequestMapping("/scheduled")public class TestScheduledController { @Resource private CronTaskRegistrar cronTaskRegistrar; /** 获取任务列表 * @return java.util.List * @since 2023/4/21 0021 * @author CC **/ @GetMapping public List getScheduledTasks() { return cronTaskRegistrar.getScheduledTasks(); } /** 添加任务 * @param param param * @return java.lang.String * @since 2023/4/21 0021 * @author CC **/ @PostMapping public String addCronTask(@RequestBody Map param) { //自己拿任务参数的逻辑:可以把每个任务保存到数据库,重新启动任务的同时,加载这些任务到任务调度中心 String taskId = (String) param.get("taskId"); Integer taskType = (Integer) param.get("taskType"); String taskName = (String) param.get("taskName"); Object params = param.get("params"); //添加任务参数 CronTaskRunnable task = new CronTaskRunnable(taskId, taskType, taskName, params); //注册任务:cron表达式,可以从传入不一样的 cronTaskRegistrar.addCronTask(task, "0/5 * * * * ?"); return "ok"; } /** 根据任务id删除定时任务 * @param taskId 任务id * @return java.lang.String * @since 2023/4/21 0021 * @author CC **/ @DeleteMapping public String removeCronTaskByTaskId(@RequestParam String taskId) { cronTaskRegistrar.removeCronTaskByTaskId(taskId); return "ok"; } /** 删除全部任务 * @return java.lang.String * @since 2023/4/21 0021 * @author CC **/ @DeleteMapping("/removeAll") public String removeCronTask() { cronTaskRegistrar.destroy(); return "ok"; }}
6、最后效果自己用controller去测试一波吧 关键词: