Springboot集成Quartz
Quartz简介
Job 表示一个工作,要执行的具体业务内容。
JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
Trigger 代表一个调度参数的配置,什么时候去调。
Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。
案例
背景
系统中有一张自定义的定时任务表,需要实现动态的添加、修改、删除、启停定时任务等功能,定时任务里包含了业务需要执行的按设置周期执行的代码
由于不想使用Quartz的数据存储功能,所以下面实现里直接使用了这张自定义的表,以及使用了Quartz的任务调度和触发功能
实现步骤
1. 引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId><version>2.7.18</version>
</dependency>
2. 添加配置类
@Configuration
public class ScheduleQuartzConfig {@Beanpublic Scheduler scheduler() throws SchedulerException {Scheduler scheduler = schedulerFactoryBean().getScheduler();return scheduler;}@Beanpublic SchedulerFactoryBean schedulerFactoryBean() {Properties properties = new Properties();properties.setProperty("org.quartz.threadPool.threadCount", "10");properties.setProperty("org.quartz.threadPool.threadNamePrefix","quartz_worker");SchedulerFactoryBean factory = new SchedulerFactoryBean();factory.setSchedulerName("QUARTZ_SCHEDULER");factory.setQuartzProperties(properties);return factory;}
}
3. 编写Job类
该类就是将来要定时执行的业务代码,具体代码路径根据实际情况规划即可,重点是继承 QuartzJobBean
,重写 executeInternal
方法
//锁定机制,以确保在同一时间只有一个任务实例运行
@DisallowConcurrentExecution
public class MyJob extends QuartzJobBeanprivate final static Logger logger = LoggerFactory.getLogger(PushPvImitateDataJob.class);@Overrideprotected void executeInternal(JobExecutionContext context) throws JobExecutionException {//业务代码logger.info("开始执行业务代码了。。。");try {Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();} logger.info("业务代码执行完成了!!!!");}
}
4. 编写定时任务表的实体类
ScheduleLog.java
@Data
public class ScheduleLog {private Integer id;private String name; //任务名称private String jobClassName; //任务的实现类 如:xxx.xxx.xx.MyJobprivate String cronExpression; //触发时机表达式,比如每5秒执行一次 0/5 * * * * ?private Integer status; //状态:0 启动 1 禁用private String remark;private Long createTime;private Long updateTime;private Long lastTime; //上一次执行时间private Long nextTime; //下一次执行时间private String lastTimeText;private String nextTimeText;
}
5. 创建管理定时任务的工具类
@Component
public class ScheduleQuartzManage {private static final Logger logger = LoggerFactory.getLogger(ScheduleQuartzManage.class);/*** 创建定时任务 定时任务创建之后默认启动状态** @param scheduler: 调度器* @param scheduleLog: 报告订阅对象* @return: void**/public void createScheduleJob(Scheduler scheduler, ScheduleLog scheduleLog) throws SchedulerException {//获取到定时任务的执行类 必须是类的绝对路径名称//定时任务类需要是job类的具体实现 QuartzJobBean是job的抽象类。
// Class<? extends Job> jobClass = PushPvImitateDataJob.class;Class<? extends Job> jobClass = null;try {jobClass = (Class<? extends Job>) Class.forName(scheduleLog.getJobClassName());} catch (ClassNotFoundException e) {e.printStackTrace();}// 构建定时任务信息JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(scheduleLog.getId().toString(),"jobGroup").usingJobData("id",scheduleLog.getId()).build();// 设置定时任务执行方式CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleLog.getCronExpression());// 构建触发器trigger// 如果已经有下一次时间,就设置为下一次时间为触发时间CronTrigger trigger;if (!Objects.isNull(scheduleLog.getNextTime())) {Date date = new Date(scheduleLog.getNextTime());trigger = TriggerBuilder.newTrigger().startAt(date).withIdentity(scheduleLog.getId().toString(), "jobGroup").withSchedule(scheduleBuilder).build();} else {trigger = TriggerBuilder.newTrigger().withIdentity(scheduleLog.getId().toString(),"jobGroup").withSchedule(scheduleBuilder).build();}scheduler.scheduleJob(jobDetail, trigger);// 设置下次执行时间//long nextTime = trigger.getNextFireTime().getTime();//LocalDateTime nextTime = DateUtil.dateToLocalDate(trigger.getNextFireTime());//logger.info("下次一执行时间:{}",DateUtil.formatDateTime(new Date(nextTime)));//scheduleLog.setNextTime(nextTime);}/*** 根据任务名称暂停定时任务** @param scheduler 调度器* @param jobName 定时任务名称(这里直接用ReportSubscribePO的Id)* @throws SchedulerException*/public void pauseScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName, "jobGroup");scheduler.pauseJob(jobKey);}/*** 根据任务名称恢复定时任务** @param scheduler 调度器* @param scheduleLog 定时任务名称(这里直接用ReportSubscribePO的Id)* @throws SchedulerException*/public void resumeScheduleJob(Scheduler scheduler, ScheduleLog scheduleLog) throws SchedulerException {// 判断当前任务是否在调度中Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.groupEquals("jobGroup"));List<JobKey> thisNameJobs = jobKeys.stream().filter(jobKey -> Objects.equals(scheduleLog.getId().toString(), jobKey.getName())).collect(Collectors.toList());if (thisNameJobs.size() > 0){JobKey jobKey = JobKey.jobKey(scheduleLog.getId().toString(), "jobGroup");scheduler.resumeJob(jobKey);// 下一次执行时间设置回去Trigger trigger = scheduler.getTrigger(TriggerKey.triggerKey(scheduleLog.getId().toString(), "jobGroup"));long nextTime = trigger.getNextFireTime().getTime();scheduleLog.setNextTime(nextTime);}else {createScheduleJob(scheduler, scheduleLog);}}/*** 更新定时任务** @param scheduler 调度器* @param scheduleLog 报告订阅对象* @throws SchedulerException*/public void updateScheduleJob(Scheduler scheduler, ScheduleLog scheduleLog) throws SchedulerException {//获取到对应任务的触发器TriggerKey triggerKey = TriggerKey.triggerKey(scheduleLog.getId().toString(),"jobGroup");//设置定时任务执行方式CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleLog.getCronExpression());//重新构建任务的触发器triggerCronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);if (trigger == null){return;}// trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();trigger = TriggerBuilder.newTrigger().startNow().withIdentity(scheduleLog.getId().toString(), "jobGroup").withSchedule(scheduleBuilder).build();//重置对应的jobscheduler.rescheduleJob(triggerKey, trigger);scheduleLog.setNextTime(trigger.getNextFireTime().getTime());}/*** 根据定时任务名称从调度器当中删除定时任务** @param scheduler 调度器* @param jobName 定时任务名称 (这里直接用 ScheduleLog 的Id)* @throws SchedulerException*/public void deleteScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {JobKey jobKey = JobKey.jobKey(jobName,"jobGroup");scheduler.deleteJob(jobKey);}
}
6. 项目启动时做任务初始化
这里我直接让service实现了CommandLineRunner,然后在run()方法中,将初始化逻辑写入进来,让数据库中的持久化的任务全部添加进内存中。
@Service
public class ScheduleLogServiceImpl implements ScheduleLogService, CommandLineRunnerprivate final static Logger logger = LoggerFactory.getLogger(ScheduleLogServiceImpl.class);@Autowiredprivate ScheduleLogDao scheduleLogDao;@Autowiredprivate ScheduleQuartzManage scheduleQuartzManage;@Autowiredprivate Scheduler scheduler; //这里的scheduler来源于配置@Overridepublic void run(String... args) throws Exception {// 初始化所有的已经启用的订阅List<ScheduleLog> enableSubs = getEnableScheduleList();logger.info("需要初始化的任务个数:{}", enableSubs.size());for (ScheduleLog sub : enableSubs) {try {logger.info("开始初始化订阅任务,任务name:{}", sub.getName());scheduleQuartzManage.createScheduleJob(scheduler, sub);} catch (Exception e) {logger.error("启动时初始化订阅任务失败:{}", e.getMessage());}}}@Overridepublic List<ScheduleLog> getEnableScheduleList() {HashMap<String, Object> param = new HashMap<>();param.put("status",1);List<ScheduleLog> list = scheduleLogDao.getList(param);return list;}@Overridepublic boolean add(Map data) {data.put("createTime", DateUtil.currentSeconds());Integer add = scheduleLogDao.add(data);if(add > 0){//创建调度器data.put("id",add);ScheduleLog scheduleLog = BeanUtil.mapToBean(data, ScheduleLog.class,false,null);if(scheduleLog.getStatus() < 1){return true;}try {scheduleQuartzManage.createScheduleJob(scheduler, scheduleLog);} catch (SchedulerException e) {e.printStackTrace();}return true;}return false;}@Overridepublic boolean del(Integer id) {Integer del = scheduleLogDao.del(id);if(del > 0){try {scheduleQuartzManage.deleteScheduleJob(scheduler,String.valueOf(id));} catch (SchedulerException e) {e.printStackTrace();}return true;}return false;}@Overridepublic boolean edit(Map data) {data.put("updateTime",DateUtil.currentSeconds());Integer save = scheduleLogDao.save(data);if(save > 0){try {ScheduleLog scheduleLog = getDetail((Integer) data.get("id"));if(data.get("status").equals(0)){//关闭scheduleQuartzManage.deleteScheduleJob(scheduler,data.get("id").toString());}else{//开启scheduleQuartzManage.deleteScheduleJob(scheduler,data.get("id").toString());scheduleQuartzManage.createScheduleJob(scheduler,scheduleLog);}} catch (SchedulerException e) {e.printStackTrace();}return true;}return false;}
}
至此基本就实现了定时任务的管理了,controller 里的内容包含了对定时任务进行管理的接口,就不写了
总结
- 同一个任务是否可以并行执行,可参考第3步设置
- 每次项目重新部署后,自动加载数据库中的定时任务
- Quartz在发生异常时会重试一次,注意异常处理,可在第3步中处理
参考链接:https://juejin.cn/post/7054762566035193869#heading-7
Cron表达式
不管是Spring自带的定时任务实现,还是SpringBoot整合Quartz的定时任务实现,其触发器都支持用corn表达式来表示。
corn表达式是一个字符串,有6或7个域,域之间是用空格进行间隔。
从左到右每个域表示的含义如下:
第几个域 | 英文释义 | 允许值 | 备注 |
---|---|---|---|
一 | Seconds | 0~59 | 秒 |
二 | Minutes | 0~59 | 分 |
三 | Hours | 0~23 | 时 |
四 | DayOfMonth | 1-31 | 天 |
五 | Month | 1-12或月份简写 | 月 |
六 | DayOfWeek | 1-7或星期简写 | 星期,1表示SUN,在day-of-week字段用”6#3”指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发 |
七 | Year | 1970~2099 | 年 |
然后,某些域还支持部分特殊字符,特殊字符的含义如下:
特殊字符 | 含义及注意事项 |
---|---|
* | 任意值 |
? | 占位符,只能在第四域和第六域中使用,表示未说明的值,即不关心它为何值 |
- | 区间,表示区间内有效 |
/ | 固定间隔,符号前表示开始时间,符号后表示每次递增的值; |
, | 枚举有效值的间隔符,表示附加一个可能值 |
L | 表示该区间的最后一个有效值,只能在第四域和第六域中使用, L(“last”) (“last”) “L” 用在day-of-month字段意思是 “这个月最后一天”;用在 day-of-week字段, 它简单意思是 “7” or “SAT”。 如果在day-of-week字段里和数字联合使用,它的意思就是 “这个月的最后一个星期几” – 例如: “6L” means “这个月的最后一个星期五”. 当我们用“L”时,不指明一个列表值或者范围是很重要的,不然的话,我们会得到一些意想不到的结果。 |
W | 表示离指定日期的最近的有效工作日,(周一-周五为工作日),W(“weekday”) 只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发,注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日。 |
常用corn表达式例子含义说明:
corn表示式 | 表达式含义 |
---|---|
*/10 * * * * ? * | 每隔10秒执行一次 |
0 30 1 * * ? * | 每天凌晨1点30分0秒开始执行 |
0 0 10,14,16 * * ? | 每天10点、14点、16点执行一次 |
0 15 10 L * ? | 每个月最后一天的10点15分执行一次 |
0 15 10 ? * 6L | 每月的最后一个星期五上午10:15触发 |
0 15 10 15 * ? | 每月15日上午10:15触发 |
0 15 10 ? * 6#3 | 每月的第三个星期五上午10:15触发 |
0 15 10 ? * 6L 2018-2020 | 2018年到2020年每个月最后一个星期五的10:15执行 |
校验地址:https://www.bejson.com/othertools/cronvalidate/
相关文章:

Springboot集成Quartz
Quartz简介 Job 表示一个工作,要执行的具体业务内容。 JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。 Trigger 代表一个调度参数的配置…...

Android面试题之Kotlin Jetpack组件LifecycleScope
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点 在Kotlin中,LifecycleScope是Android Jetpack架构组件的一部分,主要用于简化与生命周期相关的协程管理。 它属于android…...
MySQL深分页优化
MySQL中的深分页问题通常是指当我们通过LIMIT语句查询数据,尤其是在翻到较后面的页码时,性能会急剧下降。例如,查询第1000页的数据,每页10条,系统需要跳过前9990条数据,然后才能获取到所需的记录࿰…...

问题:律师会见委托人的方式包括团体会见和( )。 #职场发展#笔记#学习方法
问题:律师会见委托人的方式包括团体会见和( )。 参考答案如图所示...

Spring Boot中整合Jasypt 使用自定义注解+AOP实现敏感字段的加解密
😄 19年之后由于某些原因断更了三年,23年重新扬帆起航,推出更多优质博文,希望大家多多支持~ 🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志 🎐 个人CSND主页——Mi…...
pytorch中的维度变换操作性质大总结:view, reshape, transpose, permute
在深度学习中,张量的维度变换是很重要的操作。在pytorch中,有四个用于维度变换的函数,view, reshape, transpose, permute。其中view, reshape都用于改变张量的形状,transpose, permute都用于重新排列张量的维度,但它们…...

LeetCode刷题 | Day 4 分割等和子集(Partition Equal Subset Sum)自底向上动态规划
LeetCode刷题 | Day 4 分割等和子集(Partition Equal Subset Sum)自底向上动态规划 文章目录 LeetCode刷题 | Day 4 分割等和子集(Partition Equal Subset Sum)自底向上动态规划前言一、题目概述二、解题方法2.1 一维表格的自底向上动态规划2.1.1 思路讲解2.1.2 伪代码 + 逐…...

基于工业互联网打造敏捷供应链的实现方式:创新路径与实践应用
引言 工业互联网和敏捷供应链是当今制造业发展中的两个重要概念。工业互联网以数字化、网络化和智能化为核心,致力于将传统工业生产与互联网技术相融合,从而实现生产过程的高效、智能和灵活。而敏捷供应链则强调快速响应市场需求、灵活调整生产和供应计划…...

碳化硅柱式膜的广泛应用
碳化硅柱式膜是一种高性能的过滤材料,以其独特的性质和广泛的应用领域在现代工业中占据着重要地位。以下是对碳化硅柱式膜的详细介绍: 一、基本概述 碳化硅柱式膜是以碳化硅超滤膜为过滤单元构成的,其过滤精度高达0.1微米。这种膜材料具有耐化…...
【QT】QFont字体设置
设置字体大小 f.setPointSize(12); // 设置字体大小为12点设置字体加粗 f.setBold(true); // 使字体加粗设置字体斜体 f.setItalic(true); // 使字体斜体设置字体下划线 f.setUnderline(true); // 给字体添加下划线设置字体删除线 f.setStrikeOut(true); // 给字体添加删除…...

Vue3+vite部署nginx的二级目录,使用hash模式
修改router访问路径 import { createRouter, createWebHashHistory } from vue-routerconst router createRouter({history: createWebHashHistory (/mall4pc-bbc/),routes: [XXX,] })配置package.json文件 "build:testTwo": "vite build --mode testing --ba…...

云南区块链商户平台发票助手成品
目录 1 概述2 功能对比3 项目演示图4 核心逻辑4.1智能赋码4.2 解密方法4.3 登录与检测4.4 发票金额大写转换4.5 检查登录是否失效4.6 验证码识别5 演示效果6 项目部署6.1 Web站点部署6.1.1 环境6.1.2 前端6.1.3 后端6.2 Docker部署6.2.1 构建镜像6.2.2 创建容器6.3.3 访问项目域…...

AI图书推荐:检索增强生成RAG赋能大语言模型
本书《检索增强生成RAG赋能大型语言模型》(Retrieval-Augmented Generation - Dr. Ray Islam :Mohammad Rubyet )深入探讨了如何通过结合检索系统与神经语言模型,提升人工智能在自然语言处理领域的能力。 以下是各章节内容的概要&…...

高效学习LabVIEW的方法
学习LabVIEW可以通过系统化课程、在线资源、自学实验、参与论坛、结合实际项目等多角度进行。系统课程提供全面基础,在线资源便于查漏补缺,自学实验强化理解,论坛互动解决疑难,结合实际项目应用提高实践技能。结合项目学习是最高效…...

C语言 | Leetcode C语言题解之第136题只出现一次的数字
题目: 题解: class Solution { public:vector<int> singleNumbers(vector<int>& nums) {int eor 0;for (int num:nums)eor ^ num;int rightOne eor & (~eor 1); // 提取出最右的1int onlyOne 0;for (int cur : nums) {if ((cur…...

如何利用Varjo混合现实技术改变飞机维修训练方式
自2017年以来,总部位于休斯顿的HTX实验室一直在推进混合现实技术,与美国空军密切合作,通过其EMPACT平台提供可扩展的沉浸式飞机维护虚拟现实培训。 虚拟和混合现实对维修训练的好处: l 实践技能:提供一个非常接近真实场…...
C++:按指定字符分割字符串
按指定字符分割字符串 [C]对string按指定分隔符分割(split) #include <iostream> #include <vector> #include <string> #include <sstream> using namespace std; int main() {string origin_str "hello world !"; // 需要进行分割的字符串…...

网络网络层之(6)ICMPv4协议
网络网络层之(6)ICMPv4协议 Author: Once Day Date: 2024年6月2日 一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦… 漫漫长路,有人对你微笑过嘛… 全系列文章可参考专栏: 通信网络技术_Once-Day的博客-CS…...
Opengrok代码在线查看平台
OpenGrok 是一个基于 Web 的源代码搜索引擎和交叉引用工具,它可以用来浏览和搜索代码库。虽然 OpenGrok 提供了代码搜索、查看文件和历史等功能,但它本身不是一个完整的在线集成开发环境(IDE)。然而,OpenGrok 可以作为…...

济南适宜地提取
题目: 网上下载中国的DEM、土地利用地图(1980、2000、2015年的)和一张最新济南市行政区划 图(要求:莱芜市并入济南后的区划图); 2.网上下载中国2015年年平均降水空间插值数据;3..网上下载中国2015年年平均气温空间插值数据; (注:以上数据可到资源环境科学与数据中心下载http…...
基于大模型的 UI 自动化系统
基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...

以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...

解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...
LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用
中达瑞和自2005年成立以来,一直在光谱成像领域深度钻研和发展,始终致力于研发高性能、高可靠性的光谱成像相机,为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...
HTML前端开发:JavaScript 获取元素方法详解
作为前端开发者,高效获取 DOM 元素是必备技能。以下是 JS 中核心的获取元素方法,分为两大系列: 一、getElementBy... 系列 传统方法,直接通过 DOM 接口访问,返回动态集合(元素变化会实时更新)。…...