Quartz学习笔记(三)JobKey、TriggerKey、Trigger
JobKey与TriggerKey
JobKey是表明Job身份的一个对象,里面封装了Job的name和group,TriggerKey同理。
之前我们在构建JobDetail和Trigger是用的是以下方法来指定他们对于的name和group
JobBuilder里面
public JobBuilder withIdentity(String name, String group) {
key = new JobKey(name, group);
return this;
}
TriggerBuilder里面
public TriggerBuilder<T> withIdentity(String name, String group) {
key = new TriggerKey(name, group);
return this;
}
关于withIdentity方法,我们可以直接构造一个TriggerKey或者JobKey,然后通过withIdentity传递给对于的Builder
在TriggerKey和JobKey中分别封装了两个静态方法来获取TriggerKey和JobKey
当我们不指定group时,Quartz会用默认的组名DEFAULT
Trigger
所有类型的trigger都有TriggerKey这个属性,表示trigger的身份;除此之外,trigger还有很多其它的公共属性。这些属性,在构建trigger的时候可以通过TriggerBuilder设置。
我们可以通过查看TriggerBuilder里面的一些方法跟Trigger的源码来了解到这些属性。
如下:
public class TriggerBuilder<T extends Trigger> {
private TriggerKey key;
private String description;
private Date startTime = new Date();
private Date endTime;
private int priority = Trigger.DEFAULT_PRIORITY;
private String calendarName;
private JobKey jobKey;
private JobDataMap jobDataMap = new JobDataMap();
private ScheduleBuilder<?> scheduleBuilder = null;
private TriggerBuilder() {
}
.........
}
public abstract class AbstractTrigger<T extends Trigger> implements OperableTrigger {
private static final long serialVersionUID = -3904243490805975570L;
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Data members.
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
private String name;
private String group = Scheduler.DEFAULT_GROUP;
private String jobName;
private String jobGroup = Scheduler.DEFAULT_GROUP;
private String description;
private JobDataMap jobDataMap;
@SuppressWarnings("unused")
private boolean volatility = false; // still here for serialization backward compatibility
private String calendarName = null;
private String fireInstanceId = null;
private int misfireInstruction = MISFIRE_INSTRUCTION_SMART_POLICY;
private int priority = DEFAULT_PRIORITY;
private transient TriggerKey key = null;
}
其中比较常用的公共属性有
- jobKey属性:当trigger触发时被执行的job的身份;
- startTime属性:设置trigger第一次触发的时间;该属性的值是java.util.Date类型,表示某个指定的时间点;有些类型的trigger,会在设置的startTime时立即触发,有些类型的trigger,表示其触发是在startTime之后开始生效。比如,现在是1月份,你设置了一个trigger–“在每个月的第5天执行”,然后你将startTime属性设置为4月1号,则该trigger第一次触发会是在几个月以后了(即4月5号)。
- endTime属性:表示trigger失效的时间点。比如,”每月第5天执行”的trigger,如果其endTime是7月1号,则其最后一次执行时间是6月5号。
除了以上比较常用的属性,Trigger还有优先级(priority)、错过触发(misfire Instructions)和日历示例(calendar)这三个比较重要的属性
优先级(priority)
如果你的trigger很多(或者Quartz线程池的工作线程太少),Quartz可能没有足够的资源同时触发所有的trigger;这种情况下,你可能希望控制哪些trigger优先使用Quartz的工作线程,要达到该目的,可以在trigger上设置priority属性。比如,你有N个trigger需要同时触发,但只有Z个工作线程,优先级最高的Z个trigger会被首先触发。如果没有为trigger设置优先级,trigger使用默认优先级,值为5;priority属性的值可以是任意整数,正数、负数都可以。
注意:只有同时触发的trigger之间才会比较优先级。10:59触发的trigger总是在11:00触发的trigger之前执行。
注意:如果trigger是可恢复的,在恢复后再调度时,优先级与原trigger是一样的。
在构造Trigger,通过TriggerBuilder的withPriority方法来设置
public TriggerBuilder<T> withPriority(int triggerPriority) {
this.priority = triggerPriority;
return this;
}
错过触发(misfire Instructions)
如果scheduler关闭了,或者Quartz线程池中没有可用的线程来执行job,此时持久性的trigger就会错过(miss)其触发时间,即错过触发(misfire)。
当下次调度器启动或者有可以线程时,会检查处于misfire状态的Trigger。而misfire的状态值决定了调度器如何处理这个Trigger。
不同类型的trigger,有不同的misfire机制。它们默认都使用“智能机制(smart policy)”,即根据trigger的类型和配置动态调整行为。
具体的misfire机制在学习具体的trigger时再详细整理一下。
日历(calendar)
Quartz的Calendar对象(不是java.util.Calendar对象)可以在定义和存储trigger的时候与trigger进行关联。
Calendar用于从trigger的调度计划中排除时间段。比如,可以创建一个trigger,每个工作日的上午9:30执行,然后增加一个Calendar,排除掉所有的商业节日。
Calendar排除时间段的单位可以精确到毫秒,同时Quartz提供了很多开箱即用的实现来满足我们的不同需求,如下
类名 | 用法 |
---|---|
org.quartz.impl.calendar.HolidayCalendar | 指定特定的日期,比如20140613 精度到天 |
org.quartz.impl.calendar.AnnualCalendar | 指定每年的哪一天,精度是天 |
org.quartz.impl.calendar.MonthlyCalendar | 指定每月的几号,可选值为1-31,精度是天 |
org.quartz.impl.calendar.WeeklyCalendar | 指定每星期的星期几,可选值比如为java.util.Calendar.SUNDAY,精度是天 |
org.quartz.impl.calendar.DailyCalendar | 指定每天的时间段(rangeStartingTime, rangeEndingTime),格式是HH:MM[:SS[:mmm]],也就是最大精度可以到毫秒 |
org.quartz.impl.calendar.CronCalendar | 指定Cron表达式,精度取决于Cron表达式,也就是最大精度可以到秒 |
注意,所有的Calendar既可以是排除,也可以是包含
HolidayCalendar与AnnualCalendar的区别
- HolidayCalendar考虑的是年,需要指定确定的年月日
- AnnualCalendar是指每一年的哪一天。
所以通常情况下,比如我想排除每年的5月1日,用AnnualCalendar就比较方便,若用HolidayCalendar,就需要注册多个HolidayCalendar到Scheduler中。
AnnualCalendar
创建与注册
// 构建SchedulerFactory实例
SchedulerFactory schedFact = new StdSchedulerFactory();
// 获取Scheduler实例
Scheduler scheduler = schedFact.getScheduler();
//创建AnnualCalendar对象
AnnualCalendar annualday = new AnnualCalendar();
Calendar laborDay = GregorianCalendar.getInstance();
//设置5月1日,这里需要注意在Calendar月份的基础上+1才是真实的月份
laborDay.set(Calendar.MONTH, Calendar.MAY+1);
laborDay.set(Calendar.DATE, 1);
// 排除每年5月1日,true为排除,false为包含
annualday .setDayExcluded(laborDay, true);
// 向Scheduler注册日历
scheduler.addCalendar("annualday", annualday, true, true);
需要注意一下几点
- AnnualCalendar就算设置了年,也不会生效,它会在每一年都生效,具体逻辑可以查看源码
- AnnualCalendar .setDayExcluded(java.util.Calendar day, boolean exclude)方法中的exclude是用来决定是包含还是排除
- setDayExcluded有一个重载的方法,可以传入一个ArrayList<java.util.Calendar>,一次性加入多个java.util.Calendar,但是该方法默认exclude为true,就是默认是排除
- Scheduler.addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)方法中的参数
replace表示是否覆盖之前设置的相同名称的Calendar ,若为false,当add相同名称的Calendar 时,会抛SchedulerException
updateTriggers表示若已存在的triggers已经引用 了相同名称的Calendar,是否更新triggers绑定的Calendar
绑定Trigger
SimpleTrigger triggerB = TriggerBuilder.newTrigger()
.withIdentity("helloTriggerB", "helloB")
.startNow()
.withSchedule(SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(5).repeatForever())
//指定trigger使用Calendar
.modifiedByCalendar("annualday")
.build();
HolidayCalendar
该日历与AnnualCalendar差不多,区别就是设置的year是有效的,如果你希望在未来5年内都排除5月1日这一天,你需要添加5个日期,分别是 2019-5-1,2020-5-1,2021-5-1,2022-5-1,2023-5-1
// 构建SchedulerFactory实例
SchedulerFactory schedFact = new StdSchedulerFactory();
// 获取Scheduler实例
Scheduler scheduler = schedFact.getScheduler();
//创建HolidayCalendar对象
HolidayCalendar holidays = new HolidayCalendar();
Calendar laborDay1 = new GregorianCalendar(2019, 5, 1);
Calendar laborDay2 = new GregorianCalendar(2020, 5, 1);
Calendar laborDay3 = new GregorianCalendar(2021, 5, 1);
Calendar laborDay4 = new GregorianCalendar(2022, 5, 1);
Calendar laborDay5 = new GregorianCalendar(2023, 5, 1);
holidays.addExcludedDate(laborDay1.getTime());
holidays.addExcludedDate(laborDay2.getTime());
holidays.addExcludedDate(laborDay3.getTime());
holidays.addExcludedDate(laborDay4.getTime());
holidays.addExcludedDate(laborDay5.getTime());
// 向Scheduler注册日历
scheduler.addCalendar("holidays", holidays, true, true);
需要注意的是
- HolidayCalendar只能做排除操作
- HolidayCalendar里面维护了一个TreeSet集合来存放通过addExcludedDate加入的排除日期,所以若是需要排除多个日期,多次调用addExcludedDate方法即可。
MonthlyCalendar
月日历,你可以定义一个月当中的若干天不触发。比如我想排除每个月的2,4,5号不触发
MonthlyCalendar monthlyCalendar = new MonthlyCalendar();
monthlyCalendar.setDayExcluded(2, true);
monthlyCalendar.setDayExcluded(4, true);
monthlyCalendar.setDayExcluded(6, true);
// 向Scheduler注册日历
scheduler.addCalendar("monthlyCalendar", monthlyCalendar, true, true);
WeeklyCalendar
星期日历,可以定义每周内周几不触发。例如设置每周四不触发
WeeklyCalendar weeklyCalendar = new WeeklyCalendar();
weeklyCalendar.setDayExcluded(Calendar.THURSDAY, true);
// 向Scheduler注册日历
scheduler.addCalendar("weeklyCalendar", weeklyCalendar, true, true);
需要注意的是
-
当我们定义了WeeklyCalendar ,周六和周日就默认被排除了,如果想要不排除,需要自己把这两天通过setDayExcluded设置为false,即不排除。可以查看如下源码
public WeeklyCalendar() { this(null, null); } public WeeklyCalendar(Calendar baseCalendar, TimeZone timeZone) { super(baseCalendar, timeZone); //默认排除周六和周日 excludeDays[java.util.Calendar.SUNDAY] = true; excludeDays[java.util.Calendar.SATURDAY] = true; excludeAll = areAllDaysExcluded(); }
-
WeeklyCalendar .setDayExcluded(int wday, boolean exclude)中的参数务必用java.util.Calendar中的定义的常量,因为国际上一周的开始第一天是周日,可以查看java.util.Calendar中定义的周常量,是从SUNDAY(1)开始的。
DailyCalendar
时间范围日历,定义一个时间范围,可以让触发器在这个时间范围内触发,或者在这个时间范围内不触发,每一个DailyCalendar的实例只能设置一次时间范围,并且这个时间范围不能超过一天的边界。
例如我要排除每天当中晚上20点到21点
// 构建SchedulerFactory实例
SchedulerFactory schedFact = new StdSchedulerFactory();
// 获取Scheduler实例
Scheduler scheduler = schedFact.getScheduler();
Calendar start = GregorianCalendar.getInstance();
start.setTime(DateBuilder.dateOf(20, 0, 0));
Calendar end = GregorianCalendar.getInstance();
end.setTime(DateBuilder.dateOf(21, 0, 0));
DailyCalendar dailyCalendar = new DailyCalendar(start, end);
dailyCalendar.setInvertTimeRange(false);
// 向Scheduler注册日历
scheduler.addCalendar("dailyCalendar", dailyCalendar, true, true);
需要注意的是
- 可以通过DailyCalendar中的参数invertTimeRange来设置在定义的时间范围内不触发,还是在定义的时间范围外不触发
根据源码中的描述invertTimeRange=false(默认值)表示在定义的时间范围内不触发
invertTimeRange=true表示在定义的时间范围外不触发 - DailyCalendar的构造参数支持直接输出字符串,比如:new DailyCalendar(“20:00:00”, “21:00:00”)
- DailyCalendar可以支持精确到毫秒级别的控制
CronCalendar
Cron表达式日历,可以写一个表达式来排除一个时间范围,比如可以设置为排除所有的时间,但是工作时间除外,也就是 在早8点-晚5点触发,其他时间暂停。
// 构建SchedulerFactory实例
SchedulerFactory schedFact = new StdSchedulerFactory();
// 获取Scheduler实例
Scheduler scheduler = schedFact.getScheduler();
try {
CronCalendar cronCalendar= new CronCalendar("* * 0-7,18-23 ? * *");
// 向Scheduler注册日历
scheduler.addCalendar("cronCalendar", cronCalendar, true, true);
} catch (ParseException e) {
e.printStackTrace();
}
组合日历
我们在构建Trigger是只能通过modifiedByCalendar(“日历的name”)方法关联一个已经注册到调度引擎的日历对象,这种情况已经能够满足我们大部分的需求。
但是如果有跟复杂的需求,比如我们需要在每周的周一至周五,排除晚上8点到24点,情况下按照Trigger配置的周期运行。可以用三种办法实现以上需求
- CronTrigger,这个稍后在详细介绍,通过构建corn表达式来实现。
- 组合日历,每中日历都有一个构造方法,可以传递一个日历对象进来,通过这样的方式可以构建一个复杂的日历对象。
- CronTrigger+组合日历的方式。
组合日历使用的例子:
// 构建SchedulerFactory实例
SchedulerFactory schedFact = new StdSchedulerFactory();
// 获取Scheduler实例
Scheduler scheduler = schedFact.getScheduler();
DailyCalendar dailyCalendar = new DailyCalendar("20:00:00", "23:59:59");
dailyCalendar.setInvertTimeRange(false);
//默认是排除周六和周日的
WeeklyCalendar weeklyCalendar = new WeeklyCalendar(dailyCalendar);
// 向Scheduler注册日历
scheduler.addCalendar("weeklyCalendar", weeklyCalendar, true, true);
写一个时间间隔的日历dailyCalendar,将其作为参数传递给weeklyCalendar就可以了,这样引擎在计算日历日期的时候会先判断dailyCalendar的时间范围,然后再判断weeklyCalendar是时间范围,当条件都满足的是否,触发器才会被触发。