1. 程式人生 > >根據cron表示式計算每天的執行時刻(可能多個)

根據cron表示式計算每天的執行時刻(可能多個)

         我們專案中一般會有很多的定時任務,我們怎麼知道這些定時任務是否正常執行了呢?一個基本的想法是,在任務執行前儲存一條記錄,任務執行後更新此記錄的結束時間和標記,異常的時候記錄失敗標記和異常資訊,然後管理員每天登入的時候檢查每個任務是否正常執行。如果記錄與設定的執行時刻點匹配,說明任務正常執行了,記錄不存在或者有失敗標記說明任務未正常執行。此謂任務監控。

       定時任務一般地由Quartz或者Spring實現,他們都通過一個cron表示式來指定在什麼時刻執行。對於Quartz可以用Aspectj來攔截job,前後記錄;對於Spring的Scheduled,可以用AOP攔截@Scheduled註解或者自定義一個註解,前後記錄。

       現在的關鍵問題來了,如何根據cron表示式計算每天的哪些時候執行或者該不該執行?

       我們知道,一般地,cron表示式有六個域,分別代表秒、分、時、日、月、星期。每個域可能出現*、/、,、-、數字等。為了模型簡單化,解析每個域即可知道每個域的哪些點應該執行,那麼一天中,秒、分、時的笛卡爾積即是每天的執行點。任意給定的日期,我們就能知道這個日期應不應該執行(判斷星期、月、日),哪些時刻執行(秒、分、時的笛卡爾積)?

       talk is cheap,show me the code。

      首先設計表達每個域的列舉,可以指定位置引數、最小值、最大值。提供根據位置計算列舉域的靜態方法。再聚合一個JavaBean表示一個域。

/**
 * cron表示式某個位置上的一些常量
 * @author xiongshiyan at 2018/11/17 , contact me with email [email protected] or phone 15208384257
 */
public enum  CronPosition {
    SECOND(0 , 0 , 59) ,
    MINUTE(1 , 0 , 59) ,
    HOUR  (2 , 0 , 23) ,
    DAY   (3 , 1 , 31) ,
    MONTH (4 , 1 , 12) ,
    WEEK  (5 , 0 , 6)  ,
    YEAR  (6 , 2018 , 2099);
    /**
     * 在cron表示式中的位置
     */
    private int position;
    /**
     * 該域最小值
     */
    private Integer min;
    /**
     * 該域最大值
     */
    private Integer max;

    CronPosition(int position , Integer min , Integer max){
        this.position = position;
        this.min = min;
        this.max = max;
    }

    public int getPosition() {
        return position;
    }

    public Integer getMin() {
        return min;
    }

    public Integer getMax() {
        return max;
    }

    public static CronPosition fromPosition(int position){
        CronPosition[] values = CronPosition.values();
        for (CronPosition field : values) {
            if(position == field.position){
                return field;
            }
        }
        return null;
    }
}
/**
 * cron表示式的域
 * @author xiongshiyan
 */
public class CronField {
    private CronPosition cronPosition;
    private String express;

    public CronField(CronPosition cronPosition, String express) {
        this.cronPosition = cronPosition;
        this.express = express;
    }
。。。
}

      然後就是切割cron表示式、轉換每個域、計算執行時間點(關鍵演算法,解析cron表示式)、計算某一天的哪些時間點執行。

package top.jfunc.cron.util;

import top.jfunc.cron.pojo.CronField;
import top.jfunc.cron.pojo.CronPosition;
import top.jfunc.cron.pojo.HMS;

import java.util.*;

/**
 * @author xiongshiyan at 2018/11/17 , contact me with email [email protected] or phone 15208384257
 */
public class CronUtil {
    private static final String STAR = "*";
    private static final String COMMA = ",";
    private static final String HYPHEN = "-";
    private static final String SLASH = "/";
    private static final int CRON_LEN = 6;
    private static final int CRON_LEN_YEAR = 7;
    private static final String CRON_CUT = "\\s+";

    /**
     * 計算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觸發
     * ---------------------
     *
     * @param cron cron表示式
     * @param date 時間,某天
     * @return 這一天的哪些時分秒執行, 不執行的返回空
     */
    public static List<HMS> calculate(String cron, Date date) {
        List<CronField> cronFields = convertCronField(cron);
        int year = DateUtil.year(date);
        int week = DateUtil.week(date);
        int month = DateUtil.month(date);
        int day = DateUtil.day(date);
        /// 如果包含年域
        if (CRON_LEN_YEAR == cronFields.size()) {
            if (!assertExecute(year, cronFields.get(CronPosition.YEAR.getPosition()))) {
                return Collections.emptyList();
            }
        }
        ///
        /*//檢查星期域是否應該執行
        assertExecute(week, cronFields.get(CronPosition.WEEK.getPosition()));
        //檢查月域是否應該執行
        assertExecute(month, cronFields.get(CronPosition.MONTH.getPosition()));
        //檢查日域是否應該執行
        assertExecute(day, cronFields.get(CronPosition.DAY.getPosition()));*/

        ///今天不執行就直接返回空
        if (!assertExecute(week, cronFields.get(CronPosition.WEEK.getPosition()))
                || !assertExecute(month, cronFields.get(CronPosition.MONTH.getPosition()))
                || !assertExecute(day, cronFields.get(CronPosition.DAY.getPosition()))) {
            return Collections.emptyList();
        }

        CronField fieldHour = cronFields.get(CronPosition.HOUR.getPosition());
        List<Integer> listHour = calculatePoint(fieldHour);
        CronField fieldMinute = cronFields.get(CronPosition.MINUTE.getPosition());
        List<Integer> listMinute = calculatePoint(fieldMinute);
        CronField fieldSecond = cronFields.get(CronPosition.SECOND.getPosition());
        List<Integer> listSecond = calculatePoint(fieldSecond);

        List<HMS> points = new ArrayList<>(listHour.size() * listMinute.size() * listSecond.size());
        for (Integer hour : listHour) {
            for (Integer minute : listMinute) {
                for (Integer second : listSecond) {
                    points.add(new HMS(hour, minute, second));
                }
            }
        }
        return points;
    }

    private static boolean assertExecute(int num, CronField cronField) {
        if (STAR.equals(cronField.getExpress())) {
            return true;
        }
        //計算出來几几幾要執行
        List<Integer> list = calculatePoint(cronField);
        return numInList(num, list);
    }


    private static boolean numInList(int num, List<Integer> list) {
        for (Integer tmp : list) {
            if (tmp == num) {
                //相同要執行
                return true;
            }
        }
        return false;
    }

    /**
     * 計算某域的哪些點
     *
     * @param cronField cron域
     */
    public static List<Integer> calculatePoint(CronField cronField) {
        List<Integer> list = new ArrayList<>(5);
        String express = cronField.getExpress();
        CronPosition cronPosition = cronField.getCronPosition();
        Integer min = cronPosition.getMin();
        Integer max = cronPosition.getMax();

        // *這種情況
        if (STAR.equals(express)) {
            for (int i = min; i <= max; i++) {
                list.add(i);
            }
            return list;
        }
        // 帶有,的情況,分割之後每部分單獨處理
        if (express.contains(COMMA)) {
            String[] split = express.split(COMMA);
            for (String part : split) {
                list.addAll(calculatePoint(
                        new CronField(cronField.getCronPosition(), part)
                ));
            }
            if (list.size() > 1) {
                //去重
                removeDuplicate(list);
                //排序
                Collections.sort(list);
            }

            return list;
        }
        // 0-3 0/2 3-15/2 5  模式統一為 (min-max)/step
        Integer left;
        Integer right;
        Integer step = 1;

        //包含-的情況
        if (express.contains(HYPHEN)) {
            String[] strings = express.split(HYPHEN);
            left = Integer.valueOf(strings[0]);
            assertRange(cronPosition, left);
            //1-32/2的情況
            if (strings[1].contains(SLASH)) {
                String[] split = strings[1].split(SLASH);
                //32
                right = Integer.valueOf(split[0]);
                assertSize(left, right);
                assertRange(cronPosition, right);
                //2
                step = Integer.valueOf(split[1]);
            } else {
                //1-32的情況
                right = Integer.valueOf(strings[1]);
                assertSize(left, right);
                assertRange(cronPosition, right);
            }
            //僅僅包含/
        } else if (express.contains(SLASH)) {
            String[] strings = express.split(SLASH);
            left = Integer.valueOf(strings[0]);
            assertRange(cronPosition, left);
            step = Integer.valueOf(strings[1]);
            right = max;
            assertSize(left, right);
        } else {
            // 普通的數字
            Integer single = Integer.valueOf(express);
            assertRange(cronPosition, single);
            list.add(single);
            return list;
        }

        for (int i = left; i <= right; i += step) {
            list.add(i);
        }
        return list;

    }

    /**
     * 比較大小,左邊的必須比右邊小
     */
    private static void assertSize(Integer left, Integer right) {
        if (left > right) {
            throw new IllegalArgumentException("right should bigger than left , but find " + left + " > " + right);
        }
    }

    /**
     * 某個域的範圍
     */
    private static void assertRange(CronPosition cronPosition, Integer value) {
        Integer min = cronPosition.getMin();
        Integer max = cronPosition.getMax();
        if (value < min || value > max) {
            throw new IllegalArgumentException(cronPosition.name() + " 域[" + min + " , " + max + "],  but find " + value);
        }
    }

    /**
     * 列表去重
     */
    private static void removeDuplicate(Collection<Integer> list) {
        LinkedHashSet<Integer> set = new LinkedHashSet<>(list.size());
        set.addAll(list);
        list.clear();
        list.addAll(set);
    }

    /**
     * cron表示式轉換為域
     */
    public static List<CronField> convertCronField(String cron) {
        List<String> cut = cut(cron);
        int size = cut.size();
        if (CRON_LEN != size && (CRON_LEN + 1) != size) {
            throw new IllegalArgumentException("cron表示式必須有六個域或者七個域(最後為年)");
        }
        List<CronField> cronFields = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            CronPosition cronPosition = CronPosition.fromPosition(i);
            cronFields.add(new CronField(
                    cronPosition,
                    CronShapingUtil.shaping(
                            cut.get(i), cronPosition)));
        }
        return cronFields;
    }

    /**
     * 把cron表示式切成域
     *
     * @param cron cron
     * @return 代表每個域的列表
     */
    public static List<String> cut(String cron) {
        cron = cron.trim();
        String[] split = cron.split(CRON_CUT);
        List<String> shaping = new ArrayList<>(split.length);
        shaping.addAll(Arrays.asList(split));
        return shaping;
    }
}

    一些輔助工具類。替換?JAN-DEC、SUN-SAT等字串的整形工具類。

package top.jfunc.cron.util;

import top.jfunc.cron.pojo.CronPosition;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author xiongshiyan at 2018/10/12 , contact me with email [email protected] or phone 15208384257
 */
public class CronShapingUtil {
    private static final Map<String , String> MONTH_MAP = new HashMap<>(12);
    private static final Map<String , String> WEEK_MAP  = new HashMap<>(7);
    static {
        MONTH_MAP.put("JAN" , "1");
        MONTH_MAP.put("FEB" , "2");
        MONTH_MAP.put("MAR" , "3");
        MONTH_MAP.put("APR" , "4");
        MONTH_MAP.put("May" , "5");
        MONTH_MAP.put("JUN" , "6");
        MONTH_MAP.put("JUL" , "7");
        MONTH_MAP.put("AUG" , "8");
        MONTH_MAP.put("SEP" , "9");
        MONTH_MAP.put("OCT" , "10");
        MONTH_MAP.put("NOV" , "11");
        MONTH_MAP.put("DEC" , "12");

        WEEK_MAP.put("SUN" , "0");
        WEEK_MAP.put("MON" , "1");
        WEEK_MAP.put("TUE" , "2");
        WEEK_MAP.put("WED" , "3");
        WEEK_MAP.put("THU" , "4");
        WEEK_MAP.put("FRI" , "5");
        WEEK_MAP.put("SAT" , "6");
    }

    /**
     * 域整形,把某些英文字串像JAN/SUN等轉換為相應的數字
     */
    public static String shaping(String express , CronPosition cronPosition){
        if(cronPosition == CronPosition.MONTH){
            express = shapingMonth(express);
        }

        if(cronPosition == CronPosition.WEEK){
            express = shapingWeek(express);
            express = express.replace("?" , "*");
        }

        if(cronPosition == CronPosition.DAY){
            express = express.replace("?" , "*");
        }
        return express;
    }

    private static String shapingMonth(String express){
        Set<Map.Entry<String, String>> entrySet = MONTH_MAP.entrySet();
        for (Map.Entry<String, String> entry : entrySet) {
            if(express.toUpperCase().contains(entry.getKey())){
                express = express.toUpperCase().replace(entry.getKey() , entry.getValue());
            }
        }
        return express;
    }

    private static String shapingWeek(String express){
        Set<Map.Entry<String, String>> entrySet = WEEK_MAP.entrySet();
        for (Map.Entry<String, String> entry : entrySet) {
            if(express.toUpperCase().contains(entry.getKey())){
                express = express.toUpperCase().replace(entry.getKey() , entry.getValue());
            }
        }
        return express;
    }
}

時間計算等工具類。

package top.jfunc.cron.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
 * @author xiongshiyan at 2018/11/18 , contact me with email [email protected] or phone 15208384257
 */
public class DateUtil {
    public static final String SDF_DATETIME       = "yyyy-MM-dd HH:mm:ss";
    public static final String SDF_DATETIME_SHORT = "yyyyMMddHHmmss";
    public static final String SDF_DATETIME_MS    = "yyyyMMddHHmmssSSS";
    public static final String SDF_DATE           = "yyyy-MM-dd";

    /**
     * 字串轉日期
     * @param dateStr 日期字串
     * @return 日期 yyyy-MM-dd HH:mm:ss
     */
    public static Date toDate(String dateStr) {
        return toDate(dateStr, null);
    }

    /**
     * 日期轉字串
     * @param date 日期
     * @return 字串 yyyy-MM-dd HH:mm:ss
     */
    public static String toStr(Date date) {
        return toStr(date, SDF_DATETIME);
    }

    /**
     * 日期轉字串
     * @param date 日期
     * @param format 格式化字串
     * @return 字串
     */
    public static String toStr(Date date, String format) {
        SimpleDateFormat sdf = null;
        if (null != format && !"".equals(format)) {
            sdf = new SimpleDateFormat(format);
            return sdf.format(date);
        } else {
            sdf = new SimpleDateFormat(SDF_DATETIME);
            return sdf.format(date);
        }
    }

    /**
     * 字串轉日期
     * @param dateStr 日期字串
     * @param pattern 格式化字串
     * @return 日期
     */
    public static Date toDate(String dateStr, String pattern) {
        try {
            if (null != pattern && !"".equals(pattern)) {
                return new SimpleDateFormat(pattern).parse(dateStr);
            } else {
                return new SimpleDateFormat(SDF_DATETIME).parse(dateStr);
            }
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 計算某一天是一個月的哪一天
     * @param date 日期
     * @return 1-31
     */
    public static int day(Date date){
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(Calendar.DAY_OF_MONTH);
    }
    /**
     * 計算某一天是星期幾
     * @param date 日期
     * @return 星期幾,星期1是1,星期天是0  0-6
     */
    public static int week(Date date){
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(Calendar.DAY_OF_WEEK) - 1;
    }
    /**
     * 計算某一天的月份
     * @param date 日期
     * @return 月份,1開始
     */
    public static int month(Date date){
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(Calendar.MONTH) + 1;
    }
    /**
     * 計算某一天的年
     * @param date 日期
     * @return 年
     */
    public static int year(Date date){
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get(Calendar.YEAR);
    }
}

除開解析L、W、C等,至此完成。

專案地址見: