記不住Spring中Scheduled中的Cron語法?讓咱們看看源碼吧

在Spring源碼中,解析cron的源碼位於CronExpression中,在建立定時任務的時候,調用了CornExpression.parse方法作解析java

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表達式的神祕面紗express

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中的相關定義。ide

// 秒
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表達式解析規則源碼分析

一、cron表達式能夠由 秒 分 時 日 月 年 六部分註冊,每一個部分由空格隔開。系統中定義了一組用@開頭的字符串來替代標準Cron表達式,不過個數有限this

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 * * * *"
};

例如:url

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

二、對於每一項,能夠用逗號隔開,用來表示不一樣的時間點code

例如:orm

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

三、對於每一項,可使用橫槓隔開,用來表示時間段token

例如:

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

四、對於每一項,可使用斜槓+橫槓的組合,表示在這段時間內,以斜槓後的值爲公差的時間點

例如:

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

五、對於每一項,使用星號表示當前時間類型的整個範圍

例如:

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

炒雞辣雞原創文章,轉載請註明來源

相關文章
相關標籤/搜索