spring cron表達式及解析過程


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.    
  27. private int[] getRange(String field, int min, int max) {  
  28.    int[] result = new int[2];  
  29.    if (field.contains("*")) {  
  30.       result[0] = min;  
  31.       result[1] = max - 1;  
  32.       return result;  
  33.    }  
  34.    if (!field.contains("-")) {  
  35.       result[0] = result[1] = Integer.valueOf(field);  
  36.    }  
  37.    else {  
  38.       String[] split = StringUtils.delimitedListToStringArray(field, "-");  
  39.       if (split.length > 2) {  
  40.          throw new IllegalArgumentException("Range has more than two fields: '" +  
  41.                field + "' in expression \"" + this.expression + "\"");  
  42.       }  
  43.       result[0] = Integer.valueOf(split[0]);  
  44.       result[1] = Integer.valueOf(split[1]);  
  45.    }  
  46.    if (result[0] >= max || result[1] >= max) {  
  47.       throw new IllegalArgumentException("Range exceeds maximum (" + max + "): '" +  
  48.             field + "' in expression \"" + this.expression + "\"");  
  49.    }  
  50.    if (result[0] < min || result[1] < min) {  
  51.       throw new IllegalArgumentException("Range less than minimum (" + min + "): '" +  
  52.             field + "' in expression \"" + this.expression + "\"");  
  53.    }  
  54.    return result;  
  55. }  

對於日期,先將該域的子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.    
  9. private void setDays(BitSet bits, String field, int max) {  
  10.    if (field.contains("?")) {  
  11.       field = "*";  
  12.    }  
  13.    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.     
  8. private void setDays(BitSet bits, String field, int max) {  
  9.    if (field.contains("?")) {  
  10.       field = "*";  
  11.    }  
  12.    setNumberHits(bits, field, 0, max);  
  13. }  

 

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

 

(注:源碼中對秒的處理與圖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.   
  12.    1 Round up to the next whole second 
  13.   
  14.    2 If seconds match move on, otherwise find the next match: 
  15.    2.1 If next match is in the next minute then roll forwards 
  16.   
  17.    3 If minute matches move on, otherwise find the next match 
  18.    3.1 If next match is in the next hour then roll forwards 
  19.    3.2 Reset the seconds and go to 2 
  20.   
  21.    4 If hour matches move on, otherwise find the next match 
  22.    4.1 If next match is in the next day then roll forwards, 
  23.    4.2 Reset the minutes and seconds and go to 2 
  24.   
  25.    ... 
  26.    */  
  27.    
  28.    Calendar calendar = new GregorianCalendar();  
  29.    calendar.setTimeZone(this.timeZone);  
  30.    calendar.setTime(date);  
  31.    
  32.    // First, just reset the milliseconds and try to calculate from there...  
  33.    calendar.set(Calendar.MILLISECOND, 0);  
  34.    long originalTimestamp = calendar.getTimeInMillis();  
  35.    doNext(calendar, calendar.get(Calendar.YEAR));  
  36.    
  37.    if (calendar.getTimeInMillis() == originalTimestamp) {  
  38.       // We arrived at the original timestamp - round up to the next whole second and try again...  
  39.       calendar.add(Calendar.SECOND, 1);  
  40.       doNext(calendar, calendar.get(Calendar.YEAR));  
  41.    }  
  42.    
  43.    return calendar.getTime();  
  44. }  

 

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

 

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.    
  12.             long nanoTime1 = System.nanoTime();  
  13.             Date date1 = null;  
  14.             try {  
  15.                 date1 = cronSequenceGenerator.next(date);  
  16.             } catch (Exception e) {  
  17.             }  
  18.             long nanoTime2 = System.nanoTime();  
  19.             String str1 = null;  
  20.             if (date1 != null) {  
  21.                 str1 = sdf.format(date1);  
  22.             }  
  23.             System.out.println("old method : result date = " + str1  
  24.                     + " , consume " + (nanoTime2 - nanoTime1)/1000 + "us");  
  25.    
  26.    
  27.             long nanoTime3 = System.nanoTime();  
  28.             Date date2 = null;  
  29.             try {  
  30.                 date2 = cronSequenceGenerator.nextNew(date);  
  31.             } catch (Exception e) {  
  32.                 e.printStackTrace();  
  33.             }  
  34.             long nanoTime4 = System.nanoTime();  
  35.             String str2 = null;  
  36.             if (date2 != null) {  
  37.                 str2 = sdf.format(date2);  
  38.             }  
  39.             System.out.println("new method : result date = " + str2  
  40.                     + " , consume " + (nanoTime4 - nanoTime3)/1000 + "us");  
  41.         }  
  42.     }  
  43.    
  44.     public static void main(String[] args) throws Exception {  
  45.         Map<String, String> map = new HashMap<>();  
  46.         map.put("0 0 8 * * *", "2011-03-25 13:22:43");  
  47.         map.put("0/2 1 * * * *", "2016-12-25 18:00:45");  
  48.         map.put("0 0/5 14,18 * * ?", "2016-01-29 04:01:12");  
  49.         map.put("0 15 10 ? * MON-FRI", "2022-08-31 23:59:59");  
  50.         map.put("0 26,29,33 * * * ?", "2013-09-12 03:04:05");  
  51.         map.put("10-20/4 10,44,30/2 10 ? 3 WED", "1999-10-18 12:00:00");  
  52.         map.put("0 0 0 1/2 MAR-AUG ?", "2008-09-11 19:19:19");  
  53.         map.put("0 10-50/3,57-59 * * * WED-FRI", "2003-02-09 06:17:19");  
  54.         map.put("0/2 0 1 29 2 FRI ", "2016-05-23 09:13:53");  
  55.         map.put("0/2 0 1 29 2 5 ", "2016-05-23 09:13:53");  
  56.         map.put("0 10,44 14 ? 3 WED", "2016-12-28 19:01:35");  
  57.         testCronAlg(map);  
  58.     }  
  59. }  
新舊算法測試結果對比:
 
  1. 1  
  2. cron = 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/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/10,44,30/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/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 = 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 = 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 = 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/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/29 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 = 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 = 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

 

寫博客是為了記住自己容易忘記的東西,另外也是對自己工作的總結,文章可以轉載,無需版權。希望盡自己的努力,做到更好,大家一起努力進步!

如果有什么問題,歡迎大家一起探討,代碼如有問題,歡迎各位大神指正!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM