在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"); }炒雞辣雞原創文章,轉載請註明來源