美文网首页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