美文网首页SpringMVCCron表达式
Quartz 源码解析(六) —— 解析Cron表达式

Quartz 源码解析(六) —— 解析Cron表达式

作者: icameisaw | 来源:发表于2018-09-15 16:56 被阅读709次

大概内容

解析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);      

系列文章

相关文章

网友评论

    本文标题:Quartz 源码解析(六) —— 解析Cron表达式

    本文链接:https://www.haomeiwen.com/subject/vqltnftx.html