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

大概内容

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

系列文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容