个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道
如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充
前言
本文只做基于SpringBoot的示例,其余版本的请自行查阅资料,大同小异
1.介绍
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:
- 持久性作业 - 就是保持调度定时的状态;
- 作业管理 - 对调度作业进行有效的管理;
使用quartz的调度器统一管理定时任务,可以动态的添加、删除、暂停等
推荐几个博客,写的真的挺好
2.使用步骤
2.1.4静态定时器
静态定时器即主要使用代码控制,一旦项目部署将无法对定时器做出调整(增删改),适用于强制执行而不需要灵活变通的任务
-
添加quartz依赖
<!-- 定时器依赖 --> <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency>
-
创建需要执行的任务类MyTask,必须继承并实现QuartzJobBean
public class MyTask extends QuartzJobBean { /** * 计数器 */ private static Integer count = 0; /** * 执行内容 * * @param jobExecutionContext 上下文 */ @Override protected void executeInternal(JobExecutionContext jobExecutionContext) { JobDetail jobDetail = jobExecutionContext.getJobDetail(); Trigger trigger = jobExecutionContext.getTrigger(); synchronized (count) { System.out.println("" + new Date() //打印时间 + " :" + count //打印计时器 + " jobDetailKey : " + jobDetail.getKey().toString() //打印jobDetail的key + " jobDetailData : " + jobDetail.getJobDataMap().getString("jobName") //打印来自jobDetail的参数 + " triggerKey : " + trigger.getKey().toString() //打印trigger的key + " triggerData : " + trigger.getJobDataMap().getString("triggerName") //打印来自trigger的参数 ); count++; } } }
-
创建quartz配置文件QuartzConfig.java
- 定义业务组件
myJob1()
,配置需要执行的任务类、业务自定义身份、转递给任务的参数等等 - 定义触发器
myTrigger1()
,配置关联的业务组件、触发器自定义身份、转递给任务的参数等等 - 别忘了使用
@Configuration
标记配置文件类、@Bean
标记业务组件和触发器
@Configuration public class QuartzConfig { @Bean public JobDetail myJob1() { JobDetail jobDetail = JobBuilder.newJob(MyTask.class) .withIdentity("myJob1", "myJobGroup1") //JobDataMap可以给任务execute传递参数,且可传递多个 .usingJobData("jobName", "myJob1") .storeDurably() .build(); return jobDetail; } @Bean public Trigger myTrigger1() { Trigger trigger = TriggerBuilder.newTrigger() .forJob(myJob1()) .withIdentity("myTrigger1", "myTriggerGroup1") //JobDataMap可以给任务execute传递参数,且可传递多个 .usingJobData("triggerName", "myTrigger1") .startNow() //.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) .withSchedule(CronScheduleBuilder.cronSchedule("*/3 * * * * ?")) .build(); return trigger; } @Bean public Trigger myTrigger2() { Trigger trigger = TriggerBuilder.newTrigger() .forJob(myJob1()) .withIdentity("myTrigger2", "myTriggerGroup1") .usingJobData("triggerName", "myTrigger2") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()) // .withSchedule(CronScheduleBuilder.cronSchedule("*/3 * * * * ?")) .build(); return trigger; } }
- 定义业务组件
执行结果如下:
- myTrigger1和myTrigger2均使用业务组件myJob1
- myTrigger1每隔3秒触发一次
- myTrigger2每隔5秒触发一次
2.2.动态定时器
动态定时器即可以对定时器动态进行调整,通常由前端页面和数据库配合,从而灵活调整定时器
因为需要动态管理,那么数据必须保证持久化,否则重新启动项目就什么都不剩了,那肯定不得行
添加quartz依赖
<!-- 定时器依赖 -->
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
持久化配置文件
写入配置文件即可,仅编写quartz相关部分,其余按照自己需求即可,不影响
推荐下面两篇博文,介绍的很详细,此处只做简单实现
spring:
quartz:
#相关属性配置
properties:
org:
quartz:
scheduler:
instanceName: quartzScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: false
clusterCheckinInterval: 10000
useProperties: false
misfireThreshold : 5000
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
#数据库方式
job-store-type: JDBC
#初始化表结构
jdbc:
initialize-schema: NEVER
建表
表需要依照quartz官方给出的实例,可以到http://www.quartz-scheduler.org/downloads/下载后解压,将目录
\src\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql
里的语句在数据库直接执行即可嫌卡或者懒得可以在这个地址获取2.3.0版本https://gitee.com/echo_ye/assets/blob/master/doc/tables_mysql_innodb.sql
数据封装类和查询方法
数据封装类需要包括前后端传递的所有参数,查询则需要向quartz相关的表中查询
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yezi_tool.demo_basic.mapper.JobMapper">
<select id="listJob" resultType="com.yezi_tool.demo_basic.entity.QuartzJob">
SELECT
job.JOB_NAME as jobName,
job.JOB_GROUP as jobGroup,
job.DESCRIPTION as description,
job.JOB_CLASS_NAME as jobClassName,
cron.CRON_EXPRESSION as cronExpression,
tri.TRIGGER_NAME as triggerName,
tri.TRIGGER_STATE as triggerState,
job.JOB_NAME as oldJobName,
job.JOB_GROUP as oldJobGroup
FROM qrtz_job_details AS job
LEFT JOIN qrtz_triggers AS tri ON job.JOB_NAME = tri.JOB_NAME
LEFT JOIN qrtz_cron_triggers AS cron ON cron.TRIGGER_NAME = tri.TRIGGER_NAME
WHERE tri.TRIGGER_TYPE = 'CRON'
</select>
</mapper>
package com.yezi_tool.demo_basic.mapper;
import com.yezi_tool.demo_basic.entity.QuartzJob;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface JobMapper {
List<QuartzJob> listJob();
}
package com.yezi_tool.demo_basic.entity;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class QuartzJob {
private String jobName;//任务名称
private String jobGroup;//任务分组
private String description;//任务描述
private String jobClassName;//执行类
private String cronExpression;//执行时间
private String triggerState;//任务状态
private List<Map<String,Object>> dataMap;//参数
private List<Map<String, Object>> jobDataParam;//备用数据域
public QuartzJob() {
super();
}
public QuartzJob(String jobName, String jobGroup, String description, String jobClassName, String cronExpression) {
super();
this.jobName = jobName;
this.jobGroup = jobGroup;
this.description = description;
this.jobClassName = jobClassName;
this.cronExpression = cronExpression;
}
}
执行的任务类
与静态的任务类相同,可以建立多个,但是最好保证在同一个目录下,方便实例化的时候确认路径
本案例的任务类放置在目录
com.yezi_tool.demo_basic.test
下
服务层方法
控制层为主要代码,在这里对定时任务进行增删改查等操作
因为懒就不写接口实现了。。。实际使用中请自己补上
重点为创建定时任务的方法。步骤如下
- 检查任务是否已存在,若是,则删除任务
- 根据任务类的名字(路径固定),实例化任务类task
- 创建定时任务job,设置目标任务类、job名、分组名、传递给任务的数据、任务描述等参数
- 创建触发器trigger,设置触发器的名字、分组、触发时间、传递给任务的数据等参数
- 通过调度程序schedule将job和trigger进行绑定并启动
也可在trigger中绑定job,那么schedule直接启动trigger即可
@Service
public class QuartzService {
public String QUARTZ_TASK_PATH_HEAD = "com.yezi_tool.demo_basic.test" + ".";
private final Scheduler scheduler;
private final JobMapper jobMapper;
public QuartzService(Scheduler scheduler, JobMapper jobMapper) {
this.scheduler = scheduler;
this.jobMapper = jobMapper;
}
public List<QuartzJob> list(String jobName) {
List<QuartzJob> list = jobMapper.listJob();
list.forEach(m -> {
//只查询类名,无视路径
m.setJobClassName(m.getJobClassName().replace(QUARTZ_TASK_PATH_HEAD, ""));
});
return list;
}
/**
* 保存定时任务
*
* @param quartzJob 定时任务类
* @throws Exception 抛出异常
*/
public void save(QuartzJob quartzJob) throws Exception {
try {
//组装参数
JobKey jobKey = new JobKey(quartzJob.getJobName(), quartzJob.getJobGroup());
JobDataMap jobDataMap = new JobDataMap();
if (quartzJob.getDataMap() != null) {
for (Map<String, Object> map : quartzJob.getDataMap())
jobDataMap.putAll(map);
}
//删除旧的job
if (scheduler.checkExists(jobKey)) {
scheduler.deleteJob(jobKey);
}
//构建新的job
Class clazz = Class.forName(QUARTZ_TASK_PATH_HEAD + quartzJob.getJobClassName()); //实例化目标任务类
clazz.getDeclaredConstructor().newInstance();
JobDetail jobDetail = JobBuilder
.newJob(clazz)//设置目标任务类
.withIdentity(quartzJob.getJobName(), quartzJob.getJobGroup())//设置job名和分组名
.usingJobData("jobName", quartzJob.getJobName())//传递的参数,可以自定义,可传递多组,以map形式传递
// .usingJobData(new JobDataMap(quartzJob.getDataMap()))
.withDescription(quartzJob.getDescription())//设置描述,可为空
.build();
//构建新的触发器
Trigger trigger = TriggerBuilder
.newTrigger()
// .forJob(jobDetail)//目标job,这里不设置,如果设置了将会自动触发
.withIdentity(quartzJob.getJobName() + "Trigger", quartzJob.getJobGroup())//设置job名和分组名
.usingJobData("triggerName", quartzJob.getJobName() + "Trigger")//传递的参数,可以自定义,可传递多组,以map形式传递
.usingJobData(jobDataMap)
.withSchedule(CronScheduleBuilder
.cronSchedule(quartzJob.getCronExpression().trim())
.withMisfireHandlingInstructionDoNothing()
)
.startNow()//现在就启动
// .startAt(new Date())//启动时间,可以自定义,与startNow冲突
.build();
//通过schedule触发
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
throw new BaseException("保存定时任务失败");
} catch (ClassNotFoundException e) {
throw new BaseException("任务目标类不存在");
}
}
/**
* 启动任务
*
* @param jobName 任务名
* @param jobGroup 任务分组
* @param dataMap 数据,可为空
* @throws Exception 相关异常
*/
public void trigger(String jobName, String jobGroup, List<Map<String, Object>> dataMap) throws Exception {
//组装参数
JobKey jobKey = new JobKey(jobName, jobGroup);
JobDataMap jobDataMap = new JobDataMap();
if (dataMap != null) {
for (Map<String, Object> map : dataMap)
jobDataMap.putAll(map);
}
//启动触发器
try {
scheduler.triggerJob(jobKey, jobDataMap);
} catch (SchedulerException e) {
throw new BaseException("启动定时任务失败");
}
}
/**
* 暂停任务
*
* @param jobName 任务名
* @param jobGroup 任务分组
* @throws Exception 相关异常
*/
public void pause(String jobName, String jobGroup) throws Exception {
//组装参数
// JobKey jobKey = new JobKey(jobName, jobGroup);
TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", jobGroup);
//暂停触发器
try {
// scheduler.pauseJob(jobKey);
scheduler.pauseTrigger(triggerKey);
} catch (SchedulerException e) {
throw new BaseException("暂停定时任务失败");
}
}
/**
* 继续任务
*
* @param jobName 任务名
* @param jobGroup 任务分组
* @throws Exception 相关异常
*/
public void resume(String jobName, String jobGroup) throws Exception {
//组装参数
// JobKey jobKey = new JobKey(jobName, jobGroup);
TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", jobGroup);
//继续触发器
try {
// scheduler.resumeJob(jobKey);
scheduler.resumeTrigger(triggerKey);
} catch (SchedulerException e) {
throw new BaseException("启动定时任务失败");
}
}
/**
* 取消任务
*
* @param jobName 任务名
* @param jobGroup 任务分组
* @throws Exception 相关异常
*/
public void cancel(String jobName, String jobGroup) throws Exception {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName + "Trigger", jobGroup);
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
//暂停
scheduler.pauseTrigger(triggerKey);
//解除绑定
scheduler.unscheduleJob(triggerKey);
//删除触发器
scheduler.deleteJob(jobKey);
} catch (SchedulerException e) {
throw new BaseException("取消定时任务失败");
}
}
/**
* 取消全部任务
*
* @throws Exception 相关异常
*/
public void cancelAll() throws Exception {
List<QuartzJob> jobList = jobMapper.listJob();
for (QuartzJob quartzJob : jobList) {
cancel(quartzJob.getJobName(), quartzJob.getJobGroup());
}
}
/**
* 暂停全部任务
*
* @throws Exception 相关异常
*/
public void pauseAll() throws Exception {
try {
scheduler.pauseAll();
} catch (SchedulerException e) {
throw new BaseException("暂停定时任务失败");
}
}
/**
* 恢复所有任务
*
* @throws Exception
*/
public void resumeAll() throws Exception {
try {
scheduler.resumeAll();
} catch (SchedulerException e) {
throw new BaseException("启动定时任务失败");
}
}
}
控制层
直接调用服务层方法就可以了,没啥技术含量
这里只调用部分功能用于测试,请按照实际需求调整
@Controller
@RequestMapping("/quartz")
@Slf4j
public class QuartzController {
private QuartzService quartzService;
public QuartzController(QuartzService quartzService) {
this.quartzService = quartzService;
}
@GetMapping("/list")
@ResponseBody
public ReturnMsg list(String jobName) {
log.info("查询列表");
return ReturnMsg.success(quartzService.list(jobName));
}
@PostMapping("/save")
@ResponseBody
public ReturnMsg save(@RequestBody QuartzJob quartzJob) throws Exception {
log.info("保存");
quartzService.save(quartzJob);
return ReturnMsg.success();
}
@PostMapping("/pauseAll")
@ResponseBody
public ReturnMsg pauseAll() throws Exception {
log.info("全部暂停");
quartzService.pauseAll();
return ReturnMsg.success();
}
@PostMapping("/resumeAll")
@ResponseBody
public ReturnMsg resumeAll() throws Exception {
log.info("恢复全部");
quartzService.resumeAll();
return ReturnMsg.success();
}
@PostMapping("/cancelAll")
@ResponseBody
public ReturnMsg cancelAll() throws Exception {
log.info("删除全部");
quartzService.cancelAll();
return ReturnMsg.success();
}
}
测试结果
save的参数如下,其余请求为空参数
{ "jobName": "job1", "jobGroup": "group1", "description": "demoJob", "jobClassName": "MyTask", "cronExpression": "*/3 * * * * ?" }
3.补充
-
job与trigger的关系
job表示任务,trigger表示触发器
实际上两者是1-N的关系,一个任务可以被多个trigger持有,而trigger只能绑定一个job,但为了方便管理建议按1-1处理
因此对于job的操作(暂停、恢复)会同步至与之关联的trigger,部分操作如下
- 暂停/恢复job时,暂停/恢复与之相关的所有trigger
- 删除job时,解除所有trigger与自身的关联,解除失败时报错
而对于trigger的操作并不会同步到job
-
执行的时间
先上图
可以看到trigger中自己生成了一个字段
nextFireTime
,即下一次执行时间到了nextFireTime时间则会执行一次任务,并将该字段更新
-
执行失效的处理方案
配置文件中有一条属性
misfireThreshold = 5000
,即失效阈值为5秒钟,表示一个任务若5秒内没有执行完成,则表示执行失效在对
Trigger.withSchedule
的时候对ScheduleBuilder
设置处理方案-
CronScheduleBuilder
有4种方案,设置方法如下- 不设置:默认,智能处理。具体多智能我也不知道,但是这种不确定性显然是比较危险的,慎用
- withMisfireHandlingInstructionIgnoreMisfires():忽略失效策略。服务恢复(包括项目启动)后,将一次性执行多次直至错过的任务全部补充。适用于部分场景。
-
withMisfireHandlingInstructionDoNothing():啥也不做。服务恢复后,将直接把
nextFireTime
更新至当前时间之后的下一次的执行时间,那么之前漏掉的任务将会被抛弃掉。适用于大部分场景。 - withMisfireHandlingInstructionFireAndProceed():立即执行一次。服务恢复后立即执行一次,即将下次执行时间修改为现在,执行任务并从现在计算下一次时间。适用于部分场景。
-
SimpleScheduleBuilder
有5种方案 -
不设置:默认,智能处理,慎用
- withMisfireHandlingInstructionIgnoreMisfires():忽略失效策略。服务恢复后,将一次性执行多次直至错过的任务全部补充。适用于部分场景。
- withMisfireHandlingInstructionFireNow():立即执行一次,通常用于不重复执行的任务。即恢复后立即执行一次,对于不重复的任务相当于重试,对于重复的任务会立即执行一次,仅保留剩余次数而直接遗忘掉开始时间和总次数
-
withMisfireHandlingInstructionNextWithExistingCount():从当前时间之后的下次执行时间继续,错过的次数保留,不扣除
- withMisfireHandlingInstructionNextWithRemainingCount():从当前时间之后的下次执行时间继续,错过的次数扣除
-
withMisfireHandlingInstructionNowWithExistingCount():从现在继续,错过的次数保留,不扣除。遗忘开始时间和总次数
- withMisfireHandlingInstructionNowWithRemainingCount():从现在继续,错过的次数扣除。遗遗忘开始时间和总次数
剩余两种
ScheduleBuilder
不多做描述,有兴趣的可以直接查源码,写的还是比较详细的若未设置
misfireThreshold
属性,以上方案均不会生效,依旧为智能处理,且大概率为忽略失效策略 -
-
pauseTrigger与pauseJob与pauseAll(resume同理)
- pauseTrigger,即暂停触发器,那么自然任务也就不会再被触发
- pauseJob,即暂停job,会将与自己关联的所有trigger一同暂停,resume同理
- pauseAll,即全部暂停,实际上是暂停所有的group,并在数据库做记录,resume同理
4.可能遇到的坑
-
resume或重启项目后任务快速执行多次
原因就是前面补充的,错过时间的处理方案,默认值是智能处理,而智能成啥样并不知道。。。建议主动设置为需要的模式
如修改trigger构建方法如下
//构建新的触发器 Trigger trigger = TriggerBuilder .newTrigger() // .forJob(jobDetail)//目标job,这里不设置,如果设置了将会自动触发 .withIdentity(quartzJob.getJobName() + "Trigger", quartzJob.getJobGroup())//设置job名和分组名 .usingJobData("triggerName", quartzJob.getJobName() + "Trigger")//传递的参数,可以自定义,可传递多组,以map形式传递 .usingJobData(jobDataMap) .withSchedule(CronScheduleBuilder .cronSchedule(quartzJob.getCronExpression().trim()) .withMisfireHandlingInstructionDoNothing() ) .startNow()//现在就启动 // .startAt(new Date())//启动时间,可以自定义,与startNow冲突 .build();
-
pauseAll之后删除,然后再建立一样的任务,任务默认为暂停状态,而不是自动启动
pauseAll会暂停所有的triggerGroup并存储到表
qrtz_paused_trigger_grps
,而删除任务时并不会删除这张表里的内容(但会删除trigger)那么当从新建立一个一毛一样的任务的时候,quartz就会将其识别为pause状态,因而并不会启动
至于解决办法。。。个人建议批量暂停不要用pauseAll实现,完全可以查出所有任务,然后挨个pauseJob
5.demo地址
https://gitee.com/echo_ye/demo_basic/tree/scheduleDemo
仅实现了部分功能作为样例,请按照需求自己扩展哦,有疑问或者建议欢迎联系我~
BB两句
quartz之前只在spring用过。。。本来以为就一个小组件,打算几个定时器整理在一起得了。。。结果越学越多。。不得不分离出来作为单独的文章
当然整理出来的依然只有一小部分,后面有机会再扩充整理一遍
而且讲道理quartz的注释是写的真的挺好,我这个英语渣渣跟着debug看源码也问题不大
作者:Echo_Ye
WX:Echo_YeZ
EMAIL :echo_yezi@qq.com
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)