定时任务框架 - Quartz
1 核心概念
- 任务Job
Job就是你想要实现的任务类,每一个Job必须实现org.quartz.job接口,且只需实现接口定义的execute()方法。
- 触发器 Trigger
Trigger为你执行任务的触发器,比如你想每天定时3点发送一份统计邮件,Trigger将会设置3点进行执行该任务。Trigger主要包含两种SimpleTrigger和CronTrigger两种。关于二者的区别的使用场景,后续的课程会进行讨论。
- 调度器Scheduler
Scheduler为任务的调度器,它会将任务job及触发器Trigger整合起来,负责基于Trigger设定的时间来执行Job。
2 体系结构
3 Quartz的几个常用API
- Scheduler 用于与调度程序交互的主程序接口。
- Scheduler 调度程序-任务执行计划表
- Job 我们预先定义的希望在未来时间能被调度程序执行的任务类,我们可以自定义。
- JobDetail 使用JobDetail来定义定时任务的实例,JobDetail实例是通过JobBuilder类创建的。
- JobDataMap 可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用 其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数 据的方法。
- Trigger 触发器,Trigger对象是用来触发执行Job的。当调度一个job时,我们实例一个触发器然后调整它的属性来满足job执行的条件。表明任务在什么时候会执行。定义了一个已经被安排的任务将会在什么时候执行的时间条件,比如每2秒就执行一次。
- JobBuilder -用于声明一个任务实例,也可以定义关于该任务的详情比如任务名、组名等,这个声明的实例将会作为一个实际执行的任务。
- TriggerBuilder 触发器创建器,用于创建触发器trigger实例。
- JobListener、TriggerListener、SchedulerListener监听器,用于对组件的监听
4 Quartz的使用
4.1 准备工作
maven 项目
创建HelloJob任务类
// 定义任务类
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext arg0) throws JobExecutionException {
// 定义时间
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = dateFormat.format(date);
// 定义工作任务内容
System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
}
}
创建任务调度类HelloSchedulerDemo
public class HelloSchedulerDemo {
public static void main(String[] args) throws Exception {
// 1:从工厂中获取任务调度的实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1") // 定义该实例唯一标识
.build();
// 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1") // 定义该实例唯一标识
.startNow() // 马上执行
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.repeatSecondlyForever(5)) // 每5秒执行一次
.build();
// 4:使用触发器调度任务的执行
scheduler.scheduleJob(job, trigger);
// 5:开启
scheduler.start();
// 关闭
// scheduler.shutdown();
}
}
4.2 Job、JobDetail、JobExecutionContext、JobDataMap
- Job:
- 工作任务调度的接口,任务类需要实现该接口, eg:
HelloJob implements Job,该类中定义了execute方法 - Job 实例: 在 Quartz 中的生命周期:每次调度器执行 Job 时,它在调用 execute 方法前会创建一个新的 Job 实例,当调用完成后,关联的 Job 对象实例会被释放,释放的实例会被垃圾回收机制回收
- 工作任务调度的接口,任务类需要实现该接口, eg:
- JobDetail:
- JobDetail 为 Job 实例提供了许多
设置的属性,以及 JobDataMap 成员变量属性,它用来存储特定 Job 实例的状态信息,调度器需要借助 JobDetail 对象来添加Job实例 - JobDetail 重要属性: name, group, jobClass, jobDataMap
- JobDetail 为 Job 实例提供了许多
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1") // 定义该实例唯一标识,并指定一个组。
.build();
System.out.println("name:"+job.getKey().getName()); // key的名称
System.out.println("group:"+job.getKey().getGroup()); // 组的名称,默认为Default
System.out.println("jobClass:"+job.getJobClass().getName()); // 任务的类
-
JobExecutionContext:
- 当Scheduler调用一个Job,就会将JobExecutionContext传递给Job的execute()方法;
- Job能通过JobExecutionContext对象访问到Quartz运行时候的环境以及Job本身的明细数据。
-
JobDataMap:
- JobDataMap存储在JobExecutionContext中 ,非常方便获取
- JobDataMap可以用来装载任何可序列化的数据对象,当job实例对象被执行时这些参数对象会传递给它
- JobDataMap实现了JDK的Map接口,并且添加了非常方便的方法用来存取基本数据类型
4.3 有状态的Job和无状态的Job
- 当前是无状态,即每次调用时都会创建一个新的JobDataMap
修改boot程序,添加.usingJobData(“count”, 0),其表示计数器。
JobDetail job = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1") // 定义该实例唯一标识
.usingJobData("message", "打印日志")
.usingJobData("count", 0)
.build();
修改HelloJob.java
private Integer count;
public void setCount(Integer count) {
this.count = count;
}
在public void execute(JobExecutionContext context) throws JobExecutionException的方法中添加。
++count;
System.out.println("count数量:"+count);
context.getJobDetail().getJobDataMap().put("count", count);
我们发现,打印的count始终为1
- 如何变成有状态呢?使用@PersistJobDataAfterExecution 注解
@PersistJobDataAfterExecution
public class HelloJob implements Job {
}
4.4 Trigger 触发器
-
jobKey
- 表示job实例的标识,触发器被触发时,该指定的job实例会被执行
-
startTime
- 表示触发器的时间表,第一次开始被触发的时间,数据类型是java.util.Date
-
endTime
- 指定触发器终止被触发的时间,数据类型是java.util.Date
4.5 CronTrigger
使用场景:需要像日历那样按日程来触发任务,而不是像SimpleTrigger 那样每隔特定的间隔时间触发,CronTriggers通常比SimpleTrigger更有用,因为它是基于日历的作业调度器。
使用CronTrigger,你可以指定诸如“每个周五中午”,或者“每个工作日的9:30”或者“从每个周一、周三、周五的上午9:00到上午10:00之间每隔五分钟”这样日程安排来触发。甚至,象SimpleTrigger一样,CronTrigger也有一个startTime以指定日程从什么时候开始,也有一个(可选的)endTime以指定何时日程不再继续
4.6 Cron表达式
Cron表达式被用来配置CronTrigger实例。Cron表达式是一个由7个子表达式组成的字符串。每个子表达式都描述了一个单独的日程细节。这些子表达式用空格分隔,分别表示:
- Seconds 秒
- Minutes 分钟
- Hours 小时
- Day-of-Month 月中的天
- Month 月
- Day-of-Week 周中的天
- Year (optional field) 年(可选的域)
5 Quartz任务调度
5.1 配置、资源SchedulerFactory
所有的Scheduler实例由SchedulerFactory创建 Quartz的三个核心概念:调度器、任务、触发器,三者之间的关系是:
注意!
- 我们知道一个作业,比较重要的三个要素就是Schduler,jobDetail,Trigger;而Trigger 对于job而言就好比一个驱动器;没有触发器来定时驱动作业,作业就无法运行;
- 对于Job而言,一个job可以对应多个Trigger,但对于Trigger而言,一个Trigger只能对应一个job;所以一个Trigger 只能被指派给一个 Job;如果你需要一个更复杂的触发计划,你可以创建多个 Trigger并指派它们给同一个 Job
Scheduler的创建方式:StdSchedulerFactory
- 使用一组参数(java.util.Properties)来创建和初始化Quartz调度器
- 配置参数一般存储在quartz.properties文件中
- 调用getScheduler方法就能创建和初始化调度器对象
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
用法1: 输出调度时间
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("调度器开始的时间是:"+dateFormat.format(scheduler.scheduleJob(job, trigger)));
用法2: 启动任务调度
scheduler.start();
用法3: 任务调度挂起
// Scheduler 执行2秒后自动挂起
Thread.sleep(2000L);
scheduler.standby();
// Scheduler 执行5秒后自动开启
Thread.sleep(5000L);
scheduler.start();
用法四:关闭任务调度
// Scheduler执行2秒后自动挂起
Thread.sleep(2000L);
scheduler.shutdown();
// Scheduler执行5秒后自动开启Thread.sleep(5000L);
scheduler.start();
区分
- shutdown(true)表示等待所有正在执行的job执行完毕之后,再关闭scheduler
- shutdown(false)即直接关闭scheduler
5.2 Quartz.properties
我们也可以在项目的资源下添加quartz.properties文件,去覆盖底层的配置文件。
- 调度器属性:
- org.quartz.scheduler.instanceName: 属性用来区分特定的调度器实例
- org.quartz.scheduler.instanceId: 这个值必须在所有调度器实例中是唯一的,尤其是在一个集群环境中,作为集群的唯一key
- 线程池属性:
- threadCount: 至少为1,但最多的话最好不要超过100
- threadPriority: 最小为1,最大为10,默认为5
6 监听器
6.1 概念
区分全局监听器与非全局监听器
- 全局监听器: 全局监听器能够接收到所有的Job/Trigger的事件通知
- 非全局监听器: 而非全局监听器只能接收到在其上注册的Job或Trigger的事件,不在其上注册的Job或Trigger则不会进行监听
6.2 JobListener
使用场景: 任务调度过程中,与任务Job相关的事件包括:job开始要执行的提示; job执行完成的提示
public interface JobListener {
String getName();
void jobToBeExecuted(JobExecutionContext context);
void jobExecutionVetoed(JobExecutionContext context);
void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException);
}
示例:
HelloJobListener.java
// 定义任务类
public class HelloJobListener implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 定义时间
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = dateFormat.format(date);
// 定义工作任务内容
System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
}
}
创建自定义的JobListener
MyJobListener.java
public class MyJobListener implements JobListener{
@Override
public String getName() {
String name = getClass().getSimpleName();
System.out.println("监听器的名称是:"+name);
return name;
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
String jobName = context.getJobDetail().getKey().getName();
System.out.println("Job的名称是:"+jobName+" Scheduler在JobDetail将要被执行时调用这个方法");
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
String jobName = context.getJobDetail().getKey().getName();
System.out.println("Job的名称是:"+jobName+" Scheduler在JobDetail即将被执行,但又被TriggerListerner否决时会调用该方法");
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
String jobName = context.getJobDetail().getKey().getName();
System.out.println("Job的名称是:"+jobName+" Scheduler在JobDetail被执行之后调用这个方法");
}
}
执行调度器
HelloSchedulerDemoJobListener.java
public class HelloSchedulerDemoJobListener {
public static void main(String[] args) throws Exception {
// 1:从工厂中获取任务调度的实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
JobDetail job = JobBuilder.newJob(HelloJobListener.class)
.withIdentity("job1", "group1") // 定义该实例唯一标识
.build();
// 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1") // 定义该实例唯一标识
.startNow() // 马上执行
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.repeatSecondlyForever(5)) // 每5秒执行一次
.build();
// 4:使用触发器调度任务的执行
scheduler.scheduleJob(job, trigger);
// 创建并注册一个全局的 Listener
// scheduler.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs());
// 创建并注册一个局部的 Listener, 表示指定的job
scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(JobKey.jobKey("job1","group1")));
// 5:开启
scheduler.start();
// 关闭
// scheduler.shutdown();
}
}
6.3 TriggerListener
使用场景: 任务调度过程中,与触发器Trigger相关的事件包括:触发器触发、触发器未正常触发、触发器完成等。
public interface TriggerListener {
public String getName();
public void triggerFired(Trigger trigger, JobExecutionContext context);
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context);
public void triggerMisfired(Trigger trigger);
public void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode);
}
HelloJobListener.java
同上
MyTriggerListener.java
public class MyTriggerListener implements TriggerListener{
private String name;
public MyTriggerListener(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void triggerFired(Trigger trigger, JobExecutionContext context) {
String triggerName = trigger.getKey().getName();
System.out.println(triggerName + " 被触发");
}
@Override
public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
String triggerName = trigger.getKey().getName();
System.out.println(triggerName + " 没有被触发");
return true; // true:表示不会执行Job的方法
}
@Override
public void triggerMisfired(Trigger trigger) {
String triggerName = trigger.getKey().getName();
System.out.println(triggerName + " 错过触发");
}
@Override
public void triggerComplete(Trigger trigger, JobExecutionContext context, CompletedExecutionInstruction triggerInstructionCode) {
String triggerName = trigger.getKey().getName();
System.out.println(triggerName + " 完成之后触发");
}
}
任务调度类HelloSchedulerDemoTriggerListener.java
public class HelloSchedulerDemoTriggerListener {
public static void main(String[] args) throws Exception {
// 1:从工厂中获取任务调度的实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
JobDetail job = JobBuilder.newJob(HelloJobListener.class)
.withIdentity("job1", "group1") // 定义该实例唯一标识
.build();
// 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1") // 定义该实例唯一标识
.startNow() // 马上执行
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.repeatSecondlyForever(5)) // 每5秒执行一次
.build();
// 4:使用触发器调度任务的执行
scheduler.scheduleJob(job, trigger);
// 创建并注册一个全局的Trigger Listener
scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), EverythingMatcher.allTriggers());
// 创建并注册一个局部的Trigger Listener
// scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), KeyMatcher.keyEquals(TriggerKey.triggerKey("trigger1", "group1")));
// 5:开启
scheduler.start();
// 关闭
// scheduler.shutdown();
}
}
6.4 SchedulerListener
略 ...