在springboot中,我們一般是通過如下的做法新增一個定時任務
上面的new CronTrigger("0 * * * * *")
中的引數0 * * * * *
就是cron
表示式了。
這裡主要是對
cron
表示式的原始碼進行分析,其他內容不再展開了。
這能看到會建立一個CronTrigger
物件,這個物件它主要就是用來包裝解析後的cron
表示式,獲取任務下次執行的時間。
在CronTrigger
構造方法中會呼叫到this.expression = CronExpression.parse(expression);
將我們傳入的cron
字串解析成為CronExpression
物件。
CronExpression
主要有一個next
方法,它會根據當前cron
表示式解析出來的物件,以及傳入的時間,返回一個時間值,也就是下次任務執行的時間。
這裡的入參需要實現Temporal
介面。這是在JDK8引入的一套全新的時間、日期。
能引入新的,至少說明之前的
Date
等等之類的時間處理是不能滿足各方面需要的。
下面看看它的主要實現
這裡我們一般常用的可能就是Instant
,LocalDateTime
,ZonedDateTime
了。
從上面就可以看到cron
表示式的處理,主要是分為兩步:1、將cron
表示式字串解析為CronExpression
物件;2、根據傳入的時間計算下次任務的執行時間。
在分析原始碼之前,我們簡單看幾個java
中的類
ValueRange
主要用來表示時間、日期欄位的有效範圍。當然它也可以不用來表示時間、日期。下面我們簡單看下它的使用。
它主要有4個欄位,4個屬性值從上到下是不小於的關係。
private final long minSmallest; //最小的最小值
private final long minLargest; //較大的最小值
private final long maxSmallest; //較小的最大值
private final long maxLargest; //最大的最大值
//定義1個(1-10)的範圍指定minSmallest和minLargest都是1,maxSmallest和maxLargest都是10
ValueRange valueRange=ValueRange.of(1,10);
//判斷5是不是在上面定義的minSmallest和maxLargest(1-10)的範圍內,在的話返回true
boolean validValue = valueRange.isValidValue(5);
System.out.println(validValue);
ChronoField
是一個列舉類,就是用來表示時間、日期的欄位。
下面我們簡單看它的幾個範例
//用納秒來表示表,1秒==1000000000納秒,所以它的範圍是0-999999999
NANO_OF_SECOND("NanoOfSecond", NANOS, SECONDS, ValueRange.of(0, 999_999_999)),
//用納秒來表示一天,1天==86400秒,再轉成納秒就是86400L*1000000000
NANO_OF_DAY("NanoOfDay", NANOS, DAYS, ValueRange.of(0, 86400L * 1000_000_000L - 1)),
......其他基本類似,就不繼續說了
ChronoUnit
也是一個列舉類,表示一個時間單元。有一個addTo
方法表示給時間加上一個對應的時間單元。
//下面的程式碼就是給當前時間加上1天
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime zonedDateTime = ChronoUnit.DAYS.addTo(now, 1);
我們先看第一步:
cron
表示式字串解析為CronExpression
物件我們傳入的表示式用空格分成6個部分,每個部分代表的含義如下:
在CronField
類中有一個內部列舉類Type
,它就是用來表示cron
表示式中的欄位(
在cron表示式中沒有納秒欄位,其他都跟
cron
表示式是一一對應的
先看下它的構造方法
從上面也可以看到這個列舉類有兩個欄位,第一個是表示當前時間、日期的欄位,後面是一個用來表示小於它的時間、日期欄位的陣列。
程式碼的如下圖
主要程式碼就是上面框出來的:
將我們傳入的cron
字串分割成陣列。
分別解析每個部分,建立CronExpression
物件。
解析每個部分都呼叫的是CronField.parsexxx
這樣的靜態方法。所有的解析基本是一致的,分別建立BitsCronField
物件。所以我們就只看CronField.parseSeconds
方法。
CronField.parseSeconds
原始碼分析
這個方法會調到BitsCronField.parseSeconds(value);
方法,繼續呼叫到BitsCronField
的靜態方法 parseField(value, Type.SECOND)
,下面我們主要看看這個方法的程式碼。
上面就是這個方法的全部程式碼了,從上面我標註的地方就能看到一個cron
欄位可以包含的其他符號,分別是,
、/
、-
這3種額外的符號。
1、在標號1的地方首先把欄位用,
號拆分成陣列,後面在for
迴圈中對每個部分進行處理。
2、在for
迴圈中,首先判斷是否包含/
,如果不包含,就呼叫parseRange
返回一個ValueRange
。
2.1、 下面我們先看下不包含/
的if
分支
parseRange
方法比較簡單,這裡簡單說下:
如果當前的rangeStr
==*
,那就返回type
對應的預設ValueRange
;
這裡的
type
就是前面看到的CronField
的內部列舉類Type
如果rangeStr
不包含-
,那就表示一個固定的值,用ValueRange.of(result, result)
返回;
如果rangeStr
包含-
,那就表示一個範圍-
前面的表示最小值、-
之後的表示最大值。組裝成一個ValueRange
返回。
parseRange
返回之後,再呼叫result.setBits(range)
方法。
我們先看看BitsCronField
這個類,它有一個屬性private static final long MASK = 0xFFFFFFFFFFFFFFFFL;
表示掩碼,還有一個屬性private long bits;
我們最終計算出來的執行時間都會體現在這個欄位上。
由於對於cron
表示式中的6個部分,最大的也就是比如表示分鐘、秒鐘的一共60個。由於long
是64位元,所以這是按照bit
來設定對應的可用的值。
舉個例子比如cron
表示式計算出來秒的部分是第50秒執行,那就會將對應的bits
欄位的第50位設定為1
。
下面我們看看setBits
方法
如果傳入的ValueRange
只表示一個值,那就把對應的bit
位置1;
否則就將將最小值與最大值之間的bit
位置1。
這裡使用|
是由於外面我們可能會是,
分割的多個欄位,會出現多次賦值,要確保本次賦值不會將之前賦值1的bit
位清空。
這裡需要注意的是右移是一個負數,這是由於
MASK
是long
型別,也就是64位元,所以右移負數其實也就是移動(64-(range.getMaximum() + 1))位。後面的+1主要是由於我們最小值是從0開始的。舉個例子,如果範圍的最小值是0,最大值是1。如果沒有+1,最大值掩碼就會右移64-1=63,最終只有第0位是1,這明顯就是錯誤的。
2.2、下面我們看下包含/
分支的部分
首先也還是拆分/
前後,前面的作為ValueRange
,後面的作為delta
。
這裡需要注意的是如果
/
前面不包含-
,那/
前面的只是作為最小值,最大值還是用type
對應的最大值。
標註2的地方就是設定對應的bit
位了,這裡主要是按照delta
的增量在最小值和最大值之間分別設定對應bit
位。
上面就是cron
表示式中一個欄位的解析了,建立一個BitsCronField
物件,設定對應的bits
屬性對應的bit
位為1,下面我們簡單看看各種設定。
下面我們看看
cron
表示式秒欄位的各種情況
*
表示將bits
中bit
位從0-59
位都設定成1.
2,6,8
表示將bits
中bit
位第2、6、8位元都設定成1.
2/20,8
表示將bits
中bit
位第2、22、42、8位元都設定成1.
在解析完cron
表示式的每個部分之後,就會建立一個CronExpression
物件,這類會新增一個CronField.zeroNanos()
欄位,用來表示納秒欄位,同時將bits
設定為0,表示我們的定時任務希望在納秒為0的時刻執行。
上面已經建立好了CronExpression
物件,下面我們看看如果計算下次執行時間。
這裡就是根據傳入的時間去計算下次任務的執行時間了 。
首先給入參時間加上1納秒,這個主要是避免在1個時間點任務執行多次。
舉個例子:
比如我們的定時任務很快,在0納秒後就返回了 ,由於我們的定時任務設定了只在0納秒執行,那這時候計算出來的下次執行任務時間和上一次任務是同一時間,就又會去執行一遍定時任務。
這裡也能看到我們的定時任務最快也是每秒執行一次。加1納秒就是為了確保當前任務和下次任務不會在同一秒執行。
下次任務執行時間是在本次任務執行完就就算出來的
在這裡能看到,最多會嘗試MAX_ATTEMPTS
次,檢視計算出來的時間不再變化,那這個時間就是我們計算出來的下次任務執行的時間。如果嘗試MAX_ATTEMPTS
次每次的時間都和上次的不一樣,那就返回null
在nextOrSameInternal
方法中會分別對每個field
進行處理。這些field
有額外新增的納秒(設定了bits=0,表示在0納秒執行),其他6個就分別是cron
表示式對應的部分,分別是秒、分、小時、日、月、星期
。
下面我們看下對單個欄位的處理。
在標號1的地方首先獲取傳入的時間對應當前時間單位的值。
比如現在傳入的是2022-10-01 12:00:15,對應秒的單位的值就是15.
在標號2的位置會根據當前時間單位的值去計算下次值
具體的做法就是用將全F左移
current
位,與對應欄位的bits
做與運算。然後返回最低位為1bit
位的索引。就表示下次任務可以執行的對應的時間欄位的值。當前有可能與運算後結果是0。那就沒有1的
bit
位。這時就會返回-1。在下面標註3的地方就會對這種場景進行處理。
在標註3的地方主要是對時間已經過去的情況進行處理。比如我們cron
表示式計劃在秒數為00的時刻進行執行,由於現在已經是15
秒了。那隻能在它的上一級時間單位(分鐘)+1,同時將本時間單位置為0。
在這裡已經對我們的時間進行了+1處理,所以時間值和傳入的值已經有變化了 ,這時在外層就會進入下次迴圈。
在標註4的地方會重新計算對應時間單位最早執行的最小值。
能走到標註5的地方說明對應下次任務執行時間對應時間單位的值已經有變化了,在這裡主要也還是調整時間,將時間調整成符合下次執行任務的時間。主要的程式碼是elapseUntil
方法。
下面是elapseUntil
方法的程式碼。
從圖上看,主要分3種情況:
下次執行時間在合法範圍內,那就直接講欄位的值進行設定。
這裡需要注意的是這個範圍不一定是固定的。如日,在1月範圍有效範圍就是在1-31。2月就是在1-28或1-29。
如果時間不在有效範圍內,那就在當前的時間單位上加上一個差值。
這個加操作會使上一級時間單位變化。如當前時間單位是日,執行加操作,可能會使月單位的值也有所變化。
如果下次執行時間小於當前時間單位的值,那就只能進行加。
比如當前是1月15號,下次任務是10號執行。那就只有給日時間單位加(下次任務時間
10
+最大值31
-當前值15
+1-最小值1
)=26將時間變成下個月對應的時間單位進行重新計算(將時間變成2月10號)。
在標註6的地方,這裡是由於已經將當前的時間單位進行至少+1的調整。那這時就需要將它對應的所有下一級時間單位統一調整成最小值,以便下次重新計算。
上面就是整個下次任務執行時間的計算了 。
總結下,就是設定下次任務執行的納秒單位為0,分別在秒,分,小時,日,月,星期單位上進行計算。至少時間不再調整。
上面只是分析了cron
表示式的解析處理,關於cron
表示式的各種寫法並沒有列出。不過相信大家根據原始碼反推cron
表示式的各種寫法,應該是個簡單事情了。