记不住Spring中Scheduled中的Cron语法?让我们看看源码吧

在Spring源码中,解析cron的源码位于CronExpression中,在创建定时任务的时候,调用了CornExpression.parse方法做解析

public CronTrigger(String expression, ZoneId zoneId) {
    Assert.hasLength(expression, "Expression must not be empty");
    Assert.notNull(zoneId, "ZoneId must not be null");

    this.expression = CronExpression.parse(expression);
    this.zoneId = zoneId;
}

那现在就让我们揭开解析cron表达式的神秘面纱

public static CronExpression parse(String expression) {
    Assert.hasLength(expression, "Expression string must not be empty");
    // 如果 expression 是注解形式,就将注解替换为下面的形式(见尾部)
    expression = resolveMacros(expression);

    // StringUtils.tokenizeToStringArray 与 split方法功能差不多
    String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
    if (fields.length != 6) {
        // cron表达式必须由六项组成
        throw new IllegalArgumentException(String.format(
            "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
    }
    try {
        CronField seconds = CronField.parseSeconds(fields[0]); // 第一项是秒
        CronField minutes = CronField.parseMinutes(fields[1]); // 第二项是分
        CronField hours = CronField.parseHours(fields[2]); // 第三项是时
        CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]); // 第四项是日
        CronField months = CronField.parseMonth(fields[4]); // 第五项是月
        CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]); // 第六项是年

        return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
    }
    catch (IllegalArgumentException ex) {
        String msg = ex.getMessage() + " in cron expression \"" + expression + "\"";
        throw new IllegalArgumentException(msg, ex);
    }
}

// resolveMacros 函数
private static String resolveMacros(String expression) {
    expression = expression.trim();
    for (int i = 0; i < MACROS.length; i = i + 2) {
        if (MACROS[i].equalsIgnoreCase(expression)) {
            return MACROS[i + 1];
        }
    }
    return expression;
}

private static final String[] MACROS = new String[] {
    "@yearly", "0 0 0 1 1 *",
    "@annually", "0 0 0 1 1 *",
    "@monthly", "0 0 0 1 * *",
    "@weekly", "0 0 0 * * 0",
    "@daily", "0 0 0 * * *",
    "@midnight", "0 0 0 * * *",
    "@hourly", "0 0 * * * *"
};

现在,cron表达式的顺序我们就记住,必须是六项,顺序是 秒,分,时,日,月,年或者用系统中定义的MACROS来代替,六项中间用空格隔开。那么究竟每一项是怎么解析和表达的呢?来看看CronField中的相关定义。

// 秒
public static CronField parseSeconds(String value) {
    return BitsCronField.parseSeconds(value);
}

// 这调用栈就跟套娃一样
public static BitsCronField parseSeconds(String value) {
    return parseField(value, Type.SECOND);
}

private static BitsCronField parseField(String value, Type type) {
    Assert.hasLength(value, "Value must not be empty");
    Assert.notNull(type, "Type must not be null");
    try {
        BitsCronField result = new BitsCronField(type);
        // 将字符串按照逗号分隔,也就是,我们在每一项里面都可以用逗号来隔断,代表不同的时间
        String[] fields = StringUtils.delimitedListToStringArray(value, ",");
        for (String field : fields) {
            int slashPos = field.indexOf('/');
            // 判断时间中有没有斜杠
            if (slashPos == -1) {
                // 如果没有,就解析并设置时间范围
                ValueRange range = parseRange(field, type);
                result.setBits(range);
            }
            else {
                String rangeStr = value.substring(0, slashPos);
                String deltaStr = value.substring(slashPos + 1);
                // 根据斜杠前的内容解析并创建时间范围
                ValueRange range = parseRange(rangeStr, type);
                if (rangeStr.indexOf('-') == -1) {
                    // 如果斜杠前的表达式不包含横杠,则将当前range的结束时间设置为当前类型的最大值
                    range = ValueRange.of(range.getMinimum(), type.range().getMaximum());
                }
                int delta = Integer.parseInt(deltaStr);
                if (delta <= 0) {
                    throw new IllegalArgumentException("Incrementer delta must be 1 or higher");
                }
                // 将delta带入进去设置时间范围
                result.setBits(range, delta);
            }
        }
        return result;
    }
    catch (DateTimeException | IllegalArgumentException ex) {
        String msg = ex.getMessage() + " '" + value + "'";
        throw new IllegalArgumentException(msg, ex);
    }
}

// parseRange

private static ValueRange parseRange(String value, Type type) {
    if (value.equals("*")) {
        // 如果是*号,则直接返回该类型的range()
        return type.range();
    }
    else {
        int hyphenPos = value.indexOf('-');
        if (hyphenPos == -1) {
            int result = type.checkValidValue(Integer.parseInt(value));
            // 如果没有横杠,那么时间段的开始和结束都是当前事件点
            return ValueRange.of(result, result);
        }
        else {
            // 如果有横杠,那么时间段的开始为横杠前数字,结束就是横杠后的数字
            int min = Integer.parseInt(value.substring(0, hyphenPos));
            int max = Integer.parseInt(value.substring(hyphenPos + 1));
            min = type.checkValidValue(min); // 校验
            max = type.checkValidValue(max); // 校验
            return ValueRange.of(min, max);
        }
    }
}

// setBits 方法,BitsCronField 在实现的时候用一个长整型的bits来存储一个时间位
private void setBits(ValueRange range) {
    // 如果没有delta
    if (range.getMinimum() == range.getMaximum()) {
        // 如果是一个时间点,由于我们的bits的默认值是0,所以这里的语义就是直接将bits的第range.getMinimum()位,置为1
        setBit((int) range.getMinimum());
    }
    else {
        // 如果是一个时间段,则将Mask左移range.getMinimum()位的值设置为minMask
        // 将Mask无符号右移 - (range.getMaximum() + 1) 位
        // private static final long MASK = 0xFFFFFFFFFFFFFFFFL;
        // 这里整得很复杂是为了避免右移溢出的问题,但是本质上也是在bits的 range.getMinimum() 和 range.getMaximum() 位,置为1
        long minMask = MASK << range.getMinimum();
        long maxMask = MASK >>> - (range.getMaximum() + 1);
        this.bits |= (minMask & maxMask);
    }
}

// 有斜杠的情况调用这个方法
private void setBits(ValueRange range, int delta) {
    if (delta == 1) {
        // 如果有delta,且为1,则跟没有没区别
        setBits(range);
    }
    else {
        // 如果delta不为1,则按照delta为公差设置位置1
        for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) {
            setBit(i);
        }
    }
}

// 获取当前bits与(1L << index) 按位或的结果,按位或就是 有一则一
// 我们知道,基本类型都是有默认值的,long型的默认值是0
// 例如,如果是一个时间点,由于我们的bits的默认值是0,所以这里的语义就是直接将bits的第range.getMinimum()位置为1
private void setBit(int index) {
    this.bits |= (1L << index);
}

刚刚里面调用了type.range方法,根据调用栈,最终会来到ChronoField枚举中,也就是说,如果是星号,返回的就是当前解析类型的整个事件范围。从这里我们可以看出,星号代表所有当前解析类型的所有时间,如果表达式中有横杠,那么就代表一个时间段,如果是一个纯数字,那么就代表那个时间点。

public enum ChronoField implements TemporalField {
    NANO_OF_SECOND("NanoOfSecond", NANOS, SECONDS, ValueRange.of(0, 999_999_999)),
    NANO_OF_DAY("NanoOfDay", NANOS, DAYS, ValueRange.of(0, 86400L * 1000_000_000L - 1)),
    MICRO_OF_SECOND("MicroOfSecond", MICROS, SECONDS, ValueRange.of(0, 999_999)),
    MICRO_OF_DAY("MicroOfDay", MICROS, DAYS, ValueRange.of(0, 86400L * 1000_000L - 1)),
    MILLI_OF_SECOND("MilliOfSecond", MILLIS, SECONDS, ValueRange.of(0, 999)),
    MILLI_OF_DAY("MilliOfDay", MILLIS, DAYS, ValueRange.of(0, 86400L * 1000L - 1)),
    SECOND_OF_MINUTE("SecondOfMinute", SECONDS, MINUTES, ValueRange.of(0, 59), "second"),
    SECOND_OF_DAY("SecondOfDay", SECONDS, DAYS, ValueRange.of(0, 86400L - 1)),
    MINUTE_OF_HOUR("MinuteOfHour", MINUTES, HOURS, ValueRange.of(0, 59), "minute"),
    MINUTE_OF_DAY("MinuteOfDay", MINUTES, DAYS, ValueRange.of(0, (24 * 60) - 1)),
    HOUR_OF_AMPM("HourOfAmPm", HOURS, HALF_DAYS, ValueRange.of(0, 11)),
    CLOCK_HOUR_OF_AMPM("ClockHourOfAmPm", HOURS, HALF_DAYS, ValueRange.of(1, 12)),
    HOUR_OF_DAY("HourOfDay", HOURS, DAYS, ValueRange.of(0, 23), "hour"),
    CLOCK_HOUR_OF_DAY("ClockHourOfDay", HOURS, DAYS, ValueRange.of(1, 24)),
    AMPM_OF_DAY("AmPmOfDay", HALF_DAYS, DAYS, ValueRange.of(0, 1), "dayperiod"),
    DAY_OF_WEEK("DayOfWeek", DAYS, WEEKS, ValueRange.of(1, 7), "weekday"),
    ALIGNED_DAY_OF_WEEK_IN_MONTH("AlignedDayOfWeekInMonth", DAYS, WEEKS, ValueRange.of(1, 7)),
    ALIGNED_DAY_OF_WEEK_IN_YEAR("AlignedDayOfWeekInYear", DAYS, WEEKS, ValueRange.of(1, 7)),
    DAY_OF_MONTH("DayOfMonth", DAYS, MONTHS, ValueRange.of(1, 28, 31), "day"),
    DAY_OF_YEAR("DayOfYear", DAYS, YEARS, ValueRange.of(1, 365, 366)),
    EPOCH_DAY("EpochDay", DAYS, FOREVER, ValueRange.of((long) (Year.MIN_VALUE * 365.25), (long) (Year.MAX_VALUE * 365.25))),
    ALIGNED_WEEK_OF_MONTH("AlignedWeekOfMonth", WEEKS, MONTHS, ValueRange.of(1, 4, 5)),
    ALIGNED_WEEK_OF_YEAR("AlignedWeekOfYear", WEEKS, YEARS, ValueRange.of(1, 53)),
    MONTH_OF_YEAR("MonthOfYear", MONTHS, YEARS, ValueRange.of(1, 12), "month"),
    PROLEPTIC_MONTH("ProlepticMonth", MONTHS, FOREVER, ValueRange.of(Year.MIN_VALUE * 12L, Year.MAX_VALUE * 12L + 11)),
    YEAR_OF_ERA("YearOfEra", YEARS, FOREVER, ValueRange.of(1, Year.MAX_VALUE, Year.MAX_VALUE + 1)),
    YEAR("Year", YEARS, FOREVER, ValueRange.of(Year.MIN_VALUE, Year.MAX_VALUE), "year"),
    ERA("Era", ERAS, FOREVER, ValueRange.of(0, 1), "era"),
    INSTANT_SECONDS("InstantSeconds", SECONDS, FOREVER, ValueRange.of(Long.MIN_VALUE, Long.MAX_VALUE)),
    OFFSET_SECONDS("OffsetSeconds", SECONDS, FOREVER, ValueRange.of(-18 * 3600, 18 * 3600));

    private final String name;
    private final TemporalUnit baseUnit;
    private final TemporalUnit rangeUnit;
    private final ValueRange range;
    private final String displayNameKey;

    private ChronoField(String name, TemporalUnit baseUnit, TemporalUnit rangeUnit, ValueRange range) {
        this.name = name;
        this.baseUnit = baseUnit;
        this.rangeUnit = rangeUnit;
        this.range = range;
        this.displayNameKey = null;
    }

    private ChronoField(String name, TemporalUnit baseUnit, TemporalUnit rangeUnit,
            ValueRange range, String displayNameKey) {
        this.name = name;
        this.baseUnit = baseUnit;
        this.rangeUnit = rangeUnit;
        this.range = range;
        this.displayNameKey = displayNameKey;
    }

   // ... ...

    @Override
    public ValueRange range() {
        return range;
    }
    // ... ...
}

得出规则

从上面的源码分析,我们可以总结出这样一套cron表达式解析规则

1、cron表达式可以由 秒 分 时 日 月 年 六部分注册,每个部分由空格隔开。系统中定义了一组用@开头的字符串来替代标准Cron表达式,不过个数有限

private static final String[] MACROS = new String[] {
    "@yearly", "0 0 0 1 1 *",
    "@annually", "0 0 0 1 1 *",
    "@monthly", "0 0 0 1 * *",
    "@weekly", "0 0 0 * * 0",
    "@daily", "0 0 0 * * *",
    "@midnight", "0 0 0 * * *",
    "@hourly", "0 0 * * * *"
};

例如:

@Scheduled(cron = "@yearly")
public void test(){
    logger.info("123");
}

2、对于每一项,可以用逗号隔开,用来表示不同的时间点

例如:

@Scheduled(cron = "1,2,3 0 0 * * *")
public void test(){
    logger.info("123");
}

3、对于每一项,可以使用横杠隔开,用来表示时间段

例如:

@Scheduled(cron = "1,2-4,5 0 0 * * *")
public void test(){
    logger.info("123");
}

4、对于每一项,可以使用斜杠+横杠的组合,表示在这段时间内,以斜杠后的值为公差的时间点

例如:

@Scheduled(cron = "1,2-20/3,5 0 0 * * *")
public void test(){
    logger.info("123");
}

5、对于每一项,使用星号表示当前时间类型的整个范围

例如:

@Scheduled(cron = "1,2-20/3,5 * * * * *")
public void test(){
    logger.info("123");
}

炒鸡辣鸡原创文章,转载请注明来源

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