基于SpringBoot实现轻量级的动态定时任务调度
在使用SpringBoot框架进行开发时,一般都是通过@Scheduled注解进行定时任务的开发:
@Component
public class TestTask
{@Scheduled(cron="0/5 * * * * ? ") //每5秒执行一次public void execute(){SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); log.info("任务执行" + df.format(new Date()));}
}
但是这种方式存在一个问题,那就是任务的周期控制是死的,必须编写在代码中,如果遇到需要在系统运行过程中想中止、立即执行、修改执行周期等动态操作的需求时,使用注解的方式便不能满足了,当然为了满足此种需求可以额外再引入其他任务调度插件(例如XXL-Job等),但是引入其他组件是需要衡量成本的,额外的依赖成本、组件的维护成本、开发的复杂度等等,所以如果系统体量不是那么大,完全没必要通过增加组件来完成,可以基于SpringBoot框架实现一套内置轻量级的任务调度。
设计思路
整体设计

这里我们把定时任务以类作为基础单位,即一个类为一个任务,然后通过配置数据的方式,进行任务的读取,通过反射生成任务对象,使用SpringBoot本身的线程池任务调度,完成动态的定时任务驱动,同时通过接口支撑实现相应的REST API对外暴露接口
任务模型
首先基于模板模式,设计基础的任务执行流程抽象类,定义出一个定时任务需要执行的内容和步骤和一些通用的方法函数,后续具体的定时任务直接继承该父类,实现该父类的before、start、after三个抽象函数即可,所有公共操作均在抽象父类完成

特殊说明:
基于此方法创建的类是不归Spring的容器管理的,所以自定义的任务子类中是无法使用SpringBoot中的任何注解,尤其在自定义任务类中如果需要依赖其他Bean时,需要借助抽象父类AbstractBaseCronTask中已经实现的<T> T getServer(Class<T> className)来完成,getServer的实现如下:
public <T> T getServer(Class<T> className){return applicationContext.getBean(className);}
是通过SpringBoot中的ApplicationContext接口来获取Spring的上下文,以此来满足可以获取Spring中其他Bean的诉求。
例如,有个定时任务TaskOne类,它需要使用UserService类中的 caculateMoney()的方法,势必这个定时任务需要依赖UserService类,而TaskOne并非是Spring创建的对象,而是我们人为干预生成的对象,所以它是不在Spring的Bean管理范围的,自然也就无法使用@Autowird等方式注入UserService类,此时就需要使用getServer方法来获取UserService对象
//自定义定时任务类
public class TaskOne extends AbstractBaseCronTask {private UserService userService;public TestTask(TaskEntity taskEntity) {super(taskEntity);}@Overridepublic void beforeJob() {//任务运行第一步,先将userService进行变量注入userService = getServer(UserService.class);……}@Overridepublic void startJob() {if(XXXX){//直接调用getServer获取需要的beanUser user = getServer(UserMapper.class).findUser("111223")userService.caluateMoney(user);//……其他代码}}@Overridepublic void afterJob() {}
}
任务对象加载过程

核心逻辑在于利用反射,在SpringBoot启动后动态创建相应的定时任务类,并将其放置到SpringBoot的定时线程池中进行维护,同时将该对象同步存放至内存中一份,便于可以实时调用,当进行修改任务相关配置时,需要重新加载一次内容。
public class TaskScheduleServerImpl implements TaskScheduleServer {//正在运行的任务private static ConcurrentHashMap<String, ScheduledFuture> runningTasks = new ConcurrentHashMap<>();//线程池任务调度private ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();public boolean addTaskToScheduling(TaskEntity task) {if(!runningTasks.containsKey(task.getTaskId())){try{Class<?> clazz = Class.forName(task.getTaskClass());Constructor c = clazz.getConstructor(TaskEntity.class);AbstractBaseCronTask runnable = (AbstractBaseCronTask) c.newInstance(task);//反射方式生成对象不属于Spring容器管控,对于Spring的bean使用需要手动注入runnable.setApplicationContext(context);CronTrigger cron = new CronTrigger(task.getTaskCron());//put到runTasksrunningTasks.put(task.getTaskId(), Objects.requireNonNull(this.threadPoolTaskScheduler.schedule(runnable, cron)));//存入内存中,便于外部调用ramTasks.put(task.getTaskId(),runnable);task.setTaskRamStatus(1);taskInfoOpMapper.updateTaskInfo(task);return true;}catch (Exception e){log.error("定时任务加载失败..."+e);}}return false;}
}
部分源码
这里将配置内容放入数据库中,直接以数据库中的表作为任务配置的基础
/**
* 任务对象
**/
@Data
public class TaskEntity implements Serializable {//任务唯一IDprivate String taskId;//任务名称private String taskName;//任务描述private String taskDesc;//执行周期配置private String taskCron;//任务类的全路径private String taskClass;//任务的额外配置private String taskOutConfig;//任务创建时间private String taskCreateTime;//任务是否启动,1启用,0不启用private Integer taskIsUse;//是否随系统启动立即执行private Integer taskBootUp;//任务上次执行状态private Integer taskLastRun;//任务是否加载至内存中 private Integer taskRamStatus;
}
核心逻辑,加载定时任务接口及其实现类
public interface TaskScheduleServer {ConcurrentHashMap<String, AbstractBaseCronTask> getTaskSchedulingRam();/*** 初始化任务调度*/void initScheduling();/*** 添加任务至内存及容器* @param taskEntity 任务实体* @return boolean*/boolean addTaskToScheduling(TaskEntity taskEntity);/*** 从任务调度器中移除任务* @param id 任务id* @return Boolean*/boolean removeTaskFromScheduling(String id);/*** 执行指定任务* @param id 任务id* @return double 耗时*/double runTaskById(String id);/*** 清空任务*/void claearAllTask();/*** 加载所有任务*/void loadAllTask();/*** 运行开机自启任务*/void runBootUpTask();}@Slf4j
@Component
public class TaskScheduleServerImpl implements TaskScheduleServer {…………@Overridepublic double runTaskById(String id) {TaskEntity task = taskInfoOpMapper.queryTaskInfoById(id);if(null!=task) {if (runningTasks.containsKey(task.getTaskId())){ramTasks.get(task.getTaskId()).run();return ramTasks.get(task.getTaskId()).getRunTime();}}return 0d;}@Overridepublic void claearAllTask() {ramTasks.clear();log.info("【定时任务控制器】清除内存任务 完成");runningTasks.clear();log.info("【定时任务控制器】清除线程任务 完成");threadPoolTaskScheduler.shutdown();}@Overridepublic void loadAllTask() {List<TaskEntity> allTask = taskInfoOpMapper.queryTaskInfo(null);for (TaskEntity task : allTask) {if(addTaskToScheduling(task)){log.info("【定时任务初始化】装填任务:{} [ 任务执行周期:{} ] [ bootup:{}]",task.getTaskName(),task.getTaskCron(),task.getTaskBootUp());}}}@Overridepublic void runBootUpTask() {TaskEntity entity = new TaskEntity().taskBootUp(1);List<TaskEntity> list = taskInfoOpMapper.queryTaskInfo(entity);for(TaskEntity task:list){runTaskById(task.getTaskId());}}
}
在SpringBoot中的加载类
@Order(3)
@Component
@Slf4j
public class AfterAppStarted implements ApplicationRunner {TaskScheduleServer taskScheduleServer;@Autowiredpublic void setTaskScheduleServer(TaskScheduleServer taskScheduleServer) {this.taskScheduleServer = taskScheduleServer;}@Overridepublic void run(ApplicationArguments args) throws Exception {//运行随系统启动的定时任务taskScheduleServer.runBootUpTask();}}
对外暴露控制接口及其Service
@RestController
@RequestMapping("/taskScheduling/manage")
@Api(tags = "数据源管理服务")
public class TaskSchedulingController {TaskScheduleManagerService taskScheduleManagerService;@Autowiredpublic void setTaskScheduleManagerService(TaskScheduleManagerService taskScheduleManagerService) {this.taskScheduleManagerService = taskScheduleManagerService;}@PostMapping("/search")@Operation(summary = "分页查询任务")public Response searchData(@RequestBody SearchTaskDto param){return Response.success(taskScheduleManagerService.searchTaskForPage(param));}@GetMapping("/detail")@Operation(summary = "具体任务对象")public Response searchDetail(String taskId){return Response.success(taskScheduleManagerService.searchTaskDetail(taskId));}@GetMapping("/shutdown")@Operation(summary = "关闭指定任务")public Response shutdownTask(String taskId){return Response.success(taskScheduleManagerService.shutdownTask(taskId));}@GetMapping("/open")@Operation(summary = "开启指定任务")public Response openTask(String taskId){return Response.success(taskScheduleManagerService.openTask(taskId));}@GetMapping("/run")@Operation(summary = "运行指定任务")public Response runTask(String taskId){return Response.success(taskScheduleManagerService.runTask(taskId));}@PostMapping("/update")@Operation(summary = "更新指定任务")public Response updateTask(@RequestBody TaskEntity taskEntity){return Response.success(taskScheduleManagerService.updateTaskBusinessInfo(taskEntity));}}
相关接口实现类
@Service
public class TaskScheduleManagerServiceImpl implements TaskScheduleManagerService {private TaskInfoOpMapper taskInfoOpMapper;private TaskScheduleServer taskScheduleServer;@Autowiredpublic void setTaskInfoOpMapper(TaskInfoOpMapper taskInfoOpMapper) {this.taskInfoOpMapper = taskInfoOpMapper;}@Autowiredpublic void setTaskScheduleServer(TaskScheduleServer taskScheduleServer) {this.taskScheduleServer = taskScheduleServer;}@Overridepublic IPage<TaskEntity> searchTaskForPage(SearchTaskDto dto) {Page<TaskEntity> pageParam = new Page<>(1,10);pageParam.setAsc("task_id");return taskInfoOpMapper.queryTaskInfoPage(pageParam,dto.getFilterKey(),dto.getBootUp(),dto.getLastRunStatus());}@Overridepublic TaskEntity searchTaskDetail(String taskId) {if(!StringUtils.isEmpty(taskId)){return taskInfoOpMapper.queryTaskInfoById(taskId);}return null;}@Overridepublic TaskRunRetDto runTask(String taskId) {AbstractBaseCronTask task = taskScheduleServer.getTaskSchedulingRam().get(taskId);TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.run, 0);if(null != task) {double time = taskScheduleServer.runTaskById(taskId);result.setResult(1);return result.extend(time).taskInfo(task.getThisTaskInfo());} else {return result.extend("任务未启用");}}@Overridepublic TaskRunRetDto shutdownTask(String taskId) {AbstractBaseCronTask task = taskScheduleServer.getTaskSchedulingRam().get(taskId);TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.shutdown, 0);if(null != task) {boolean flag = taskScheduleServer.removeTaskFromScheduling(taskId);if(flag) {result.setResult(1);}return result.extend("任务成功关闭").taskInfo(task.getThisTaskInfo());} else {return result.extend("任务未启用");}}@Overridepublic TaskRunRetDto openTask(String taskId) {TaskEntity task = taskInfoOpMapper.queryTaskInfoById(taskId);TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.open, 0);if(null != task) {if (!taskScheduleServer.getTaskSchedulingRam().containsKey(taskId)) {boolean flag = taskScheduleServer.addTaskToScheduling(task);if(flag) {result.setResult(1);}return result.extend("任务开启成功").taskInfo(task);} else {return result.extend("任务处于启动状态").taskInfo(task);}}else {return result.extend("任务不存在!");}}@Overridepublic TaskRunRetDto updateTaskBusinessInfo(TaskEntity entity) {TaskEntity task = searchTaskDetail(entity.getTaskId());TaskRunRetDto result = new TaskRunRetDto(TaskRunRetDto.TaskOperation.update, 0).taskInfo(entity);String config = entity.getTaskOutConfig();if(null != config && !JSONUtil.isJson(config) && !JSONUtil.isJsonArray(config)){result.setResult(0);result.extend("更新任务失败,任务配置必须为JSON或空");result.taskInfo(entity);return result;}task.setTaskCron(entity.getTaskCron());task.setTaskOutConfig(entity.getTaskOutConfig());task.setTaskName(entity.getTaskName());task.setTaskDesc(entity.getTaskDesc());int num = taskInfoOpMapper.updateTaskInfo(task);if (num == 1) {result.setResult(1);result.extend("成功更新任务");result.taskInfo(entity);//重新刷新任务taskScheduleServer.removeTaskFromScheduling(entity.getTaskId());taskScheduleServer.addTaskToScheduling(task);}return result;}
效果
数据库中配置任务

任务代码
public class TestTask extends AbstractBaseCronTask {public TestTask(TaskEntity taskEntity) {super(taskEntity);}@Overridepublic void beforeJob() {log.info("测试任务开始");}@Overridepublic void startJob() {}@Overridepublic void afterJob() {}
}
任务查看

执行效果

相关文章:
基于SpringBoot实现轻量级的动态定时任务调度
在使用SpringBoot框架进行开发时,一般都是通过Scheduled注解进行定时任务的开发: Component public class TestTask {Scheduled(cron"0/5 * * * * ? ") //每5秒执行一次public void execute(){SimpleDateFormat df new SimpleDateFormat(…...
夸克升级“超级搜索框” 推出AI搜索为中心的一站式AI服务
大模型时代,生成式AI如何革新搜索产品?阿里智能信息事业群旗下夸克“举手答题”。7月10日,夸克升级“超级搜索框”,推出以AI搜索为中心的一站式AI服务,为用户提供从检索、创作、总结,到编辑、存储、分享的一…...
element-ui el-select选择器组件下拉框增加自定义按钮
element-ui el-select选择器组件下拉框增加自定义按钮 先看效果 原理:在el-select下添加禁用的el-option,将其value绑定为undefined,然后覆盖el-option禁用状态下的默认样式即可 示例代码如下: <template><div class…...
Python基于you-get下载网页上的视频
1.python 下载地址 下载 : https://www.python.org/downloads/ 2. 配置环境变量 配置 python_home 地址 配置 python_scripts 地址 在path 中加入对应配置 3. 验证 C:\Users>python --version Python 3.12.4C:\Users>wheel version wheel 0.43.04. 下载 c…...
大模型/NLP/算法面试题总结3——BERT和T5的区别?
1、BERT和T5的区别? BERT和T5是两种著名的自然语言处理(NLP)模型,它们在架构、训练方法和应用场景上有一些显著的区别。以下是对这两种模型的详细比较: 架构 BERT(Bidirectional Encoder Representation…...
vue3项目打包的时候,怎么区别测试环境,和本地环境
在Vue 3项目中区别测试环境和本地环境,并标记接口的方法可以通过环境变量来实现。 首先,你可以在你的项目根目录下创建一个.env文件,并定义你的环境变量。比如,你可以创建.env.local作为本地环境的配置文件,.env.test…...
小特性 大用途 —— YashanDB JDBC驱动的这些特性你都get了吗?
在现代数据库应用场景中,系统的高可用性和负载均衡是确保服务稳定性的基石。YashanDB JDBC驱动通过其创新的多IP配置特性,为用户带来了简洁而强大的解决方案,以实现数据库连接的高可用性和负载均衡,满足企业级应用的高要求。 01 …...
全网最全的软件测试面试八股文
前面看到了一些面试题,总感觉会用得到,但是看一遍又记不住,所以我把面试题都整合在一起,都是来自各路大佬的分享,为了方便以后自己需要的时候刷一刷,不用再到处找题,今天把自己整理的这些面试题…...
VMware虚拟机配置桥接网络
转载:虚拟机桥接网络配置 一、VMware三种网络连接方式 VMware提供了三种网络连接方式,VMnet0, VMnet1, Vmnet8,分别代表桥接,Host-only及NAT模式。在VMware的编辑-虚拟网络编辑器可看到对应三种连接方式的设置(如下图…...
华为机考真题 -- 攀登者1
题目描述: 攀登者喜欢寻找各种地图,并且尝试攀登到最高的山峰。地图表示为一维数组,数组的索引代表水平位置,数组的元素代表相对海拔高度。其中数组元素0代表地面。 一个山脉可能有多座山峰(山峰定义:高度大于相邻位置的高度,或在地图边界且高度大于相邻的高度)。登山者…...
深入理解Python密码学:使用PyCrypto库进行加密和解密
深入理解Python密码学:使用PyCrypto库进行加密和解密 引言 在现代计算领域,信息安全逐渐成为焦点话题。密码学,作为信息保护的关键技术之一,允许我们加密(保密)和解密(解密)数据。P…...
MMSegmentation笔记
如何训练自制数据集? 首先需要在 mmsegmentation/mmseg/datasets 目录下创建一个自制数据集的配置文件,以我的苹果叶片病害分割数据集为例,创建了mmsegmentation/mmseg/datasets/appleleafseg.py 可以看到,这个配置文件主要定义…...
Python基础语法:变量和数据类型详解(整数、浮点数、字符串、布尔值)①
文章目录 变量和数据类型详解(整数、浮点数、字符串、布尔值)一、变量二、数据类型1. 整数(int)2. 浮点数(float)3. 字符串(str)4. 布尔值(bool) 三、类型转换…...
【C++航海王:追寻罗杰的编程之路】关联式容器的底层结构——红黑树
目录 1 -> 红黑树 1.1 -> 红黑树的概念 1.2 -> 红黑树的性质 1.3 -> 红黑树节点的定义 1.4 -> 红黑树的结构 1.5 -> 红黑树的插入操作 1.6 -> 红黑树的验证 1.8 -> 红黑树与AVL树的比较 2 -> 红黑树模拟实现STL中的map与set 2.1 -> 红…...
MySQL DDL
数据库 1 创建数据库 CREATE DATABASE 数据库名 CREATE DATABASE IF NOT EXISTS 数据库名;(判断是否存在) CREATE DATABASE 数据库名 CHARACTER SET 字符 2 查看数据库 SHOW DATABASES; 查看某个数据库的信息 SHOW CAEATE DATABASE 数据库名 3 修改数据库 …...
从模型到应用:李彦宏解读AI时代的新趋势与挑战
如何理解李彦宏说的“不要卷模型,要卷应用” 开源项目的机遇与挑战 7月4日,2024世界人工智能大会暨人工智能全球治理高级别会议在上海世博中心举办。在产业发展主论坛上,百度创始人、董事长兼首席执行官李彦宏呼吁:“大家不要卷…...
C++ STL 随机数用法介绍
目录 一:C语言中的随机数 二:C中的随机数 1. 生成随机数的例子 2. 随机数引擎 3. 随机数引擎适配器 4. C中预定义的随机数引擎,引擎适配器 5. 随机数分布 一:C语言中的随机数 <stdlib.h>//初始化随机种子 srand(static_ca…...
容器之docker compose
Docker Compose 是一个用于定义和运行多容器 Docker 应用的工具。通过一个 YAML 文件,您可以配置应用程序需要的所有服务,并使用单个命令来创建和启动这些服务。以下是对 Docker Compose 的详细介绍: 核心概念 服务(Services&am…...
MIT机器人运动控制原理浅析-人形机器人
MIT人形机器人基于开发改进的执行器全新设计,通过可感知执行器运动动力学移动规划器(Actuator-Aware Kino-Dynamic Motion Planner)及着地控制器(Landing Controller)等实现机器人的运动控制。 机器人设计 机器人高0.7米,21KG(四肢重量 25%),…...
开源 WAF 解析:选择最适合你的防护利器
前言 随着网络安全风险的增加,Web 应用防火墙(WAF)成为保护网站和应用程序免受攻击的关键工具。在众多的选择中,开源 WAF 以其灵活性、可定制性和成本效益备受青睐。本文将深入探讨几种主流开源 WAF 解决方案,帮助你选…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...
elementUI点击浏览table所选行数据查看文档
项目场景: table按照要求特定的数据变成按钮可以点击 解决方案: <el-table-columnprop"mlname"label"名称"align"center"width"180"><template slot-scope"scope"><el-buttonv-if&qu…...
小木的算法日记-多叉树的递归/层序遍历
🌲 从二叉树到森林:一文彻底搞懂多叉树遍历的艺术 🚀 引言 你好,未来的算法大神! 在数据结构的世界里,“树”无疑是最核心、最迷人的概念之一。我们中的大多数人都是从 二叉树 开始入门的,它…...
