@[toc]
一、Quartz 简介
1.Java 主流三大定时任务框架优缺点
2.什么是 Quartz?
3.模型图
4.名词解释
名词举例
关系图-自己理解
<font color='red'>一个 job 可以被多个 Trigger 绑定,但是一个 Trigger 只能绑定一个 job!</font>
(重点掌握) job
Job 是 Quartz 中的一个接口,接口下只有 execute 方法,在这个方法中编写业务逻辑。Job 有个子接口 StatefulJob ,代表有状态任务。有状态任务不可并发,前次任务没有执行完,后面任务处于阻塞等到。
(重点掌握) JobDetail
JobDetail 用来绑定 Job,为 Job 实例提供许多属性:【name/group/jobClass/jobDataMap】JobDetail 绑定指定的 Job,每次 Scheduler 调度执行一个 Job 的时候,首先会拿到对应的 Job,然后创建该 Job 实例,再去执行 Job 中的 execute()的内容,任务执行结束后,关联的 Job 对象实例会被释放,且会被 JVM GC 清除。JobDetails 将使用 JobBuilder 创建/定义。
问题思考:为什么设计成 JobDetail + Job,不直接使用 Job?
答案: JobDetail 定义的是任务数据,而真正的执行逻辑是在 Job 中。这是因为任务是有可能并发执行,如果 Scheduler 直接使用 Job,就会存在对同一个 Job 实例并发访问的问题。而 JobDetail & Job 方式,Sheduler 每次执行,都会根据 JobDetail 创建一个新的 Job 实例,这样就可以规避并发访问的问题。
(重点掌握) JobExecutionContext
JobExecutionContext 中包含了 Quartz 运行时的环境以及 Job 本身的详细数据信息。当 Schedule 调度执行一个 Job 的时候,就会将 JobExecutionContext 传递给该 Job 的 execute()中,Job 就可以通过 JobExecutionContext 对象获取信息。主要信息有:
(重点掌握) JobDataMap
JobDataMap 实现了 JDK 的 Map 接口,可以以 Key-Value 的形式存储数据。JobDetail、Trigger 都可以使用 JobDataMap 来设置一些参数或信息,同名后者会覆盖前者值。Job 执行 execute()方法的时候,JobExecutionContext 可以获取到 JobExecutionContext 中的信息。如:
JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .usingJobData("jobDetail1", "这个Job用来测试的") .withIdentity("job1", "group1").build();
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1") .usingJobData("trigger1", "这是jobDetail1的trigger") .startNow()//立即生效 .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(1)//每隔1s执行一次 .repeatForever()).build();//一直执行
复制代码
Job 执行的时候,可以获取到这些参数信息:
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println(jobExecutionContext.getJobDetail().getJobDataMap().get("jobDetail1")); System.out.println(jobExecutionContext.getTrigger().getJobDataMap().get("trigger1")); String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date()); System.out.println("PrintWordsJob start at:" + printTime + ", prints: Hello Job-" + new Random().nextInt(100)); }
复制代码
(重点掌握) Trigger、SimpleTrigger、CronTrigger
Quartz 中五种类型的 Trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,NthIncludedDayTrigger 和 Calendar 类( org.quartz.Calendar)。最常用的:SimpleTrigger:用来触发只需执行一次或者在给定时间触发并且重复 N 次且每次执行延迟一定时间的任务。CronTrigger:按照日历触发,例如“每个周五”,每个月 10 日中午或者 10:15 分。Trigger 是 Quartz 的触发器,会去通知 Scheduler 何时去执行对应 Job。new Trigger().startAt():表示触发器首次被触发的时间;new Trigger().endAt():表示触发器结束触发的时间;使用 TriggerBuilder 实例化实际触发器。
SimpleTrigger 可以实现在一个指定时间段内执行一次作业任务或一个时间段内多次执行作业任务。
CronTrigger 功能非常强大,是基于日历的作业调度,而 SimpleTrigger 是精准指定间隔,所以相比 SimpleTrigger,CroTrigger 更加常用。注意:* cron 表达式 6 位:秒分时日月周* cron 表达式 7 位:秒分时日月周年
(了解) QuartzSchedulerThread
负责执行向 QuartzScheduler 注册的触发 Trigger 的工作的线程。
(了解) ThreadPool
Scheduler 使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提供运行效率。
(了解) QuartzSchedulerResources
包含创建 QuartzScheduler 实例所需的所有资源(JobStore,ThreadPool 等)。
(重点掌握) SchedulerFactory
提供用于获取调度程序实例的客户端可用句柄的机制。
(了解) JobStore
通过类实现的接口,这些类要为 org.quartz.core.QuartzScheduler 的使用提供一个 org.quartz.Job 和 org.quartz.Trigger 存储机制。作业和触发器的存储应该以其名称和组的组合为唯一性。
(了解) QuartzScheduler
这是 Quartz 的核心,它是 org.quartz.Scheduler 接口的间接实现,包含调度 org.quartz.Jobs,注册 org.quartz.JobListener 实例等的方法。
(重点掌握) Scheduler
这是 Quartz Scheduler 的主要接口,代表一个独立运行容器。调度程序维护 JobDetails 和触发器的注册表。 一旦注册,调度程序负责执行作业,当他们的相关联的触发器触发(当他们的预定时间到达时)。
5.Quartz 定时任务管理(动态添加、停止、恢复任务及输出下次执行时间、删除定时任务)
停止:scheduler.pauseJob(jobKey);恢复:scheduler.resumeJob(jobKey);删除:scheduler.deleteJob(jobKey);添加:scheduler.scheduleJob(jobDetail,cronTrigger)输出下次执行时间(前提是只有执行了 resumeJob()后才能获取下次执行时间,其他方法无效) scheduler.getTriggersOfJob(jobKey).get(0).getFireTimeAfter(new Date()))
二.案例举例详细说明
使用 quartz 须引入
<!--quartz调度器--><dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version></dependency>
复制代码
使用阿里线程池须引入
<!--引入commons-lang3,为了创建阿里线程池--><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId></dependency>
复制代码
PrintWordsJob 代码
import lombok.SneakyThrows;import org.quartz.Job;import org.quartz.JobExecutionContext;import org.quartz.JobExecutionException;import org.quartz.TriggerKey;
import java.text.SimpleDateFormat;import java.util.Date;import java.util.Random;
/** * 新建一个能够打印任意内容的Job * @Author 211145187 * @Date 2022/4/6 17:06 **/public class PrintWordsJob implements Job {
@SneakyThrows @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "triggerGroup1"); //获取任务状态 System.out.println("任务状态:" + jobExecutionContext.getScheduler().getTriggerState(triggerKey));
//获取job标识 System.out.println("getJobKey():" + jobExecutionContext.getTrigger().getJobKey()); //输出job1,group1 //获取trigger标识 System.out.println("getKey():" + jobExecutionContext.getTrigger().getKey()); //输出trigger1,triggerGroup1
//get(key)和getString(key)等效 System.out.println("get(\"name1\"):" + jobExecutionContext.getMergedJobDataMap().get("name1")); System.out.println("getString(\"name2\"):" + jobExecutionContext.getMergedJobDataMap().getString("name2")); System.out.println("get(\"name1\"):" + jobExecutionContext.getJobDetail().getJobDataMap().get("name1")); System.out.println("getString(\"name2\"):" + jobExecutionContext.getJobDetail().getJobDataMap().getString("name2"));
//输出打印时间 String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date()); System.out.println("PrintWordsJob start at:" + printTime + ", prints: Hello Job-" + new Random().nextInt(100)); }
复制代码
案例 1:触发器类型为 SimpleTrigger 的简单任务实例,执行 main 方法,执行一次任务/每间隔 1s,5s 后结束执行
使用流程:创建 schedulerFactory 实例 -》 获取 scheduler 实例 -》 创建 JobDetail -》 创建 Trigger -》 封装进 scheduler 中 -》 scheduler.start()执行
public static void main(String[] args) throws SchedulerException, InterruptedException { //案例1:触发器类型为SimpleTrigger的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行 SimpleTrigger1();}
复制代码
/** * 案例1:触发器类型为SimpleTrigger的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行 * @Author 211145187 * @Date 2022/4/6 17:23 **/ public static void SimpleTrigger1() throws SchedulerException, InterruptedException { // 1、创建调度器Scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容) JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .withIdentity("job1", "group1") //使用具有给定名称和组来标识 JobDetail的身份 .build(); // 3、构建Trigger实例,每隔1s执行一次 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "triggerGroup1") .startNow()//立即生效 .withSchedule(SimpleScheduleBuilder.simpleSchedule() //设置调度器类型:构建SimpleTrigger简单触发器 .withIntervalInSeconds(1)//每隔1s执行一次 .repeatForever() ).build();//一直执行
//4、执行 scheduler.scheduleJob(jobDetail, trigger); System.out.println("--------SimpleTrigger1 scheduler start ! ------------"); scheduler.start();
//睡眠 Thread.sleep(5000); scheduler.shutdown(); System.out.println("--------SimpleTrigger1 scheduler shutdown ! ------------"); }
复制代码
案例 2:触发器类型为 SimpleTrigger 的简单任务实例,指定开始结束时间,下面的程序就实现了程序运行 5s 后开始执行 Job,执行 Job 5s 后结束执行
注意:请留意代码注释及方法注释中的文字说明注意:参数设置有两种方式:注意:封装 TriggerKey 和普通直接设置
public static void main(String[] args) throws SchedulerException, InterruptedException { //案例2:触发器类型为SimpleTrigger的简单任务实例,下面的程序就实现了程序运行5s后开始执行Job,执行Job 5s后结束执行 SimpleTrigger2();}
复制代码
/** * 案例2:触发器类型为SimpleTrigger的简单任务实例,可以实现在一个指定时间段内执行一次作业任务或一个时间段内多次执行作业任务 * 下面的程序就实现了程序运行5s后开始执行Job,执行Job 5s后结束执行 * 注意: * 1.JobDetail和Trigger设置同名参数会被覆盖 * 2.JobDetail设置的参数使用jobExecutionContext.getJobDetail().getJobDataMap()和jobExecutionContext.getMergedJobDataMap()都可以获取到,而Trigger设置的参数只能使用jobExecutionContext.getMergedJobDataMap()才可以获取到 * 3.参数设置有两种方式: * 设置参数方式1,直接使用usingJobData设置,用于执行execute()中获取 * 设置参数方式2,使用jobDetail.getJobDataMap().put(key, value)设置,用于执行execute()中获取 * 4.可以封装TriggerKey传入Trigger的withIdentity(形参中),也可以直接设置值 =》 既.withIdentity("trigger1", "triggerGroup1") 和.withIdentity(triggerKey)等效 * @Author 211145187 * @Date 2022/4/6 17:23 **/ public static void SimpleTrigger2() throws SchedulerException, InterruptedException { Date startDate = new Date(); startDate.setTime(startDate.getTime() + 5000); Date endDate = new Date(); endDate.setTime(startDate.getTime() + 5000); // 1、创建调度器Scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容) JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .withIdentity("job1", "group1") //使用具有给定名称和组来标识 JobDetail的身份 .usingJobData("name1", "孙悟空") //设置参数方式1,直接使用usingJobData设置,用于执行execute()中获取 .build(); jobDetail.getJobDataMap().put("name3", "猪八戒"); //设置参数方式2,使用jobDetail.getJobDataMap().put(key, value)设置,用于执行execute()中获取 //注意:可以封装TriggerKey传入Trigger的withIdentity(形参中),也可以直接设置值 =》 既.withIdentity("trigger1", "triggerGroup1") 和.withIdentity(triggerKey)等效 TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "triggerGroup1"); // 3、构建Trigger实例,每隔1s执行一次 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "triggerGroup1") //使用具有给定名称和组来标识 Trigger的身份 .startNow()//立即生效 .usingJobData("name2", "这是jobDetail1的trigger") //设置参数,用于执行execute()中获取 .startAt(startDate) //设置开始时间 .endAt(endDate) //设置结束时间 .withSchedule(SimpleScheduleBuilder.simpleSchedule() //设置调度器类型:构建SimpleTrigger简单触发器 .withIntervalInSeconds(1)//每隔1s执行一次 .withRepeatCount(1)//定义重复执行次数 .repeatForever() ).build();//一直执行 //4、执行 scheduler.scheduleJob(jobDetail, trigger); System.out.println("--------SimpleTrigger2 scheduler start ! ------------"); scheduler.start(); //睡眠 Thread.sleep(10000); scheduler.shutdown(); System.out.println("--------SimpleTrigger2 scheduler shutdown ! ------------"); }
复制代码
案例 3:触发器类型为 CronTrigger 的简单任务实例,执行 main 方法,执行一次任务/每间隔 1s,5s 后结束执行
注意:Trigger 和 CronTrigger 对象实例,调用的方法存在细微差别 Trigger 实例设置.withIdentity()、.startNow()、.usingJobData()、.startAt()、.endAt、.withSchedule()、.build()CronTrigger 实例设置.withIdentity()、.usingJobData()、.startNow()、.withSchedule()、.build()
public static void main(String[] args) throws SchedulerException, InterruptedException { //案例3:触发器类型为CronTrigger 的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行 CronTrigger1();}
复制代码
/** * 案例3:触发器类型为CronTrigger 的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行 * 注意: * cron表达式6位:秒分时日月周 * cron表达式7位:秒分时日月周年 * @Author 211145187 * @Date 2022/4/6 17:23 **/ public static void CronTrigger1() throws SchedulerException, InterruptedException { // 1、创建调度器Scheduler SchedulerFactory schedulerFactory = new StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容) JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .withIdentity("job1", "group1") .build(); //使用具有给定名称和组来标识 JobDetail的身份 // 3、构建Trigger实例,每隔1s执行一次,执行10s后停止 CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1") .usingJobData("trigger1", "这是jobDetail1的trigger") .startNow()//立即生效 .withSchedule(CronScheduleBuilder.cronSchedule("0/1 * * * * ?")) //设置调度器类型:构建CronTrigger简单触发器 .build();
//4、执行 scheduler.scheduleJob(jobDetail, cronTrigger); System.out.println("--------CronTrigger1 scheduler start ! ------------"); scheduler.start();
//睡眠 Thread.sleep(10000); scheduler.shutdown(); System.out.println("--------CronTrigger1 scheduler shutdown ! ------------"); }
复制代码
案例 4:使用阿里线程池,模拟定时执行任务途中停止任务,线程睡眠 2 秒后再激活任务,输出查看任务状态等信息
定时任务 CronTrigger1 一直执行, 然后新启动一个线程执行暂停方法 -》 休息 2 秒后 -》 再执行激活方法,查看 job 的 execute()方法参数输出及任务改变状态打印等。主要为了打印观察任务执行状态及改变。
import lombok.SneakyThrows;import org.apache.commons.lang3.concurrent.BasicThreadFactory;import org.quartz.CronScheduleBuilder;import org.quartz.CronTrigger;import org.quartz.JobBuilder;import org.quartz.JobDetail;import org.quartz.JobKey;import org.quartz.Scheduler;import org.quartz.SchedulerException;import org.quartz.SchedulerFactory;import org.quartz.SimpleScheduleBuilder;import org.quartz.Trigger;import org.quartz.TriggerBuilder;import org.quartz.TriggerKey;import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;/** * 创建Schedule,执行任务 * @Author 211145187 * @Date 2022/4/6 17:07 **/public class MyScheduler { //这里使用的是ThreadPoolExecutor的完整版构造函数 private static final ThreadPoolExecutor singlePool = new ThreadPoolExecutor(10,10,100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build() ,new ThreadPoolExecutor.CallerRunsPolicy()); public static void main(String[] args) throws SchedulerException, InterruptedException {CronTrigger1(); //案例4:使用阿里线程池,模拟定时执行任务途中停止任务,休息几秒后再激活任务,输出查看任务状态等信息 singlePool.execute(new Runnable() { @SneakyThrows @Override public void run() { //执行暂停方法 pauseJob(); //睡眠2秒,让暂停方法和激活方法中间有点时间间隔 Thread.sleep(2000); //激活任务 resumeJob(); } });}
复制代码
/** * 暂停任务 * 注意: * 1.这里传的是job的name和group名字,千万不要写成trigger的名字和组名 * 2.name和group都要跟任务job的一致,负责执行无效 **/ public static void pauseJob() throws SchedulerException { SchedulerFactory schedulerFactory = new StdSchedulerFactory();// JobKey jobKey = new JobKey("job1", Scheduler.DEFAULT_GROUP); //无效。组名不一致 JobKey jobKey = new JobKey("job1", "group1"); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.pauseJob(jobKey); System.out.println("--------CronTrigger1 pauseJob ! ------------"); } //激活任务 public static void resumeJob() throws SchedulerException { SchedulerFactory schedulerFactory = new StdSchedulerFactory(); JobKey jobKey = new JobKey("job1", "group1"); Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.resumeJob(jobKey); System.out.println("--------CronTrigger1 resumeJob ! ------------"); System.out.println("输出下次执行之间:" +scheduler.getTriggersOfJob(jobKey).get(0).getFireTimeAfter(new Date())); //输出下次执行时间 }
复制代码
三、容易出错点整理
1.创建 JobDetail 实例和 Trigger 实例,必须指定属性 name 和 group 值,当然也可以使用 Scheduler.DEFAULT_GROUP 提供的默认值等。
2.创建 JobDetail 实例和 Trigger 实例,都会配置属于自己的方法,比如
Trigger 实例方法
JobDetail 实例方法
3.使用 JobDetail 和 Trigger 设置参数
1.JobDetail 和 Trigger 都可以设置参数,且设置同名参数会被覆盖。2.具体 job 的 execute()方法获取形参方式有差异,比如 JobDetail 实例设置的参数,使用 jobExecutionContext.getJobDetail().getJobDataMap()和 jobExecutionContext.getMergedJobDataMap()都可以获取到。而 Trigger 实例设置的参数只能使用 jobExecutionContext.getMergedJobDataMap()才可以获取到既 :个人建议使用 getMergedJobDataMap()这个方法获取参数,目前都能获取到
jobExecutionContext.getMergedJobDataMap().get("name1") jobExecutionContext.getMergedJobDataMap().getString("name2") jobExecutionContext.getJobDetail().getJobDataMap().get("name1") jobExecutionContext.getJobDetail().getJobDataMap().getString("name2")
复制代码
4.JobDetail 设置参数两种方式
第一种方式设置参数
JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .withIdentity("job1", "group1") //使用具有给定名称和组来标识 JobDetail的身份 .usingJobData("name1", "孙悟空") //设置参数方式1,直接使用usingJobData设置,用于执行execute()中获取 .build();
复制代码
第二种方式设置参数
jobDetail.getJobDataMap().put("name3", "猪八戒");
复制代码
5.构建 Trigger 实例 2 种方式
<font color='red'> 该部分指构建 Trigger 实例时候使用</font>
第一种实例化方式
Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "triggerGroup1") .build();
复制代码
第二种实例化方式,封装 TriggerKey
TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "triggerGroup1");Trigger trigger = TriggerBuilder.newTrigger() .withIdentity(triggerKey) .build();
复制代码
6.针对暂停、激活、删除任务、获取下次执行时间说明
暂停 pauseJob(jobKey)
激活 resumeJob(jobKey)
删除 deleteJob(jobKey)输出下次执行时间 scheduler.getTriggersOfJob(jobKey).get(0).getFireTimeAfter(new Date()))
其中的 jobKey 为添加任务时候使用的 jobKey,必须一致,不然无法找到,就会导致暂停、激活、删除操作失效。注意:激活任务不会立即执行任务,而只是让任务状态处于“已就绪”状态,随时等待启动。注意:构建 JobKey 传入的是 job 的 name 和 group 名字,千万不要写成 trigger 的名字和组名。
7.案例 4 使用阿里线程池中暂停方法和激活方法之间必须设置休眠时间,不然执行太快,看不出来打印任务状态改变啥的
8.具体实现 job 接口的实现类的 execute()的形参 jobExecutionContext 输出展示
获取任务状态 TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "triggerGroup1");jobExecutionContext.getScheduler().getTriggerState(triggerKey)
获取 job 标识 jobExecutionContext.getTrigger().getJobKey() //输出 job1,group1
获取 trigger 标识 jobExecutionContext.getTrigger().getKey() //输出 trigger1,triggerGroup1
get(key)和 getString(key)等效
jobExecutionContext.getMergedJobDataMap().get("name1")jobExecutionContext.getMergedJobDataMap().getString("name2")
评论