Java日期時間API系列29-----Jdk8中java.time包中的新的日期時間API類,Java定時任務job中cron表達式計算應用。

  Java開發過程中經常會用到定時任務job的場景,比如定時處理數據報表等問題,開源作業調度框架也非常多,常用的開源作業調度框架有:Spring Task、Quartz和xxl-job等。各個框架的具體使用不再這裏討論,這裏主要討論一下其中cron表達式的計算應用,xk-time中的應用。

1.Spring Task中cron表達式的計算應用

1.1 源碼

Spring Task包含在spring-context的jar包中,org.springframework.scheduling.support.CronSequenceGenerator類。

(1)基本屬性,秒 分 小時 月份中的日期 月份 星期中的星期 總共6位

public class CronSequenceGenerator {

    private final String expression;//cron表達式

    
    private final TimeZone timeZone;//時區

    private final BitSet months = new BitSet(12);//月

    private final BitSet daysOfMonth = new BitSet(31);//日

    private final BitSet daysOfWeek = new BitSet(7);//星期

    private final BitSet hours = new BitSet(24);//小時

    private final BitSet minutes = new BitSet(60);//分鐘

    private final BitSet seconds = new BitSet(60);//秒

 

(2)表達式中支持的特殊字符:, - * ? / 分別是枚舉,範圍,任意值,任意值,間隔,不同字段使用有所區別,後面詳細說明。

 詳細見 org.springframework.scheduling.support.CronSequenceGenerator.parse(String)

2.Quartz和xxl-job中cron表達式的計算應用

Quartz中的是org.quartz.CronExpression 在所有的cron表達式中使用比較廣泛,xxl-job中借用了Quartz中的CronExpression com.xxl.job.admin.core.cron.CronExpression。

2.1 源碼

(1)基本屬性,秒 分鐘 小時 天 月份 星期 年 總共7位,比SpringTask多一位年

 

    private final String cronExpression;//cron表達式
    private TimeZone timeZone = null;//時區
    protected transient TreeSet<Integer> seconds;//
    protected transient TreeSet<Integer> minutes;//分鐘
    protected transient TreeSet<Integer> hours;//小時
    protected transient TreeSet<Integer> daysOfMonth;//
    protected transient TreeSet<Integer> months;//
    protected transient TreeSet<Integer> daysOfWeek;//星期
    protected transient TreeSet<Integer> years;//

 

(2)表達式中支持的特殊字符:, - * ? /  L W #分別是枚舉,範圍,任意值,任意值,間隔,最後,有效工作日(週一到週五),每個月第幾個星期幾,不同字段使用有所區別,後面詳細說明。比SpringTask多了3個 L W #。

 詳細見 org.quartz.CronExpression.buildExpression(String)

 

3.Cron表達式規則詳解

從1和2中源碼看出,Spring Task中cron規則爲Quartz的子集。Quartz的使用更爲廣泛。下面以Quartz爲例介紹:

Cron表達式是一個字符串,字符串以5或6個空格隔開,分爲6或7個域,每一個域代表一個含義,Cron有如下語法格式:

seconds minutes hours daysOfMonth months daysOfWeek years(可選)  其中years爲可選。

3.1、結構

  corn從左到右(用空格隔開):秒 分 小時 日期 月份 星期 年份

3.2、各字段的含義

 
字段 允許值 允許的特殊字符
秒(Seconds) 0~59的整數 , - * /    四個字符
分(Minutes 0~59的整數 , - * /    四個字符
小時(Hours 0~23的整數 , - * /    四個字符
日期(DayofMonth 1~31的整數(但是你需要考慮你月的天數) ,- * ? / L W C     八個字符
月份(Months 1~12的整數或者 JAN-DEC , - * /    四個字符
星期(DayofWeek 1~7的整數或者 SUN-SAT (1=SUN) , - * ? / L C #     八個字符
年(可選,留空)(Years 1970~2099 , - * /    四個字符

 

 

 

 

 

 

 

  注意事項:

  每一個域都使用數字,但還可以出現如下特殊字符,它們的含義是:

  (1)*:表示匹配該域的任意值。假如在Minutes域使用*, 即表示每分鐘都會觸發事件。

  (2)?:只能用在DayofMonth和DayofWeek兩個域。它也匹配域的任意值,但實際不會。因爲DayofMonth和DayofWeek會相互影響。例如想在每月的20日觸發調度,不管20日到底是星期幾,則只能使用如下寫法: 13 13 15 20 * ?, 其中最後一位只能用?,而不能使用*,如果使用*表示不管星期幾都會觸發,實際上並不是這樣。

  (3)-:表示範圍。例如在Minutes域使用5-20,表示從5分到20分鐘每分鐘觸發一次 

  (4)/:表示起始時間開始觸發,然後每隔固定時間觸發一次。例如在Minutes域使用5/20,則意味着5分鐘觸發一次,而25,45等分別觸發一次. 

  (5),:表示列出枚舉值。例如:在Minutes域使用5,20,則意味着在5和20分每分鐘觸發一次。 

  (6)L:表示最後,只能出現在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最後的一個星期四觸發。 

  (7)W:表示有效工作日(週一到週五),只能出現在DayofMonth域,系統將在離指定日期的最近的有效工作日觸發事件。例如:在 DayofMonth使用5W,如果5日是星期六,則將在最近的工作日:星期五,即4日觸發。如果5日是星期天,則在6日(週一)觸發;如果5日在星期一到星期五中的一天,則就在5日觸發。另外一點,W的最近尋找不會跨過月份 。

  (8)LW:這兩個字符可以連用,表示在某個月最後一個工作日,即最後一個星期五。 

  (9)#:用於確定每個月第幾個星期幾,只能出現在DayofMonth域。例如在4#2,表示某月的第二個星期三。

3.3、常用表達式例子

  (1)0 0 2 1 * ? *   表示在每月的1日的凌晨2點調整任務

  (2)0 15 10 ? * MON-FRI   表示週一到週五每天上午10:15執行作業

  (3)0 15 10 ? * 6L 2002-2006   表示2002-2006年的每個月的最後一個星期五上午10:15執行作

  (4)0 0 10,14,16 * * ?   每天上午10點,下午2點,4點 

  (5)0 0/30 9-17 * * ?   朝九晚五工作時間內每半小時 

  (6)0 0 12 ? * WED    表示每個星期三中午12點 

  (7)0 0 12 * * ?   每天中午12點觸發 

  (8)0 15 10 ? * *    每天上午10:15觸發 

  (9)0 15 10 * * ?     每天上午10:15觸發 

  (10)0 15 10 * * ? *    每天上午10:15觸發 

  (11)0 15 10 * * ? 2005    2005年的每天上午10:15觸發 

  (12)0 * 14 * * ?     在每天下午2點到下午2:59期間的每1分鐘觸發 

  (13)0 0/5 14 * * ?    在每天下午2點到下午2:55期間的每5分鐘觸發 

  (14)0 0/5 14,18 * * ?     在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發 

  (15)0 0-5 14 * * ?    在每天下午2點到下午2:05期間的每1分鐘觸發 

  (16)0 10,44 14 ? 3 WED    每年三月的星期三的下午2:10和2:44觸發 

  (17)0 15 10 ? * MON-FRI    週一至週五的上午10:15觸發 

  (18)0 15 10 15 * ?    每月15日上午10:15觸發 

  (19)0 15 10 L * ?    每月最後一日的上午10:15觸發 

  (20)0 15 10 ? * 6L    每月的最後一個星期五上午10:15觸發 

  (21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最後一個星期五上午10:15觸發 

  (22)0 15 10 ? * 6#3   每月的第三個星期五上午10:15觸發

 

  

  注:

  (1)有些子表達式能包含一些範圍或列表

  例如:子表達式(天(星期))可以爲 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”

“*”字符代表所有可能的值

  因此,“*”在子表達式(月)裏表示每個月的含義,“*”在子表達式(天(星期))表示星期的每一天


  “/”字符用來指定數值的增量 
  例如:在子表達式(分鐘)裏的“0/15”表示從第0分鐘開始,每15分鐘 
在子表達式(分鐘)裏的“3/20”表示從第3分鐘開始,每20分鐘(它和“3,23,43”)的含義一樣


  “?”字符僅被用於天(月)和天(星期)兩個子表達式,表示不指定值 
  當2個子表達式其中之一被指定了值以後,爲了避免衝突,需要將另一個子表達式的值設爲“?”

  “L” 字符僅被用於天(月)和天(星期)兩個子表達式,它是單詞“last”的縮寫 
  但是它在兩個子表達式裏的含義是不同的。 
  在天(月)子表達式中,“L”表示一個月的最後一天 
  在天(星期)自表達式中,“L”表示一個星期的最後一天,也就是SAT

  如果在“L”前有具體的內容,它就具有其他的含義了

  例如:“6L”表示這個月的倒數第6天,“FRIL”表示這個月的最一個星期五 
  注意:在使用“L”參數時,不要指定列表或範圍,因爲這會導致問題

 

4.xk-time中的cron表達式工具 CronExpressionUtil

  本來想在xk-time中自己實現cron表達式解析和計算,但cron表達式是一個標準,有成熟的實現方案,並且後續維護也是問題,xxl-job其中使用了Quartz,Quartz會不斷維護相關源碼,這裏也使用Quartz的CronExpression,並會定期根據Quartz更新代碼。

包含
(1)驗證和格式化Cron表達式方法,isValidExpression和formatExpression。
(2)生成下一個或多個執行時間方法,getNextTime和getNextTimeList。
(3)生成下一個或多個執行時間的日期格式化(yyyy-MM-dd HH:mm:ss)方法,getNextTimeStr和getNextTimeStrList。
(4)對比Cron表達式下一個執行時間是否與指定date相等方法,isSatisfiedBy。

注意: 底層使用 quartz的CronExpression處理。

4.1 源碼

package com.xkzhangsan.time.cron;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.xkzhangsan.time.formatter.DateTimeFormatterUtil;
import com.xkzhangsan.time.utils.CollectionUtil;

/**
 * Cron表達式工具
 * 包含
 * 1.驗證和格式化Cron表達式方法,isValidExpression和formatExpression
 * 2.生成下一個或多個執行時間方法,getNextTime和getNextTimeList
 * 3.生成下一個或多個執行時間的日期格式化(yyyy-MM-dd HH:mm:ss)方法,getNextTimeStr和getNextTimeStrList
 * 4.對比Cron表達式下一個執行時間是否與指定date相等方法,isSatisfiedBy
 * 
* @ClassName: CronExpressionUtil 
* @Description: CronExpressionUtil
* @author xkzhangsan
* @date 2020年4月16日
*
*使用 quartz CronExpression
 */
public class CronExpressionUtil {

    private CronExpressionUtil(){
    }
    
    /**
     * 驗證Cron表達式
     * @param cronExpression
     * @return
     */
    public static boolean isValidExpression(String cronExpression){
        return CronExpression.isValidExpression(cronExpression);
    }
    
    /**
     * 格式化Cron表達式
     * @param cronExpression
     * @return
     */
    public static String formatExpression(String cronExpression){
        try {
            return new CronExpression(cronExpression).toString();
        } catch (ParseException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
    
    /**
     * 生成下一個執行時間
     * @param cronExpression
     * @param date
     * @return
     */
    public static Date getNextTime(String cronExpression, Date date){
        try {
            if(date != null){
                return new CronExpression(cronExpression).getNextValidTimeAfter(date);
            }else{
                return new CronExpression(cronExpression).getNextValidTimeAfter(new Date());
            }
        } catch (ParseException e) {
            throw new RuntimeException(e.getMessage());
        }
    }
    
    /**
     * 生成下一個執行時間
     * @param cronExpression
     * @return
     */
    public static Date getNextTime(String cronExpression){
        return getNextTime(cronExpression, null);
    }
    
    /**
     * 生成下一個執行時間的日期格式化
     * @param cronExpression
     * @param date
     * @return 返回格式化時間 yyyy-MM-dd HH:mm:ss
     */
    public static String getNextTimeStr(String cronExpression, Date date){
        return DateTimeFormatterUtil.formatToDateTimeStr(getNextTime(cronExpression, date));
    }
    
    /**
     * 生成下一個執行時間的日期格式化
     * @param cronExpression
     * @return 返回格式化時間 yyyy-MM-dd HH:mm:ss
     */
    public static String getNextTimeStr(String cronExpression){
        return getNextTimeStr(cronExpression, null);
    }
    
    /**
     * 生成多個執行時間
     * @param cronExpression
     * @param date
     * @param num
     * @return
     */
    public static List<Date> getNextTimeList(String cronExpression, Date date, int num){
        List<Date> dateList = new ArrayList<>();
        if(num < 1){
            throw new RuntimeException("num must be greater than 0");
        }
        Date startDate = date != null ? date : new Date();
        for(int i=0; i<num; i++){
            startDate = getNextTime(cronExpression, startDate);
            if(startDate != null){
                dateList.add(startDate);
            }
        }
        return dateList;
    }
    
    /**
     * 生成多個執行時間
     * @param cronExpression
     * @param num
     * @return
     */
    public static List<Date> getNextTimeList(String cronExpression, int num){
        return getNextTimeList(cronExpression, null, num);
    }
    
    /**
     * 生成多個執行時間的日期格式化
     * @param cronExpression
     * @param date
     * @param num
     * @return 返回格式化時間 yyyy-MM-dd HH:mm:ss
     */
    public static List<String> getNextTimeStrList(String cronExpression, Date date, int num){
        List<String> dateStrList = new ArrayList<>();
        List<Date> dateList = getNextTimeList(cronExpression, date, num);
        if(CollectionUtil.isNotEmpty(dateList)){
            dateList.stream().forEach(d->{
                String dateStr = DateTimeFormatterUtil.formatToDateTimeStr(d);
                dateStrList.add(dateStr);
            });
        }
        return dateStrList;
    }
    
    /**
     * 生成多個執行時間的日期格式化
     * @param cronExpression
     * @param num
     * @return 返回格式化時間 yyyy-MM-dd HH:mm:ss
     */
    public static List<String> getNextTimeStrList(String cronExpression, int num){
        return getNextTimeStrList(cronExpression, null, num);
    }
    
    /**
     * 對比Cron表達式下一個執行時間是否與指定date相等
     * @param cronExpression
     * @param date
     * @return
     */
    public static boolean isSatisfiedBy(String cronExpression, Date date){
        try {
            return new CronExpression(cronExpression).isSatisfiedBy(date);
        } catch (ParseException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

}

 

4.2 測試代碼

package com.xkzhangsan.time.test;

import java.util.Date;

import org.junit.Test;

import com.xkzhangsan.time.cron.CronExpressionUtil;

public class CronExpressionTest {

    /**
     * Cron表達式工具測試
     */
    @Test
    public void cronExpressionTest(){
        //驗證cron
        System.out.println(CronExpressionUtil.isValidExpression("* * * * * ?"));
        //格式化cron
        System.out.println(CronExpressionUtil.formatExpression(" * * * * * ? *"));
        //下一個運行時間
        System.out.println(CronExpressionUtil.getNextTimeStr("10 * * * * ?"));
        //多個下一個運行時間
        System.out.println(CronExpressionUtil.getNextTimeStrList("10 * * * * ?", 10));
        //對比Cron表達式下一個執行時間是否與指定date相等
        Date date = new Date();
        String cronExpression = "0 10 * * * ?";
        Date nextDate = CronExpressionUtil.getNextTime(cronExpression, date);
        System.out.println(CronExpressionUtil.isSatisfiedBy(cronExpression, nextDate));
    }
}

 

輸出:

true
 * * * * * ? *
2020-04-17 09:44:10
[2020-04-17 09:44:10, 2020-04-17 09:45:10, 2020-04-17 09:46:10, 2020-04-17 09:47:10, 2020-04-17 09:48:10, 2020-04-17 09:49:10, 2020-04-17 09:50:10, 2020-04-17 09:51:10, 2020-04-17 09:52:10, 2020-04-17 09:53:10]
true

 

參考:https://www.cnblogs.com/yanghj010/p/10875151.html

源代碼地址:https://github.com/xkzhangsan/xk-time

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章