大概内容
解析Cron表达式
- Cron表达式的语法规则
- 相关的类
- 实现原理
cron表达式的语法
Quartz的Cron表达式有6个必要的字段和1个可选的字段组成,各个字段以空格分隔。
字段名 | 允许的值 | 允许的特殊字符 |
---|---|---|
Seconds | 0-59 | , - * / |
Minutes | 0-59 | , - * / |
Hours | 0-23 | , - * / |
Day-of-month | 1-31 | , - * / ? L W |
Month | 0-11 or JAN-DEC | , - * / |
Day-of-Week | 1-7 or SUN-SAT | , - * / ? L # |
Year (Optional) | empty, 1970-2199 | , - * / |
* : 用来表示任意值
? : 只能用在“Day-of-month”和“Day-of-Week”这两个字段,表示没有特定的值
- : 用来表示范围,例如在Hours字段配置“10-12”,解析过来就是小时数为10,11和12都满足。
, : 用来表示枚举值,例如在Day-of-Week字段配置“MON,WED,FRI”,解析过来就是星期一,星期三和星期五都满足。
/ : 用来表示增量逻辑,格式为“初始值/增量值”,例如在Seconds字段配置“5/15”,解析过来就是5,20,35,50都符合。
L : last的简写,只能用在“Day-of-month”和“Day-of-Week”这两个字段,
W : weekday的简写,只能用在“Day-of-month”字段,表示最靠近指定日期的工作日(星期一到星期五)
# : 只能用在Day-of-Week字段,“m#n”表示这个月的第n个星期m。
相关的类
- CronExpression.java
- CronScheduleBuilder.java
- CronTriggerImpl.java
实现原理
转换为TreeSet对象
Cron表达式有7个字段,CronExpression把这7个字段解析为7个TreeSet的对象。
填充TreeSet对象值的时候,表达式都会转换为起始值、结束值和增量的计算模式,然后计算出匹配的值放进TreeSet对象。
举个例子,假如Cron表达式配置为:0/15 5-10 9,18 1,15 * ? 2018-2023
解析后在各个TreeSet对象的内容如下
// [0, 15, 30, 45]
protected transient TreeSet<Integer> seconds;
// [5, 6, 7, 8, 9, 10]
protected transient TreeSet<Integer> minutes;
// [9, 18]
protected transient TreeSet<Integer> hours;
// [1, 15]
protected transient TreeSet<Integer> daysOfMonth;
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 99]
// 99代表特殊字符*,此处是作为一个标记元素
protected transient TreeSet<Integer> months;
// [98]
// 98代表特殊字符?,此处是作为一个标记元素
protected transient TreeSet<Integer> daysOfWeek;
// [2018, 2019, 2020, 2021, 2022, 2023]
protected transient TreeSet<Integer> years;
CronExpression.buildExpression()
/**
* 初始化TreeSet对象
* 各个字段做storeExpressionVals()处理
* 校验DAY_OF_MONTH和DAY_OF_WEEK字段的特殊字符
* @param expression
* @throws ParseException
*/
protected void buildExpression(String expression) throws ParseException {
expressionParsed = true;
try {
// ... 初始化TreeSet对象代码
int exprOn = SECOND;
StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
false);
while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
String expr = exprsTok.nextToken().trim();
// ... 校验DAY_OF_MONTH和DAY_OF_WEEK字段的特殊字符
StringTokenizer vTok = new StringTokenizer(expr, ",");
while (vTok.hasMoreTokens()) {
String v = vTok.nextToken();
storeExpressionVals(0, v, exprOn);
}
exprOn++;
}
// ... 校验DAY_OF_MONTH和DAY_OF_WEEK字段的特殊字符
} catch (ParseException pe) {
throw pe;
} catch (Exception e) {
throw new ParseException("Illegal cron expression format ("
+ e.toString() + ")", 0);
}
}
CronExpression.storeExpressionVals()
这个方法很多校验逻辑,我们可以先重点关注addToSet()方法。
/**
* 这里会校验特殊字符-、*、/的语法
* 然后调用addToSet()方法填充符合表达式的值到TreeSet对象
* @param pos 字段文本内容的解析位置
* @param s 字段内容
* @param type 字段类型
* @return
* @throws ParseException
*/
protected int storeExpressionVals(int pos, String s, int type) throws ParseException {
int incr = 0;
int i = skipWhiteSpace(pos, s);
if (i >= s.length()) {
return i;
}
char c = s.charAt(i);
// 关键代码
// addToSet(sval, eval, incr, type);
return i;
}
CronExpression.addToSet()
/**
* 根据incr的值,计算符合表达式的值,填充到TreeSet对象
* @param val 起始值
* @param end 结束值
* @param incr 增量
* @param type 字段类型
* @throws ParseException
*/
protected void addToSet(int val, int end, int incr, int type) throws ParseException {
TreeSet<Integer> set = getSet(type);
// ... 其他校验逻辑的代码
int startAt = val;
int stopAt = end;
// ... 其他校验逻辑的代码
// 往各个TreeSet设置具体值的代码逻辑
for (int i = startAt; i <= stopAt; i += incr) {
if (max == -1) {
// ie: there's no max to overflow over
set.add(i);
} else {
// take the modulus to get the real value
int i2 = i % max;
// 1-indexed ranges should not include 0, and should include their max
if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) {
i2 = max;
}
set.add(i2);
}
}
}
给CronTriigerImpl使用
CronTriigerImpl是如何计算出nextFireTime的呢?
在构建CronTriggerImpl对象的时候,会把CronExpression对象设置为其成员变量。
通过CronExpression.getTimeAfter(Date afterTime)计算出来的。
/**
* 以seconds字段做说明:
* 1、先获取当前时间值sec
* 2、根据sec值,截取TreeSet对象seconds的大于sec值的集合st
* 3、如果集合st不为空,那么取st的第一个元素作为下次要触发的时刻值
* 4、如果集合st为空,说明sec值是集合seconds的最大值,那么读取集合seconds的头元素,下一级别字段min需要增加1
* Q: 特殊字符*的情况,会放一个99的元素到TreeSet里面,这个元素怎么处理的?
* A: 在构建TreeSet对象的时候,没有放进去超出该字段范围的值。基于前面,使用TreeSet.tailSet(),不会返回空的st。
* 在没有特殊字符*的情况,seconds的元素为0,25,50。如果当前秒数为55,则会返回空的st。
* @param afterTime
* @return
*/
public Date getTimeAfter(Date afterTime) {
// Computation is based on Gregorian year only.
Calendar cl = new java.util.GregorianCalendar(getTimeZone());
// move ahead one second, since we're computing the time *after* the
// given time
afterTime = new Date(afterTime.getTime() + 1000);
// CronTrigger does not deal with milliseconds
cl.setTime(afterTime);
cl.set(Calendar.MILLISECOND, 0);
boolean gotOne = false;
// loop until we've computed the next time, or we've past the endTime
while (!gotOne) {
//if (endTime != null && cl.getTime().after(endTime)) return null;
if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop...
return null;
}
SortedSet<Integer> st = null;
int t = 0;
int sec = cl.get(Calendar.SECOND);
int min = cl.get(Calendar.MINUTE);
// get second.................................................
st = seconds.tailSet(sec);
if (st != null && st.size() != 0) {
sec = st.first();
} else {
sec = seconds.first();
min++;
cl.set(Calendar.MINUTE, min);
}
cl.set(Calendar.SECOND, sec);
min = cl.get(Calendar.MINUTE);
int hr = cl.get(Calendar.HOUR_OF_DAY);
t = -1;
// ... 其他字段逻辑代码
gotOne = true;
} // while( !done )
return cl.getTime();
}
维护nextFireTime的字段
追踪一下CronExpression.getTimeAfter(Date afterTime)方法的调用,可以知道是CronTriigerImpl.getFireTimeAfter()方法触发的,而这个方法有4处被调用,在下面的代码位置。这些代码都有维护nextFireTime字段的逻辑:nextFireTime = getFireTimeAfter(nextFireTime)
src/main/java
org.quartz.impl.triggers
CronTriggerImpl
computeFirstFireTime(Calendar) (2 matches)
triggered(Calendar) (2 matches)
updateAfterMisfire(Calendar) (2 matches)
updateWithNewCalendar(Calendar, long) (3 matches)
willFireOn(Calendar, boolean) (2 matches)
previousFireTime = nextFireTime;
nextFireTime = getFireTimeAfter(nextFireTime);
网友评论