记不住 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); }}// parseRangeprivate 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()位置为1private 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");}
看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
关注公众号 『 java烂猪皮 』,不定期分享原创知识。
同时可以期待后续文章ing🚀
炒鸡辣鸡原创文章
出处:https://my.oschina.net/u/3773302/blog/4704472
AI乔治
分享后端技术干货。公众号【 Java烂猪皮】 2019.06.30 加入
一名默默无闻的扫地僧!
评论