1.cron表达式

cron表达式是用来配置spring定时任务执行时间的字符串,由5个空格分隔成的6个域构成,格式如下:

{秒}  {分}  {时}  {日}  {月}  {周}

每一个域的含义解释:
1)秒:表示在指定的秒数触发定时任务,范围0-59。例如,"*"表示任何秒都触发,"0,3"表示0秒和3秒触发。
2)分:表示在指定的分钟触发定时任务,范围0-59。例如,"0-3"表示0分钟到3分钟每分钟都触发,"0/2"表示只有偶数分钟触发。
3)时:表示在指定的小时触发定时任务,范围0-23。例如,"3-15/2"表示上午3点到下午3点每隔2个小时触发。
4)日:表示在指定的日期触发定时任务,范围1-31(可以写0,但不会生效)。例如,"1"表示1号出发,"5,15"表示5号和15号出发。需要注意的是,日期可以写0,不会报错但也不会生效。
5)月:表示在指定的月份触发定时任务,范围1-12。例如,"1-4,12"表示1月到4月以及12月触发。
6)周:表示在指定的星期触发定时任务,范围0-7(0和7都表示周日)。例如,"?"表示一周都触发,"6,7"表示周六日触发。
注意,1月到12月可以用对应的英文缩写JAN-DEC代替,周日到周六可以用对应的英文缩写SUN-SAT代替。但是,周日的缩写SUN只会被替换为0,因此在cron表达式的周域,我们可以写6-7,却不能写SAT-SUN。

表1-1总结了cron表达式中域的范围和可能出现的特殊符号:

表1-1

范围
特殊字符
是否必需
0-59
, - * /
Y
0-59
, - * /
Y
0-23
, - * /
Y
1-31
, - * / ?
Y
1-12或JAN-DEC
, - * /
Y
0-7或SUN-SAT
, - * / ?
Y

特殊字符的含义说明如下:
1)"*":匹配该域的任意值,例如在日域上使用"*",则表示每天都触发该定时任务。
2)"?":只能在日和周域使用,表示非明确的值,实际作用等同"*",即匹配任意值。一般在日和周上会出现一次,当然,如果你对日和周两个域都使用"?"或者都使用其他值也没什么问题。
3)"-":表示范围,例如在分域上使用5-10表示从5分钟到10分钟每分钟触发一次。
4)"/":表示起始时间触发一次,然后每隔固定时间触发一次。例如,在分钟域使用"10/2"表示从10分钟开始每隔2分钟触发一次,直    到58分钟。也可以和字符"-"连用,例如在分钟域使用"10-30/2"表示从10分钟开始每隔2分钟触发一次,直到30分钟。
5)",":表示枚举多个值,这些值之间是"或"的关系。例如,在月份上使用"1-3,10,12"表示1月到3月,10月,12月都触发。

下面是一些cron表达式和对应的含义:
"0 15 10 ? * *"  每天上午10:15触发
"0 0/5 14 * * ?"  在每天下午2点到下午2:55期间的每5分钟触发
"0 0-5 14 * * ?"  每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED"  三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI"  周一至周五的上午10:15触发

2.cron定时任务的调度

在说cron表达式的解析过程之前,先了解一下spring的cron定时任务调度大体框架。图2-1是cron定时任务涉及的主要类及他们之间的关系。左边的红色部分包括三个类Trigger,CronTrigger,CronsequenceGenerator,它们解决的问题是如何根据任务的上一次执行时间,计算出符合cron表达式的下一次执行时间,即nextExcutionTime接口。

CronSequenceGenerator负责解析用户配置的cron表达式,并提供next接口,即根据给定时间获取符合cron表达式规则的最近的下一个时间。CronTrigger实现Trigger的nextExecutionTime接口,根据定时任务执行的上下文环境(最近调度时间和最近完成时间)决定查找下一次执行时间的左边界,之后调用CronSequenceGenerator的next接口从左边界开始找下一次的执行时间。

右边的橙色部分包括四个类Runnable,ReschedulingRunable,ScheduledExecutorService,ScheduledThreadPoolExecutor。解决的问题是当计算出定时任务的执行时间序列之后,如何沿着这个时间序列不断的执行定时任务。ReschedulingRunnable的主要接口包括schedule方法和run方法。schedule方法根据CronTrigger的nextExecutionTime接口返回的下一次执行时间,计算与当前时间的相对延迟时间delay,然后调用ScheduledExecutorService的schedule延迟执行方法对当前任务延调度。当该任务真正被执行时,运行ReschedulingRunnable的run方法。run方法首先执行用户任务,当本次用户任务执行完成之后,再调用schedule方法,继续调度当前任务。这样以来,用户任务就能够沿着计算出的执行时间序列,一次又一次的执行。

                                                        图2-1

3.cron表达式解析过程

在图2-1中,CronsequenceGenerator负责解析cron表达式并提供next接口。

3.1 cron位数组

cron表达式本身是一个字符串,虽然对于我们人来说直观易懂,但是对于计算机却并不十分友好。因此,在CronSequenceGenerator中使用与cron表达式含有等价信息的cron位数组来表示匹配规则,如下图所示。对于cron表达式中的秒,分,时,日,月,周六个域,CronSequenceGenerator分别对应设置了seconds,minutes,hours,daysOfMonth,months,daysOfWeek六个位数组。大体思路是:对于某个域,如果数字value是一个匹配值,则将位数组的第value位设置为1,否则设置0。
(注:为什么使用位数组,而不使用list,set之类的容易的,一方面是空间效率,更重要的是接下来的操作主要是判断某个值是否匹配和从某个值开始找最近的下一个能够匹配的值,这两个操作对于list和set并不是很简单)

              图3-1  cron位数组,灰色表示无效位

CronSequenceGenerator的parse方法具体负责将cron表达式解析成cron位数组。首先根据空格分隔cron表达式,得到秒分时日月周6个域分别对应的子cron表达式。对于秒分时三个域的解析使用基础解析算法处理,基础解析算法只处理","、"*"、"-"、"/"四个字符,如图3-2所示:

图3-2  基础解析算法

基础解析算法源码:

  1. private void setNumberHits(BitSet bits, String value, int min, int max) {
  2. String[] fields = StringUtils.delimitedListToStringArray(value, ",");
  3. for (String field : fields) {
  4. if (!field.contains("/")) {
  5. // Not an incrementer so it must be a range (possibly empty)
  6. int[] range = getRange(field, min, max);
  7. bits.set(range[0], range[1] + 1);
  8. }
  9. else {
  10. String[] split = StringUtils.delimitedListToStringArray(field, "/");
  11. if (split.length > 2) {
  12. throw new IllegalArgumentException("Incrementer has more than two fields: '" +
  13. field + "' in expression \"" + this.expression + "\"");
  14. }
  15. int[] range = getRange(split[0], min, max);
  16. if (!split[0].contains("-")) {
  17. range[1] = max - 1;
  18. }
  19. int delta = Integer.valueOf(split[1]);
  20. for (int i = range[0]; i <= range[1]; i += delta) {
  21. bits.set(i);
  22. }
  23. }
  24. }
  25. }
  26. private int[] getRange(String field, int min, int max) {
  27. int[] result = new int[2];
  28. if (field.contains("*")) {
  29. result[0] = min;
  30. result[1] = max - 1;
  31. return result;
  32. }
  33. if (!field.contains("-")) {
  34. result[0] = result[1] = Integer.valueOf(field);
  35. }
  36. else {
  37. String[] split = StringUtils.delimitedListToStringArray(field, "-");
  38. if (split.length > 2) {
  39. throw new IllegalArgumentException("Range has more than two fields: '" +
  40. field + "' in expression \"" + this.expression + "\"");
  41. }
  42. result[0] = Integer.valueOf(split[0]);
  43. result[1] = Integer.valueOf(split[1]);
  44. }
  45. if (result[0] >= max || result[1] >= max) {
  46. throw new IllegalArgumentException("Range exceeds maximum (" + max + "): '" +
  47. field + "' in expression \"" + this.expression + "\"");
  48. }
  49. if (result[0] < min || result[1] < min) {
  50. throw new IllegalArgumentException("Range less than minimum (" + min + "): '" +
  51. field + "' in expression \"" + this.expression + "\"");
  52. }
  53. return result;
  54. }

对于日期,先将该域的子cron表达式中出现的字符"?"替换成"*",然后使用基础解析算法进行处理。日期的范围是1-31,因此位数组的第0位是用不到的,在基础解析算法之后进行清除。位数组的第0位最后会清除。

源码:

  1. private void setDaysOfMonth(BitSet bits, String field) {
  2. int max = 31;
  3. // Days of month start with 1 (in Cron and Calendar) so add one
  4. setDays(bits, field, max + 1);
  5. // ... and remove it from the front
  6. bits.clear(0);
  7. }
  8. private void setDays(BitSet bits, String field, int max) {
  9. if (field.contains("?")) {
  10. field = "*";
  11. }
  12. setNumberHits(bits, field, 0, max);

}

对于月份,先将该域的英文缩写JAN-DEC替换成对应的数字(1-12),然后使用基础解析算法进行处理。但是由于cron表达式中配置的月份范围是1-12,Calendar中的月份范围是0-11,所以为了后续算法使用方便,在基础解析算法处理完之后将months位数组整体左移1位。

源码:

  1. private void setMonths(BitSet bits, String value) {
  2. int max = 12;
  3. value = replaceOrdinals(value, "FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC");
  4. BitSet months = new BitSet(13);
  5. // Months start with 1 in Cron and 0 in Calendar, so push the values first into a longer bit set
  6. setNumberHits(months, value, 1, max + 1);
  7. // ... and then rotate it to the front of the months
  8. for (int i = 1; i <= max; i++) {
  9. if (months.get(i)) {
  10. bits.set(i - 1);
  11. }
  12. }
  13. }

对于星期,先将该域的英文缩写SUN-SAT替换成对应的数字(0-6),接着将该域中的字符"?"替换成"*",然后使用基础解析算法处理。最后,由于周日对应的值有两个0和7,因此对daysOfWeek位数组的第0位和第7位取或,将结果保存到第0位,并清除第7位。(Calendar的星期范围是1-7,为什么使用第0-6位,不使用1-7位呢)

源码:

  1. setDays(this.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8);
  2. if (this.daysOfWeek.get(7)) {
  3. // Sunday can be represented as 0 or 7
  4. this.daysOfWeek.set(0);
  5. this.daysOfWeek.clear(7);
  6. }
  7. private void setDays(BitSet bits, String field, int max) {
  8. if (field.contains("?")) {
  9. field = "*";
  10. }
  11. setNumberHits(bits, field, 0, max);
  12. }
举个例子,图3-3是cron表达式"0 59 21 ? * MON-FRI"(周一至周五的下午21:59:00触发)解析后得到的位数组,红色表示1,白色表示0,灰色表示用不到。

图3-3

3.2 doNext算法

CronSequenceGenerator的doNext算法从指定时间开始(包括指定时间)查找符合cron表达式规则下一个匹配的时间。如图3-4所示,其整体思路是:

沿着秒→分→时→日→月逐步检查指定时间的值。如果所有域上的值都已经符合规则那么指定时间符合cron表达式,算法结束。否则,必然有某个域的值不符合规则,调整该域到下一个符合规则的值(可能调整更高的域),并将较低域的值调整到最小值,然后从秒开始重新检查和调整。(假如需要多次调整日月的话,秒分时岂不是要做很多次无用功?)

图3-4 doNext算法

具体实现上,对于秒,分,时,月四个范围固定的四个域,调用findNext方法从对应的位数组中从当前值开始(包括当前值)查找下一个匹配值,有三种情况:

1)下一个匹配值就是当前值,则匹配通过,如果当前域是月则算法结束,否则继续处理下一个更高的域。
2)下一个匹配值不是当前值但也不是-1,则将当前域设置为下一个匹配值,将比当前域低的所有域设置为最小值,递归调度本算法(如果是月份且年份超过原始年份4年以上则抛异常)。(递归之后不知道为什么没有return,其实递归调度结束后当前的执行过程就可以结束了)
3)下一个匹配值是-1,则将对更高的域做加1操作,从0开始查找下一个匹配值(肯定能找到,要不cron表达式不合法,解析阶段就抛异常了),将当前域
   设置为下一个匹配值,重置比当前域低的所有域设置为最小值,递归调度本算法(如果是月份且年份超过原始年份4年以上则抛异常)。
对于时间中的日,则情况比较复杂,比如从2016年1月31日开始找下一个30日的周五(虽然同时设置日和周的情况比较少见),则仅仅调整一次月份是无法找到下一个匹配的日期的。
spring的实现方案是从当前时间开始连续搜索366天,匹配规则是日期和周同时匹配,有三种结果:
1)找不到下一个匹配的日期,则抛异常。
2)找到下一个匹配的日期且与当前日期相等,则继续处理月份。(应该多判断一下月份和年份,万一月份或年份被调整了呢?)
3)找到下一个匹配的日期且与当前日期不等,则重置比日期低的域为最小值,并递归调度doNext算法。
(日期的处理略粗糙,总感觉打开的方式不对..)

doNext算法源码:

  1. private void doNext(Calendar calendar, int dot) {
  2. List<Integer> resets = new ArrayList<Integer>();
  3. int second = calendar.get(Calendar.SECOND);
  4. List<Integer> emptyList = Collections.emptyList();
  5. int updateSecond = findNext(this.seconds, second, calendar, Calendar.SECOND, Calendar.MINUTE, emptyList);
  6. if (second == updateSecond) {
  7. resets.add(Calendar.SECOND);
  8. }
  9. int minute = calendar.get(Calendar.MINUTE);
  10. int updateMinute = findNext(this.minutes, minute, calendar, Calendar.MINUTE, Calendar.HOUR_OF_DAY, resets);
  11. if (minute == updateMinute) {
  12. resets.add(Calendar.MINUTE);
  13. }
  14. else {
  15. doNext(calendar, dot);
  16. }
  17. int hour = calendar.get(Calendar.HOUR_OF_DAY);
  18. int updateHour = findNext(this.hours, hour, calendar, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_WEEK, resets);
  19. if (hour == updateHour) {
  20. resets.add(Calendar.HOUR_OF_DAY);
  21. }
  22. else {
  23. doNext(calendar, dot);
  24. }
  25. int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
  26. int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
  27. int updateDayOfMonth = findNextDay(calendar, this.daysOfMonth, dayOfMonth, daysOfWeek, dayOfWeek, resets);
  28. if (dayOfMonth == updateDayOfMonth) {
  29. resets.add(Calendar.DAY_OF_MONTH);
  30. }
  31. else {
  32. doNext(calendar, dot);
  33. }
  34. int month = calendar.get(Calendar.MONTH);
  35. int updateMonth = findNext(this.months, month, calendar, Calendar.MONTH, Calendar.YEAR, resets);
  36. if (month != updateMonth) {
  37. if (calendar.get(Calendar.YEAR) - dot > 4) {
  38. throw new IllegalArgumentException("Invalid cron expression \"" + this.expression +
  39. "\" led to runaway search for next trigger");
  40. }
  41. doNext(calendar, dot);
  42. }
  43. }
  44. private int findNextDay(Calendar calendar, BitSet daysOfMonth, int dayOfMonth, BitSet daysOfWeek, int dayOfWeek,
  45. List<Integer> resets) {
  46. int count = 0;
  47. int max = 366;
  48. // the DAY_OF_WEEK values in java.util.Calendar start with 1 (Sunday),
  49. // but in the cron pattern, they start with 0, so we subtract 1 here
  50. while ((!daysOfMonth.get(dayOfMonth) || !daysOfWeek.get(dayOfWeek - 1)) && count++ < max) {
  51. calendar.add(Calendar.DAY_OF_MONTH, 1);
  52. dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
  53. dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
  54. reset(calendar, resets);
  55. }
  56. if (count >= max) {
  57. throw new IllegalArgumentException("Overflow in day for expression \"" + this.expression + "\"");
  58. }
  59. return dayOfMonth;
  60. }
  61. /**
  62. * Search the bits provided for the next set bit after the value provided,
  63. * and reset the calendar.
  64. * @param bits a {@link BitSet} representing the allowed values of the field
  65. * @param value the current value of the field
  66. * @param calendar the calendar to increment as we move through the bits
  67. * @param field the field to increment in the calendar (@see
  68. * {@link Calendar} for the static constants defining valid fields)
  69. * @param lowerOrders the Calendar field ids that should be reset (i.e. the
  70. * ones of lower significance than the field of interest)
  71. * @return the value of the calendar field that is next in the sequence
  72. */
  73. private int findNext(BitSet bits, int value, Calendar calendar, int field, int nextField, List<Integer> lowerOrders) {
  74. int nextValue = bits.nextSetBit(value);
  75. // roll over if needed
  76. if (nextValue == -1) {
  77. calendar.add(nextField, 1);
  78. reset(calendar, Arrays.asList(field));
  79. nextValue = bits.nextSetBit(0);
  80. }
  81. if (nextValue != value) {
  82. calendar.set(field, nextValue);
  83. reset(calendar, lowerOrders);
  84. }
  85. return nextValue;
  86. }
  87. /**
  88. * Reset the calendar setting all the fields provided to zero.
  89. */
  90. private void reset(Calendar calendar, List<Integer> fields) {
  91. for (int field : fields) {
  92. calendar.set(field, field == Calendar.DAY_OF_MONTH ? 1 : 0);
  93. }
  94. }

(注:源码中对秒的处理与图3-4不一致,当下一个匹配的秒数与当前值不等时没有递归调用。当cron表达式为"0/2 1 * * * * *",指定时间为2016-12-25 18:00:45时,doNext算法计算出的下一个匹配时间为2016-12-25 18:01:46,正确的结果是2016-12-25 18:01:00。可能是源码少写了一行代码)

3.3 next接口

next接口首先调用doNext方法从指定时间开始(包括该指定时间)计算出下一个符合cron表达式规则的时间,如果doNext的结果和指定时间不等则直接返回,如果相等则对指定时间加一秒,然后重新调用doNext算法计算下一个时间并返回(重新计算出的时间肯定和指定时间不等了)。
(注:为什么不直接先加1秒,然后doNext呢(addSecond→doNext)?原因是虽然这样代码更简洁而且能得到正确结果但是效率相对更低。原因对Calendar表示的时间加1秒来说其实是个相对复杂的工作。另外,一般情况下指定时间不符合cron表达式的概率很大(毕竟配置6个*号也不多见),所以只执行doNext的概率比执行doNext→addSecond→doNet的概率要大得多(类似6个*这种cron配置情况除外)。另外当执行doNext→addSecond→doNext时,说明指定时间是匹配cron表达式的,当指定时间匹配cron表达式的时候,doNext仅仅对6个域分别做了一次check而已,没有递归调用,耗时可以忽略不计。这样算下来,doNext→addSecond→doNext虽然代码看起来更复杂,但效率更高一些。)

next接口源码:

  1. /**
  2. * Get the next {@link Date} in the sequence matching the Cron pattern and
  3. * after the value provided. The return value will have a whole number of
  4. * seconds, and will be after the input value.
  5. * @param date a seed value
  6. * @return the next value matching the pattern
  7. */
  8. public Date next(Date date) {
  9. /*
  10. The plan:
  11. 1 Round up to the next whole second
  12. 2 If seconds match move on, otherwise find the next match:
  13. 2.1 If next match is in the next minute then roll forwards
  14. 3 If minute matches move on, otherwise find the next match
  15. 3.1 If next match is in the next hour then roll forwards
  16. 3.2 Reset the seconds and go to 2
  17. 4 If hour matches move on, otherwise find the next match
  18. 4.1 If next match is in the next day then roll forwards,
  19. 4.2 Reset the minutes and seconds and go to 2
  20. ...
  21. */
  22. Calendar calendar = new GregorianCalendar();
  23. calendar.setTimeZone(this.timeZone);
  24. calendar.setTime(date);
  25. // First, just reset the milliseconds and try to calculate from there...
  26. calendar.set(Calendar.MILLISECOND, 0);
  27. long originalTimestamp = calendar.getTimeInMillis();
  28. doNext(calendar, calendar.get(Calendar.YEAR));
  29. if (calendar.getTimeInMillis() == originalTimestamp) {
  30. // We arrived at the original timestamp - round up to the next whole second and try again...
  31. calendar.add(Calendar.SECOND, 1);
  32. doNext(calendar, calendar.get(Calendar.YEAR));
  33. }
  34. return calendar.getTime();
  35. }

4.spring解析算法存在的问题

当前的cron解析算法,主要是doNext算法,存在的问题总结如下表:

表4-1

编号
问题
后果
1 对秒的处理有漏洞,当秒域调整之后,没有递归调度doNext算法。 导致bug,见3.2最后的问题说明。
2 在递归调用doNext方法结束之后,时间已经调整到预期值,但当前方法还会继续执行 影响效率,虽然不是很严重。 全部
3 找下一个匹配的日期,最多查找366天 方法略粗糙,而且多了一个限制
4 找到下一个匹配日期后,只判断日期域是否和指定时间的日期相等,而没有判断月份和年份是否修改。 当月份和年份被修改,而日期不变的情况下,不会递归调用doNext方法
5 从低域(秒)到高域(月)的处理过程 如果日月调整次数比较多,则秒分时上的无效调整会做很多无用功,并影响效率。 全部

5.新的doNext算法

新的doNext算法的思路主要是按照月→日→时→分→秒的顺序,对指定时间按照规则进行调整,如图5-1所示。主要思路是:当执行到某一个域时,先判断是否有更高的域已经调整过,如果更高的域调整过则我们只需要将该域设置为符合规则的最小值即可。如果更高的域都没有调整过,则判断当前域的值是否符合匹配规则。如果不匹配则调整该域的值,并通知更低的域其已经被调整过;如果匹配则进入下一个域的执行逻辑。

图5-1

图5-1可以看出,关键是如何判断某个域的值是否匹配cron表达式,以及当某个域的值不匹配时如何调整该域到下一个最近匹配的值,这两个操作称为检查操作和调整操作。
在检查操作中,假如某个域的值是value。对于月时分秒四个域只需要判断位数组的第value位是否为1即可,而对于日期,除了判断daysOfMonth的第value位之外,还要判断daysOfWeek的第value位,同时为1才算匹配。
在调整操作中,对于月,时,分,秒四个域可以直接通过对应位数组查找下一个匹配的值,有三种情况:
1)下一个匹配值是当前值,说明当前值已经符合cron表达式,不调整。
2)下一个匹配值不是当前值也不是-1,则将当前域设置为下一个匹配值。
3)下一个匹配值是-1,则先对更高一级的域做加1操作,然后调整更高一级的域使其符合cron表达式(可能涉及调整所有其他更高的域)。然后从0开始找匹配值,并设置为当前域的值(只要更高的域调整过,当前域只需要设置为最小匹配值)。
对于日期的调整稍微复杂一些,可能需要调整多次:
1).如果daysOfMonth和daysOfWeek中当前日期的对应位都是1,则不需要调整,否则进入步骤2。
2)获取当前月份的实际最大天数(考虑月份和是否闰年),根据daysOfMonth从当前日期+1开始查找下一个匹配日期(当前日期已经在第1步证明不匹配了,所以从当前日期+1处查找)。如果下一个匹配日期正常,则将月设置为下一个匹配值即可。否则,即下一个匹配日期是-1或者超过该月的实际最大天数,则将月份加1并调整月到下一个符合规则的月并设置日期为1,然后回到步骤1(为什么不走其他域的类似逻辑,即从0找到最小匹配值然后将当前域设置为这个最小值?考虑这种情况:月份不限,日期限制在30号,如果当前时间是1月31号,那么月份调整后是2,我们会设置一个不存在的2月30号)。

新的doNext算法源码:

  1. //从calendar开始寻找下一个匹配cron表达式的时间
  2. private void doNextNew(Calendar calendar) {
  3. //calendar中比当前更高的域是否调整过
  4. boolean changed = false;
  5. List<Integer> fields = Arrays.asList(Calendar.MONTH, Calendar.DAY_OF_MONTH,
  6. Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND);
  7. //依次调整月,日,时,分,秒
  8. for (int field : fields) {
  9. if (changed) {
  10. calendar.set(field, field == Calendar.DAY_OF_MONTH ? 1 : 0);
  11. }
  12. if (!checkField(calendar, field)) {
  13. changed = true;
  14. findNext(calendar, field);
  15. }
  16. }
  17. }
  18. //检查某个域是否匹配cron表达式
  19. private boolean checkField(Calendar calendar, int field) {
  20. switch (field) {
  21. case Calendar.MONTH: {
  22. int month = calendar.get(Calendar.MONTH);
  23. return this.months.get(month);
  24. }
  25. case Calendar.DAY_OF_MONTH: {
  26. int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
  27. int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1;
  28. return this.daysOfMonth.get(dayOfMonth) && this.daysOfWeek.get(dayOfWeek);
  29. }
  30. case Calendar.HOUR_OF_DAY: {
  31. int hour = calendar.get(Calendar.HOUR_OF_DAY);
  32. return this.hours.get(hour);
  33. }
  34. case Calendar.MINUTE: {
  35. int minute = calendar.get(Calendar.MINUTE);
  36. return this.minutes.get(minute);
  37. }
  38. case Calendar.SECOND: {
  39. int second = calendar.get(Calendar.SECOND);
  40. return this.seconds.get(second);
  41. }
  42. default:
  43. return true;
  44. }
  45. }
  46. //调整某个域到下一个匹配值,使其符合cron表达式
  47. private void findNext(Calendar calendar, int field) {
  48. switch (field) {
  49. case Calendar.MONTH: {
  50. if (calendar.get(Calendar.YEAR) > 2099) {
  51. throw new IllegalArgumentException("year exceeds 2099!");
  52. }
  53. int month = calendar.get(Calendar.MONTH);
  54. int nextMonth = this.months.nextSetBit(month);
  55. if (nextMonth == -1) {
  56. calendar.add(Calendar.YEAR, 1);
  57. calendar.set(Calendar.MONTH, 0);
  58. nextMonth = this.months.nextSetBit(0);
  59. }
  60. if (nextMonth != month) {
  61. calendar.set(Calendar.MONTH, nextMonth);
  62. }
  63. break;
  64. }
  65. case Calendar.DAY_OF_MONTH: {
  66. while (!this.daysOfMonth.get(calendar.get(Calendar.DAY_OF_MONTH))
  67. || !this.daysOfWeek.get(calendar.get(Calendar.DAY_OF_WEEK) - 1)) {
  68. int max = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
  69. int nextDayOfMonth = this.daysOfMonth.nextSetBit(calendar.get(Calendar.DAY_OF_MONTH) + 1);
  70. if (nextDayOfMonth == -1 || nextDayOfMonth > max) {
  71. calendar.add(Calendar.MONTH, 1);
  72. findNext(calendar, Calendar.MONTH);
  73. calendar.set(Calendar.DAY_OF_MONTH, 1);
  74. } else {
  75. calendar.set(Calendar.DAY_OF_MONTH, nextDayOfMonth);
  76. }
  77. }
  78. break;
  79. }
  80. case Calendar.HOUR_OF_DAY: {
  81. int hour = calendar.get(Calendar.HOUR_OF_DAY);
  82. int nextHour = this.hours.nextSetBit(hour);
  83. if (nextHour == -1) {
  84. calendar.add(Calendar.DAY_OF_MONTH, 1);
  85. findNext(calendar, Calendar.DAY_OF_MONTH);
  86. calendar.set(Calendar.HOUR_OF_DAY, 0);
  87. nextHour = this.hours.nextSetBit(0);
  88. }
  89. if (nextHour != hour) {
  90. calendar.set(Calendar.HOUR_OF_DAY, nextHour);
  91. }
  92. break;
  93. }
  94. case Calendar.MINUTE: {
  95. int minute = calendar.get(Calendar.MINUTE);
  96. int nextMinute = this.minutes.nextSetBit(minute);
  97. if (nextMinute == -1) {
  98. calendar.add(Calendar.HOUR_OF_DAY, 1);
  99. findNext(calendar, Calendar.HOUR_OF_DAY);
  100. calendar.set(Calendar.MINUTE, 0);
  101. nextMinute = this.minutes.nextSetBit(0);
  102. }
  103. if (nextMinute != minute) {
  104. calendar.set(Calendar.MINUTE, nextMinute);
  105. }
  106. break;
  107. }
  108. case Calendar.SECOND: {
  109. int second = calendar.get(Calendar.SECOND);
  110. int nextSecond = this.seconds.nextSetBit(second);
  111. if (nextSecond == -1) {
  112. calendar.add(Calendar.MINUTE, 1);
  113. findNext(calendar, Calendar.MINUTE);
  114. calendar.set(Calendar.SECOND, 0);
  115. nextSecond = this.seconds.nextSetBit(0);
  116. }
  117. if (nextSecond != second) {
  118. calendar.set(Calendar.SECOND, nextSecond);
  119. }
  120. break;
  121. }
  122. }
  123. }

6.试验结果

试验手动生成了10个cron表达式以及对应的10个指定日期,分别使用新旧算法从指定时间查找符合cron表达式规则的下一个时间。试验结果如下所示:
第一,从执行时间上看,新的doNext算法比spring自带的doNext算法效率更高,而且多数情况下能提升一半以上的效率。
第二,从第8组试验结果来看,新算法客服了老算法秒数调整存在的问题(3.2节最后的注)。
第三,第4组试验的目的是找2016年5月23号之后,找第一个星期是周五的2月29号。原doNext算法耗时9000多us,没有计算出下一个匹配时间(实际抛出了异常,因为年份差不能大于4,会抛出运行时异常)。而新的doNext算法仅耗时600多us,并且找到了结果-2036-02-29 01:00:00。

测试程序源码:

  1. public class Test {
  2. private static void testCronAlg(Map<String, String> map) throws Exception {
  3. int count = 0;
  4. for (Map.Entry<String, String> entry : map.entrySet()) {
  5. System.out.println(++count);
  6. System.out.println("cron = "+entry.getKey());
  7. System.out.println("date = "+entry.getValue());
  8. CronSequenceGenerator cronSequenceGenerator = new CronSequenceGenerator(entry.getKey());
  9. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  10. Date date = sdf.parse(entry.getValue());
  11. long nanoTime1 = System.nanoTime();
  12. Date date1 = null;
  13. try {
  14. date1 = cronSequenceGenerator.next(date);
  15. } catch (Exception e) {
  16. }
  17. long nanoTime2 = System.nanoTime();
  18. String str1 = null;
  19. if (date1 != null) {
  20. str1 = sdf.format(date1);
  21. }
  22. System.out.println("old method : result date = " + str1
  23. + " , consume " + (nanoTime2 - nanoTime1)/1000 + "us");
  24. long nanoTime3 = System.nanoTime();
  25. Date date2 = null;
  26. try {
  27. date2 = cronSequenceGenerator.nextNew(date);
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. long nanoTime4 = System.nanoTime();
  32. String str2 = null;
  33. if (date2 != null) {
  34. str2 = sdf.format(date2);
  35. }
  36. System.out.println("new method : result date = " + str2
  37. + " , consume " + (nanoTime4 - nanoTime3)/1000 + "us");
  38. }
  39. }
  40. public static void main(String[] args) throws Exception {
  41. Map<String, String> map = new HashMap<>();
  42. map.put("0 0 8 * * *", "2011-03-25 13:22:43");
  43. map.put("0/2 1 * * * *", "2016-12-25 18:00:45");
  44. map.put("0 0/5 14,18 * * ?", "2016-01-29 04:01:12");
  45. map.put("0 15 10 ? * MON-FRI", "2022-08-31 23:59:59");
  46. map.put("0 26,29,33 * * * ?", "2013-09-12 03:04:05");
  47. map.put("10-20/4 10,44,30/2 10 ? 3 WED", "1999-10-18 12:00:00");
  48. map.put("0 0 0 1/2 MAR-AUG ?", "2008-09-11 19:19:19");
  49. map.put("0 10-50/3,57-59 * * * WED-FRI", "2003-02-09 06:17:19");
  50. map.put("0/2 0 1 29 2 FRI ", "2016-05-23 09:13:53");
  51. map.put("0/2 0 1 29 2 5 ", "2016-05-23 09:13:53");
  52. map.put("0 10,44 14 ? 3 WED", "2016-12-28 19:01:35");
  53. testCronAlg(map);
  54. }
  55. }

新旧算法测试结果对比:

  1. 1
  2. cron = 0 15 10 ? * MON-FRI
  3. date = 2022-08-31 23:59:59
  4. old method : result date = 2022-09-01 10:15:00 , consume 403us
  5. new method : result date = 2022-09-01 10:15:00 , consume 115us
  6. 2
  7. cron = 0 0/5 14,18 * * ?
  8. date = 2016-01-29 04:01:12
  9. old method : result date = 2016-01-29 14:00:00 , consume 106us
  10. new method : result date = 2016-01-29 14:00:00 , consume 74us
  11. 3
  12. cron = 10-20/4 10,44,30/2 10 ? 3 WED
  13. date = 1999-10-18 12:00:00
  14. old method : result date = 2000-03-01 10:10:10 , consume 382us
  15. new method : result date = 2000-03-01 10:10:10 , consume 132us
  16. 4
  17. cron = 0/2 0 1 29 2 FRI
  18. date = 2016-05-23 09:13:53
  19. old method : result date = null , consume 9418us
  20. new method : result date = 2036-02-29 01:00:00 , consume 658us
  21. 5
  22. cron = 0 10,44 14 ? 3 WED
  23. date = 2016-12-28 19:01:35
  24. old method : result date = 2017-03-01 14:10:00 , consume 302us
  25. new method : result date = 2017-03-01 14:10:00 , consume 69us
  26. 6
  27. cron = 0 0 0 1/2 MAR-AUG ?
  28. date = 2008-09-11 19:19:19
  29. old method : result date = 2009-03-01 00:00:00 , consume 99us
  30. new method : result date = 2009-03-01 00:00:00 , consume 45us
  31. 7
  32. cron = 0 0 8 * * *
  33. date = 2011-03-25 13:22:43
  34. old method : result date = 2011-03-26 08:00:00 , consume 116us
  35. new method : result date = 2011-03-26 08:00:00 , consume 58us
  36. 8
  37. cron = 0/2 1 * * * *
  38. date = 2016-12-25 18:00:45
  39. old method : result date = 2016-12-25 18:01:46 , consume 35us
  40. new method : result date = 2016-12-25 18:01:00 , consume 28us
  41. 9
  42. cron = 0/2 0 1 29 2 5
  43. date = 2016-05-23 09:13:53
  44. old method : result date = null , consume 3270us
  45. new method : result date = 2036-02-29 01:00:00 , consume 346us
  46. 10
  47. cron = 0 26,29,33 * * * ?
  48. date = 2013-09-12 03:04:05
  49. old method : result date = 2013-09-12 03:26:00 , consume 53us
  50. new method : result date = 2013-09-12 03:26:00 , consume 42us
  51. 11
  52. cron = 0 10-50/3,57-59 * * * WED-FRI
  53. date = 2003-02-09 06:17:19
  54. old method : result date = 2003-02-12 00:10:00 , consume 63us
  55. new method : result date = 2003-02-12 00:10:00 , consume 44us
 

引用原文:https://blog.csdn.net/ukulelepku/article/details/54310035

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,文章可以转载,无需版权。希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!

spring cron表达式及解析过程的更多相关文章

  1. spring cron表达式源码分析

    spring cron表达式源码分析 在springboot中,我们一般是通过如下的做法添加一个定时任务 上面的new CronTrigger("0 * * * * *")中的参数 ...

  2. Java Spring cron表达式使用详解

    Java Spring cron表达式使用详解   By:授客 QQ:1033553122 语法格式 Seconds Minutes Hours DayofMonth Month DayofWeek ...

  3. spring cron表达式(定时器)

    转: spring cron表达式(定时器) 写定时器时用到,记录一下: Cron表达式是一个字符串,字符串以5或6个空格隔开,分开工6或7个域,每一个域代表一个含义,Cron有如下两种语法 格式:  ...

  4. Spring cron 表达式

    前言: 最近做的项目有用到定时器,每周只在特定时间运行一次,考虑到Spring Task的简单易用性,就果断选择了,我是配置在配置文件里面,没有用注解@Scheduled,推荐配置,注解虽方便,但更改 ...

  5. Spring cron表达式详解

    一个cron表达式有6个必选的元素和一个可选的元素,各个元素之间是以空格分隔的,从左至右,这些元素的含义如下表所示: 代表含义 是否必须 允许的取值范围 允许的特殊符号 秒 是 0-59 , - * ...

  6. spring cron表达式

    其他参考资料 http://www.blogjava.net/hao446tian/archive/2012/02/13/369872.html http://blog.sina.com.cn/s/b ...

  7. 基于Spring的最简单的定时任务实现与配置(三)--番外篇 cron表达式的相关内容

    本来这篇文章是会跟本系列的前两篇文章一起发布的.但是,昨天在找资料总结的时候遇到了一点意外,就延后了一些. 本篇的内容主要参考了 这篇博文:http://www.cnblogs.com/junrong ...

  8. spring cron 定时任务

    文章首发于个人博客:https://yeyouluo.github.io 0 预备知识:cron表达式 见 <5 参考>一节. 1 环境 eclipse mars2 + Maven3.3. ...

  9. 摆脱Spring 定时任务的@Scheduled cron表达式的困扰

    一.背景 最近因为需要,需要适用Spring的task定时任务进行跑定时任务,以前也接触过,但是因为懒没有好好地理解@Scheduled的cron表达式,这次便对它做了一个全方位的了解和任务,记录下来 ...

随机推荐

  1. 5501环路运输【(环结构)线性DP】【队列优化】

    5501 环路运输 0x50「动态规划」例题 描述 在一条环形公路旁均匀地分布着N座仓库,编号为1~N,编号为 i 的仓库与编号为 j 的仓库之间的距离定义为 dist(i,j)=min⁡(|i-j| ...

  2. Vim 字符集问题

     使用CentOS中的Vim 文本编辑器出现中文乱码的问题. 凡是字符乱码的问题,都是字符集不匹配的问题引起的.这里的字符集不匹配只的是文件的编码和解码方式不匹配,同时可能涉及到不只一次的解码过程. ...

  3. /dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), and epoll(4)

    /dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), and epoll(4) libevent ...

  4. lsof,fuser,xargs,print0,cut,paste,cat,tac,rev,exec,{},双引号,单引号,‘(字符串中执行命令)

    cut用来从文本文件或标准输出中抽取数据列或者域,然后再用paste可以将这些数据粘贴起来形成相关文件. 粘贴两个不同来源的数据时,首先需将其分类,并确保两个文件行数相同.paste将按行将不同文件行 ...

  5. Python进阶知识

    装饰器 迭代器 生成器 mixins 元编程 描述符 量化领域常用 列表推导式 字典推导式 高阶函数 lambda函数 三目表达式

  6. java基础06 switch

    public class SwitchDemo01 { /** * 韩嫣参加计算机编程大赛 如果获得第一名,将参加麻省理工大学组织的1个月夏令营 如果获得第二名,将奖励惠普笔记本电脑一部 如果获得第三 ...

  7. VMware 虚拟机 Ubuntu 不能全屏问题

    在刚安装完ubuntu后,屏幕不能全屏显示,此时: 1.安装VMware Tools 步骤: 1.1     进入ubuntu系统后,点击虚拟机上的[虚拟机]->[安装 vmware tools ...

  8. 2D游戏中的碰撞检测:圆形与矩形碰撞检测(Javascrip版)

    一,原理介绍 这回有点复杂,不过看懂了还是很好理解的.当然,我不敢保证这种算法在任何情况下都会起效果,如果有同学测试时,发现出现错误,请及时联系我. 我们首先来建立一个以圆心为原点的坐标系: 然后要检 ...

  9. django生产环境部署

    测试环境:linux centos7下 1.安装uwsgi python3下安装: pip3 install uwsgi python2下安装: pip install uwsgi 如果是系统自带的p ...

  10. 1130 - Host '' is not allowerd to connect to this MySQL server,

    是因为缺少访问权限,在MySQL ->User表里 执行 INSERT INTO `user` VALUES ('%', 'root', '*81F5E21E35407D884A6CD4A731 ...