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

chaojilaji 2020-11-06 20:59:01
java delta


在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");
}

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

版权声明
本文为[chaojilaji]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/3773302/blog/4704472

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云