.NET纯原生实现Cron定时任务执行,未依赖第三方组件

博客 分享
0 150
张三
张三 2022-08-24 13:03:32
悬赏:0 积分 收藏

.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件

常用的定时任务组件有 Quartz.Net 和 Hangfire 两种,这两种是使用人数比较多的定时任务组件,个人以前也是使用的 Hangfire ,慢慢的发现自己想要的其实只是一个能够根据 Cron 表达式来定时执行函数的功能,Quartz.Net 和 Hangfire 虽然都能实现这个目的,但是他们都只用来实现 Cron表达式解析定时执行函数就显得太笨重了,所以想着以 解析 Cron表达式定期执行函数为目的,编写了下面的一套逻辑。

首先为了解析 Cron表达式,我们需要一个CronHelper ,代码如下

using System.Globalization;using System.Text;using System.Text.RegularExpressions;namespace Common{    public class CronHelper    {        /// <summary>        /// 获取当前时间之后下一次触发时间        /// </summary>        /// <param name="cronExpression"></param>        /// <returns></returns>        public static DateTimeOffset GetNextOccurrence(string cronExpression)        {            return GetNextOccurrence(cronExpression, DateTimeOffset.UtcNow);        }        /// <summary>        /// 获取给定时间之后下一次触发时间        /// </summary>        /// <param name="cronExpression"></param>        /// <param name="afterTimeUtc"></param>        /// <returns></returns>        public static DateTimeOffset GetNextOccurrence(string cronExpression, DateTimeOffset afterTimeUtc)        {            return new CronExpression(cronExpression).GetTimeAfter(afterTimeUtc)!.Value;        }        /// <summary>        /// 获取当前时间之后N次触发时间        /// </summary>        /// <param name="cronExpression"></param>        /// <param name="count"></param>        /// <returns></returns>        public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, int count)        {            return GetNextOccurrences(cronExpression, DateTimeOffset.UtcNow, count);        }        /// <summary>        /// 获取给定时间之后N次触发时间        /// </summary>        /// <param name="cronExpression"></param>        /// <param name="afterTimeUtc"></param>        /// <returns></returns>        public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, DateTimeOffset afterTimeUtc, int count)        {            CronExpression cron = new(cronExpression);            List<DateTimeOffset> dateTimeOffsets = new();            for (int i = 0; i < count; i++)            {                afterTimeUtc = cron.GetTimeAfter(afterTimeUtc)!.Value;                dateTimeOffsets.Add(afterTimeUtc);            }            return dateTimeOffsets;        }        private class CronExpression        {            private const int Second = 0;            private const int Minute = 1;            private const int Hour = 2;            private const int DayOfMonth = 3;            private const int Month = 4;            private const int DayOfWeek = 5;            private const int Year = 6;            private const int AllSpecInt = 99;            private const int NoSpecInt = 98;            private const int AllSpec = AllSpecInt;            private const int NoSpec = NoSpecInt;            private SortedSet<int> seconds = null!;            private SortedSet<int> minutes = null!;            private SortedSet<int> hours = null!;            private SortedSet<int> daysOfMonth = null!;            private SortedSet<int> months = null!;            private SortedSet<int> daysOfWeek = null!;            private SortedSet<int> years = null!;            private bool lastdayOfWeek;            private int everyNthWeek;            private int nthdayOfWeek;            private bool lastdayOfMonth;            private bool nearestWeekday;            private int lastdayOffset;            private static readonly Dictionary<string, int> monthMap = new Dictionary<string, int>(20);            private static readonly Dictionary<string, int> dayMap = new Dictionary<string, int>(60);            private static readonly int MaxYear = DateTime.Now.Year + 100;            private static readonly char[] splitSeparators = { ' ', '\t', '\r', '\n' };            private static readonly char[] commaSeparator = { ',' };            private static readonly Regex regex = new Regex("^L-[0-9]*[W]?", RegexOptions.Compiled);            private static readonly TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local;            public CronExpression(string cronExpression)            {                if (monthMap.Count == 0)                {                    monthMap.Add("JAN", 0);                    monthMap.Add("FEB", 1);                    monthMap.Add("MAR", 2);                    monthMap.Add("APR", 3);                    monthMap.Add("MAY", 4);                    monthMap.Add("JUN", 5);                    monthMap.Add("JUL", 6);                    monthMap.Add("AUG", 7);                    monthMap.Add("SEP", 8);                    monthMap.Add("OCT", 9);                    monthMap.Add("NOV", 10);                    monthMap.Add("DEC", 11);                    dayMap.Add("SUN", 1);                    dayMap.Add("MON", 2);                    dayMap.Add("TUE", 3);                    dayMap.Add("WED", 4);                    dayMap.Add("THU", 5);                    dayMap.Add("FRI", 6);                    dayMap.Add("SAT", 7);                }                if (cronExpression == null)                {                    throw new ArgumentException("cronExpression 不能为空");                }                CronExpressionString = CultureInfo.InvariantCulture.TextInfo.ToUpper(cronExpression);                BuildExpression(CronExpressionString);            }            /// <summary>            /// 构建表达式            /// </summary>            /// <param name="expression"></param>            /// <exception cref="FormatException"></exception>            private void BuildExpression(string expression)            {                try                {                    seconds ??= new SortedSet<int>();                    minutes ??= new SortedSet<int>();                    hours ??= new SortedSet<int>();                    daysOfMonth ??= new SortedSet<int>();                    months ??= new SortedSet<int>();                    daysOfWeek ??= new SortedSet<int>();                    years ??= new SortedSet<int>();                    int exprOn = Second;                    string[] exprsTok = expression.Split(splitSeparators, StringSplitOptions.RemoveEmptyEntries);                    foreach (string exprTok in exprsTok)                    {                        string expr = exprTok.Trim();                        if (expr.Length == 0)                        {                            continue;                        }                        if (exprOn > Year)                        {                            break;                        }                        if (exprOn == DayOfMonth && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0)                        {                            throw new FormatException("不支持在月份的其他日期指定“L”和“LW”");                        }                        if (exprOn == DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0)                        {                            throw new FormatException("不支持在一周的其他日期指定“L”");                        }                        if (exprOn == DayOfWeek && expr.IndexOf('#') != -1 && expr.IndexOf('#', expr.IndexOf('#') + 1) != -1)                        {                            throw new FormatException("不支持指定多个“第N”天。");                        }                        string[] vTok = expr.Split(commaSeparator);                        foreach (string v in vTok)                        {                            StoreExpressionVals(0, v, exprOn);                        }                        exprOn++;                    }                    if (exprOn <= DayOfWeek)                    {                        throw new FormatException("表达式意料之外的结束。");                    }                    if (exprOn <= Year)                    {                        StoreExpressionVals(0, "*", Year);                    }                    var dow = GetSet(DayOfWeek);                    var dom = GetSet(DayOfMonth);                    bool dayOfMSpec = !dom.Contains(NoSpec);                    bool dayOfWSpec = !dow.Contains(NoSpec);                    if (dayOfMSpec && !dayOfWSpec)                    {                        // skip                    }                    else if (dayOfWSpec && !dayOfMSpec)                    {                        // skip                    }                    else                    {                        throw new FormatException("不支持同时指定星期和日参数。");                    }                }                catch (FormatException)                {                    throw;                }                catch (Exception e)                {                    throw new FormatException($"非法的 cron 表达式格式 ({e.Message})", e);                }            }            /// <summary>            /// Stores the expression values.            /// </summary>            /// <param name="pos">The position.</param>            /// <param name="s">The string to traverse.</param>            /// <param name="type">The type of value.</param>            /// <returns></returns>            private int StoreExpressionVals(int pos, string s, int type)            {                int incr = 0;                int i = SkipWhiteSpace(pos, s);                if (i >= s.Length)                {                    return i;                }                char c = s[i];                if (c >= 'A' && c <= 'Z' && !s.Equals("L") && !s.Equals("LW") && !regex.IsMatch(s))                {                    string sub = s.Substring(i, 3);                    int sval;                    int eval = -1;                    if (type == Month)                    {                        sval = GetMonthNumber(sub) + 1;                        if (sval <= 0)                        {                            throw new FormatException($"无效的月份值:'{sub}'");                        }                        if (s.Length > i + 3)                        {                            c = s[i + 3];                            if (c == '-')                            {                                i += 4;                                sub = s.Substring(i, 3);                                eval = GetMonthNumber(sub) + 1;                                if (eval <= 0)                                {                                    throw new FormatException(                                        $"无效的月份值: '{sub}'");                                }                            }                        }                    }                    else if (type == DayOfWeek)                    {                        sval = GetDayOfWeekNumber(sub);                        if (sval < 0)                        {                            throw new FormatException($"无效的星期几值: '{sub}'");                        }                        if (s.Length > i + 3)                        {                            c = s[i + 3];                            if (c == '-')                            {                                i += 4;                                sub = s.Substring(i, 3);                                eval = GetDayOfWeekNumber(sub);                                if (eval < 0)                                {                                    throw new FormatException(                                        $"无效的星期几值: '{sub}'");                                }                            }                            else if (c == '#')                            {                                try                                {                                    i += 4;                                    nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);                                    if (nthdayOfWeek is < 1 or > 5)                                    {                                        throw new FormatException("周的第n天小于1或大于5");                                    }                                }                                catch (Exception)                                {                                    throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面");                                }                            }                            else if (c == '/')                            {                                try                                {                                    i += 4;                                    everyNthWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);                                    if (everyNthWeek is < 1 or > 5)                                    {                                        throw new FormatException("每个星期<1或>5");                                    }                                }                                catch (Exception)                                {                                    throw new FormatException("1 到 5 之间的数值必须跟在 '/' 选项后面");                                }                            }                            else if (c == 'L')                            {                                lastdayOfWeek = true;                                i++;                            }                            else                            {                                throw new FormatException($"此位置的非法字符:'{sub}'");                            }                        }                    }                    else                    {                        throw new FormatException($"此位置的非法字符:'{sub}'");                    }                    if (eval != -1)                    {                        incr = 1;                    }                    AddToSet(sval, eval, incr, type);                    return i + 3;                }                if (c == '?')                {                    i++;                    if (i + 1 < s.Length && s[i] != ' ' && s[i + 1] != '\t')                    {                        throw new FormatException("'?' 后的非法字符: " + s[i]);                    }                    if (type != DayOfWeek && type != DayOfMonth)                    {                        throw new FormatException(                            "'?' 只能为月日或周日指定。");                    }                    if (type == DayOfWeek && !lastdayOfMonth)                    {                        int val = daysOfMonth.LastOrDefault();                        if (val == NoSpecInt)                        {                            throw new FormatException(                                "'?' 只能为月日或周日指定。");                        }                    }                    AddToSet(NoSpecInt, -1, 0, type);                    return i;                }                var startsWithAsterisk = c == '*';                if (startsWithAsterisk || c == '/')                {                    if (startsWithAsterisk && i + 1 >= s.Length)                    {                        AddToSet(AllSpecInt, -1, incr, type);                        return i + 1;                    }                    if (c == '/' && (i + 1 >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t'))                    {                        throw new FormatException("'/' 后面必须跟一个整数。");                    }                    if (startsWithAsterisk)                    {                        i++;                    }                    c = s[i];                    if (c == '/')                    {                        // is an increment specified?                        i++;                        if (i >= s.Length)                        {                            throw new FormatException("字符串意外结束。");                        }                        incr = GetNumericValue(s, i);                        i++;                        if (incr > 10)                        {                            i++;                        }                        CheckIncrementRange(incr, type);                    }                    else                    {                        if (startsWithAsterisk)                        {                            throw new FormatException("星号后的非法字符:" + s);                        }                        incr = 1;                    }                    AddToSet(AllSpecInt, -1, incr, type);                    return i;                }                if (c == 'L')                {                    i++;                    if (type == DayOfMonth)                    {                        lastdayOfMonth = true;                    }                    if (type == DayOfWeek)                    {                        AddToSet(7, 7, 0, type);                    }                    if (type == DayOfMonth && s.Length > i)                    {                        c = s[i];                        if (c == '-')                        {                            ValueSet vs = GetValue(0, s, i + 1);                            lastdayOffset = vs.theValue;                            if (lastdayOffset > 30)                            {                                throw new FormatException("与最后一天的偏移量必须 <= 30");                            }                            i = vs.pos;                        }                        if (s.Length > i)                        {                            c = s[i];                            if (c == 'W')                            {                                nearestWeekday = true;                                i++;                            }                        }                    }                    return i;                }                if (c >= '0' && c <= '9')                {                    int val = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);                    i++;                    if (i >= s.Length)                    {                        AddToSet(val, -1, -1, type);                    }                    else                    {                        c = s[i];                        if (c >= '0' && c <= '9')                        {                            ValueSet vs = GetValue(val, s, i);                            val = vs.theValue;                            i = vs.pos;                        }                        i = CheckNext(i, s, val, type);                        return i;                    }                }                else                {                    throw new FormatException($"意外字符:{c}");                }                return i;            }            // ReSharper disable once UnusedParameter.Local            private static void CheckIncrementRange(int incr, int type)            {                if (incr > 59 && (type == Second || type == Minute))                {                    throw new FormatException($"增量 > 60 : {incr}");                }                if (incr > 23 && type == Hour)                {                    throw new FormatException($"增量 > 24 : {incr}");                }                if (incr > 31 && type == DayOfMonth)                {                    throw new FormatException($"增量 > 31 : {incr}");                }                if (incr > 7 && type == DayOfWeek)                {                    throw new FormatException($"增量 > 7 : {incr}");                }                if (incr > 12 && type == Month)                {                    throw new FormatException($"增量 > 12 : {incr}");                }            }            /// <summary>            /// Checks the next value.            /// </summary>            /// <param name="pos">The position.</param>            /// <param name="s">The string to check.</param>            /// <param name="val">The value.</param>            /// <param name="type">The type to search.</param>            /// <returns></returns>            private int CheckNext(int pos, string s, int val, int type)            {                int end = -1;                int i = pos;                if (i >= s.Length)                {                    AddToSet(val, end, -1, type);                    return i;                }                char c = s[pos];                if (c == 'L')                {                    if (type == DayOfWeek)                    {                        if (val < 1 || val > 7)                        {                            throw new FormatException("星期日值必须介于1和7之间");                        }                        lastdayOfWeek = true;                    }                    else                    {                        throw new FormatException($"'L' 选项在这里无效。(位置={i})");                    }                    var data = GetSet(type);                    data.Add(val);                    i++;                    return i;                }                if (c == 'W')                {                    if (type == DayOfMonth)                    {                        nearestWeekday = true;                    }                    else                    {                        throw new FormatException($"'W' 选项在这里无效。 (位置={i})");                    }                    if (val > 31)                    {                        throw new FormatException("'W' 选项对于大于 31 的值(一个月中的最大天数)没有意义");                    }                    var data = GetSet(type);                    data.Add(val);                    i++;                    return i;                }                if (c == '#')                {                    if (type != DayOfWeek)                    {                        throw new FormatException($"'#' 选项在这里无效。 (位置={i})");                    }                    i++;                    try                    {                        nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);                        if (nthdayOfWeek is < 1 or > 5)                        {                            throw new FormatException("周的第n天小于1或大于5");                        }                    }                    catch (Exception)                    {                        throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面");                    }                    var data = GetSet(type);                    data.Add(val);                    i++;                    return i;                }                if (c == 'C')                {                    if (type == DayOfWeek)                    {                    }                    else if (type == DayOfMonth)                    {                    }                    else                    {                        throw new FormatException($"'C' 选项在这里无效。(位置={i})");                    }                    var data = GetSet(type);                    data.Add(val);                    i++;                    return i;                }                if (c == '-')                {                    i++;                    c = s[i];                    int v = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);                    end = v;                    i++;                    if (i >= s.Length)                    {                        AddToSet(val, end, 1, type);                        return i;                    }                    c = s[i];                    if (c >= '0' && c <= '9')                    {                        ValueSet vs = GetValue(v, s, i);                        int v1 = vs.theValue;                        end = v1;                        i = vs.pos;                    }                    if (i < s.Length && s[i] == '/')                    {                        i++;                        c = s[i];                        int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);                        i++;                        if (i >= s.Length)                        {                            AddToSet(val, end, v2, type);                            return i;                        }                        c = s[i];                        if (c >= '0' && c <= '9')                        {                            ValueSet vs = GetValue(v2, s, i);                            int v3 = vs.theValue;                            AddToSet(val, end, v3, type);                            i = vs.pos;                            return i;                        }                        AddToSet(val, end, v2, type);                        return i;                    }                    AddToSet(val, end, 1, type);                    return i;                }                if (c == '/')                {                    if (i + 1 >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t')                    {                        throw new FormatException("\'/\' 后面必须跟一个整数。");                    }                    i++;                    c = s[i];                    int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);                    i++;                    if (i >= s.Length)                    {                        CheckIncrementRange(v2, type);                        AddToSet(val, end, v2, type);                        return i;                    }                    c = s[i];                    if (c >= '0' && c <= '9')                    {                        ValueSet vs = GetValue(v2, s, i);                        int v3 = vs.theValue;                        CheckIncrementRange(v3, type);                        AddToSet(val, end, v3, type);                        i = vs.pos;                        return i;                    }                    throw new FormatException($"意外的字符 '{c}' 后 '/'");                }                AddToSet(val, end, 0, type);                i++;                return i;            }            /// <summary>            /// Gets the cron expression string.            /// </summary>            /// <value>The cron expression string.</value>            private static string CronExpressionString;            /// <summary>            /// Skips the white space.            /// </summary>            /// <param name="i">The i.</param>            /// <param name="s">The s.</param>            /// <returns></returns>            private static int SkipWhiteSpace(int i, string s)            {                for (; i < s.Length && (s[i] == ' ' || s[i] == '\t'); i++)                {                }                return i;            }            /// <summary>            /// Finds the next white space.            /// </summary>            /// <param name="i">The i.</param>            /// <param name="s">The s.</param>            /// <returns></returns>            private static int FindNextWhiteSpace(int i, string s)            {                for (; i < s.Length && (s[i] != ' ' || s[i] != '\t'); i++)                {                }                return i;            }            /// <summary>            /// Adds to set.            /// </summary>            /// <param name="val">The val.</param>            /// <param name="end">The end.</param>            /// <param name="incr">The incr.</param>            /// <param name="type">The type.</param>            private void AddToSet(int val, int end, int incr, int type)            {                var data = GetSet(type);                if (type == Second || type == Minute)                {                    if ((val < 0 || val > 59 || end > 59) && val != AllSpecInt)                    {                        throw new FormatException("分钟和秒值必须介于0和59之间");                    }                }                else if (type == Hour)                {                    if ((val < 0 || val > 23 || end > 23) && val != AllSpecInt)                    {                        throw new FormatException("小时值必须介于0和23之间");                    }                }                else if (type == DayOfMonth)                {                    if ((val < 1 || val > 31 || end > 31) && val != AllSpecInt                        && val != NoSpecInt)                    {                        throw new FormatException("月日值必须介于1和31之间");                    }                }                else if (type == Month)                {                    if ((val < 1 || val > 12 || end > 12) && val != AllSpecInt)                    {                        throw new FormatException("月份值必须介于1和12之间");                    }                }                else if (type == DayOfWeek)                {                    if ((val == 0 || val > 7 || end > 7) && val != AllSpecInt                        && val != NoSpecInt)                    {                        throw new FormatException("星期日值必须介于1和7之间");                    }                }                if ((incr == 0 || incr == -1) && val != AllSpecInt)                {                    if (val != -1)                    {                        data.Add(val);                    }                    else                    {                        data.Add(NoSpec);                    }                    return;                }                int startAt = val;                int stopAt = end;                if (val == AllSpecInt && incr <= 0)                {                    incr = 1;                    data.Add(AllSpec);                }                if (type == Second || type == Minute)                {                    if (stopAt == -1)                    {                        stopAt = 59;                    }                    if (startAt == -1 || startAt == AllSpecInt)                    {                        startAt = 0;                    }                }                else if (type == Hour)                {                    if (stopAt == -1)                    {                        stopAt = 23;                    }                    if (startAt == -1 || startAt == AllSpecInt)                    {                        startAt = 0;                    }                }                else if (type == DayOfMonth)                {                    if (stopAt == -1)                    {                        stopAt = 31;                    }                    if (startAt == -1 || startAt == AllSpecInt)                    {                        startAt = 1;                    }                }                else if (type == Month)                {                    if (stopAt == -1)                    {                        stopAt = 12;                    }                    if (startAt == -1 || startAt == AllSpecInt)                    {                        startAt = 1;                    }                }                else if (type == DayOfWeek)                {                    if (stopAt == -1)                    {                        stopAt = 7;                    }                    if (startAt == -1 || startAt == AllSpecInt)                    {                        startAt = 1;                    }                }                else if (type == Year)                {                    if (stopAt == -1)                    {                        stopAt = MaxYear;                    }                    if (startAt == -1 || startAt == AllSpecInt)                    {                        startAt = 1970;                    }                }                int max = -1;                if (stopAt < startAt)                {                    switch (type)                    {                        case Second:                            max = 60;                            break;                        case Minute:                            max = 60;                            break;                        case Hour:                            max = 24;                            break;                        case Month:                            max = 12;                            break;                        case DayOfWeek:                            max = 7;                            break;                        case DayOfMonth:                            max = 31;                            break;                        case Year:                            throw new ArgumentException("开始年份必须小于停止年份");                        default:                            throw new ArgumentException("遇到意外的类型");                    }                    stopAt += max;                }                for (int i = startAt; i <= stopAt; i += incr)                {                    if (max == -1)                    {                        data.Add(i);                    }                    else                    {                        int i2 = i % max;                        if (i2 == 0 && (type == Month || type == DayOfWeek || type == DayOfMonth))                        {                            i2 = max;                        }                        data.Add(i2);                    }                }            }            /// <summary>            /// Gets the set of given type.            /// </summary>            /// <param name="type">The type of set to get.</param>            /// <returns></returns>            private SortedSet<int> GetSet(int type)            {                switch (type)                {                    case Second:                        return seconds;                    case Minute:                        return minutes;                    case Hour:                        return hours;                    case DayOfMonth:                        return daysOfMonth;                    case Month:                        return months;                    case DayOfWeek:                        return daysOfWeek;                    case Year:                        return years;                    default:                        throw new ArgumentOutOfRangeException();                }            }            /// <summary>            /// Gets the value.            /// </summary>            /// <param name="v">The v.</param>            /// <param name="s">The s.</param>            /// <param name="i">The i.</param>            /// <returns></returns>            private static ValueSet GetValue(int v, string s, int i)            {                char c = s[i];                StringBuilder s1 = new StringBuilder(v.ToString(CultureInfo.InvariantCulture));                while (c >= '0' && c <= '9')                {                    s1.Append(c);                    i++;                    if (i >= s.Length)                    {                        break;                    }                    c = s[i];                }                ValueSet val = new ValueSet();                if (i < s.Length)                {                    val.pos = i;                }                else                {                    val.pos = i + 1;                }                val.theValue = Convert.ToInt32(s1.ToString(), CultureInfo.InvariantCulture);                return val;            }            /// <summary>            /// Gets the numeric value from string.            /// </summary>            /// <param name="s">The string to parse from.</param>            /// <param name="i">The i.</param>            /// <returns></returns>            private static int GetNumericValue(string s, int i)            {                int endOfVal = FindNextWhiteSpace(i, s);                string val = s.Substring(i, endOfVal - i);                return Convert.ToInt32(val, CultureInfo.InvariantCulture);            }            /// <summary>            /// Gets the month number.            /// </summary>            /// <param name="s">The string to map with.</param>            /// <returns></returns>            private static int GetMonthNumber(string s)            {                if (monthMap.ContainsKey(s))                {                    return monthMap[s];                }                return -1;            }            /// <summary>            /// Gets the day of week number.            /// </summary>            /// <param name="s">The s.</param>            /// <returns></returns>            private static int GetDayOfWeekNumber(string s)            {                if (dayMap.ContainsKey(s))                {                    return dayMap[s];                }                return -1;            }            /// <summary>            /// 在给定时间之后获取下一个触发时间。            /// </summary>            /// <param name="afterTimeUtc">开始搜索的 UTC 时间。</param>            /// <returns></returns>            public DateTimeOffset? GetTimeAfter(DateTimeOffset afterTimeUtc)            {                // 向前移动一秒钟,因为我们正在计算时间*之后*                afterTimeUtc = afterTimeUtc.AddSeconds(1);                // CronTrigger 不处理毫秒                DateTimeOffset d = CreateDateTimeWithoutMillis(afterTimeUtc);                // 更改为指定时区                d = TimeZoneInfo.ConvertTime(d, timeZoneInfo);                bool gotOne = false;                //循环直到我们计算出下一次,或者我们已经过了 endTime                while (!gotOne)                {                    SortedSet<int> st;                    int t;                    int sec = d.Second;                    st = seconds.GetViewBetween(sec, 9999999);                    if (st.Count > 0)                    {                        sec = st.First();                    }                    else                    {                        sec = seconds.First();                        d = d.AddMinutes(1);                    }                    d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, d.Minute, sec, d.Millisecond, d.Offset);                    int min = d.Minute;                    int hr = d.Hour;                    t = -1;                    st = minutes.GetViewBetween(min, 9999999);                    if (st.Count > 0)                    {                        t = min;                        min = st.First();                    }                    else                    {                        min = minutes.First();                        hr++;                    }                    if (min != t)                    {                        d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, 0, d.Millisecond, d.Offset);                        d = SetCalendarHour(d, hr);                        continue;                    }                    d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, d.Second, d.Millisecond, d.Offset);                    hr = d.Hour;                    int day = d.Day;                    t = -1;                    st = hours.GetViewBetween(hr, 9999999);                    if (st.Count > 0)                    {                        t = hr;                        hr = st.First();                    }                    else                    {                        hr = hours.First();                        day++;                    }                    if (hr != t)                    {                        int daysInMonth = DateTime.DaysInMonth(d.Year, d.Month);                        if (day > daysInMonth)                        {                            d = new DateTimeOffset(d.Year, d.Month, daysInMonth, d.Hour, 0, 0, d.Millisecond, d.Offset).AddDays(day - daysInMonth);                        }                        else                        {                            d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, 0, 0, d.Millisecond, d.Offset);                        }                        d = SetCalendarHour(d, hr);                        continue;                    }                    d = new DateTimeOffset(d.Year, d.Month, d.Day, hr, d.Minute, d.Second, d.Millisecond, d.Offset);                    day = d.Day;                    int mon = d.Month;                    t = -1;                    int tmon = mon;                    bool dayOfMSpec = !daysOfMonth.Contains(NoSpec);                    bool dayOfWSpec = !daysOfWeek.Contains(NoSpec);                    if (dayOfMSpec && !dayOfWSpec)                    {                        // 逐月获取规则                        st = daysOfMonth.GetViewBetween(day, 9999999);                        bool found = st.Any();                        if (lastdayOfMonth)                        {                            if (!nearestWeekday)                            {                                t = day;                                day = GetLastDayOfMonth(mon, d.Year);                                day -= lastdayOffset;                                if (t > day)                                {                                    mon++;                                    if (mon > 12)                                    {                                        mon = 1;                                        tmon = 3333; // 确保下面的 mon != tmon 测试失败                                        d = d.AddYears(1);                                    }                                    day = 1;                                }                            }                            else                            {                                t = day;                                day = GetLastDayOfMonth(mon, d.Year);                                day -= lastdayOffset;                                DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);                                int ldom = GetLastDayOfMonth(mon, d.Year);                                DayOfWeek dow = tcal.DayOfWeek;                                if (dow == System.DayOfWeek.Saturday && day == 1)                                {                                    day += 2;                                }                                else if (dow == System.DayOfWeek.Saturday)                                {                                    day -= 1;                                }                                else if (dow == System.DayOfWeek.Sunday && day == ldom)                                {                                    day -= 2;                                }                                else if (dow == System.DayOfWeek.Sunday)                                {                                    day += 1;                                }                                DateTimeOffset nTime = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Millisecond, d.Offset);                                if (nTime.ToUniversalTime() < afterTimeUtc)                                {                                    day = 1;                                    mon++;                                }                            }                        }                        else if (nearestWeekday)                        {                            t = day;                            day = daysOfMonth.First();                            DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);                            int ldom = GetLastDayOfMonth(mon, d.Year);                            DayOfWeek dow = tcal.DayOfWeek;                            if (dow == System.DayOfWeek.Saturday && day == 1)                            {                                day += 2;                            }                            else if (dow == System.DayOfWeek.Saturday)                            {                                day -= 1;                            }                            else if (dow == System.DayOfWeek.Sunday && day == ldom)                            {                                day -= 2;                            }                            else if (dow == System.DayOfWeek.Sunday)                            {                                day += 1;                            }                            tcal = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Offset);                            if (tcal.ToUniversalTime() < afterTimeUtc)                            {                                day = daysOfMonth.First();                                mon++;                            }                        }                        else if (found)                        {                            t = day;                            day = st.First();                            //确保我们不会在短时间内跑得过快,比如二月                            int lastDay = GetLastDayOfMonth(mon, d.Year);                            if (day > lastDay)                            {                                day = daysOfMonth.First();                                mon++;                            }                        }                        else                        {                            day = daysOfMonth.First();                            mon++;                        }                        if (day != t || mon != tmon)                        {                            if (mon > 12)                            {                                d = new DateTimeOffset(d.Year, 12, day, 0, 0, 0, d.Offset).AddMonths(mon - 12);                            }                            else                            {                                //这是为了避免从一个月移动时出现错误                                //有 30 或 31 天到一个月更少。 导致实例化无效的日期时间。                                int lDay = DateTime.DaysInMonth(d.Year, mon);                                if (day <= lDay)                                {                                    d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);                                }                                else                                {                                    d = new DateTimeOffset(d.Year, mon, lDay, 0, 0, 0, d.Offset).AddDays(day - lDay);                                }                            }                            continue;                        }                    }                    else if (dayOfWSpec && !dayOfMSpec)                    {                        // 获取星期几规则                        if (lastdayOfWeek)                        {                            int dow = daysOfWeek.First();                            int cDow = (int)d.DayOfWeek + 1;                            int daysToAdd = 0;                            if (cDow < dow)                            {                                daysToAdd = dow - cDow;                            }                            if (cDow > dow)                            {                                daysToAdd = dow + (7 - cDow);                            }                            int lDay = GetLastDayOfMonth(mon, d.Year);                            if (day + daysToAdd > lDay)                            {                                if (mon == 12)                                {                                    d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);                                }                                else                                {                                    d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);                                }                                continue;                            }                            // 查找本月这一天最后一次出现的日期...                            while (day + daysToAdd + 7 <= lDay)                            {                                daysToAdd += 7;                            }                            day += daysToAdd;                            if (daysToAdd > 0)                            {                                d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);                                continue;                            }                        }                        else if (nthdayOfWeek != 0)                        {                            int dow = daysOfWeek.First();                            int cDow = (int)d.DayOfWeek + 1;                            int daysToAdd = 0;                            if (cDow < dow)                            {                                daysToAdd = dow - cDow;                            }                            else if (cDow > dow)                            {                                daysToAdd = dow + (7 - cDow);                            }                            bool dayShifted = daysToAdd > 0;                            day += daysToAdd;                            int weekOfMonth = day / 7;                            if (day % 7 > 0)                            {                                weekOfMonth++;                            }                            daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;                            day += daysToAdd;                            if (daysToAdd < 0 || day > GetLastDayOfMonth(mon, d.Year))                            {                                if (mon == 12)                                {                                    d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);                                }                                else                                {                                    d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);                                }                                continue;                            }                            if (daysToAdd > 0 || dayShifted)                            {                                d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);                                continue;                            }                        }                        else if (everyNthWeek != 0)                        {                            int cDow = (int)d.DayOfWeek + 1;                            int dow = daysOfWeek.First();                            st = daysOfWeek.GetViewBetween(cDow, 9999999);                            if (st.Count > 0)                            {                                dow = st.First();                            }                            int daysToAdd = 0;                            if (cDow < dow)                            {                                daysToAdd = (dow - cDow) + (7 * (everyNthWeek - 1));                            }                            if (cDow > dow)                            {                                daysToAdd = (dow + (7 - cDow)) + (7 * (everyNthWeek - 1));                            }                            if (daysToAdd > 0)                            {                                d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);                                d = d.AddDays(daysToAdd);                                continue;                            }                        }                        else                        {                            int cDow = (int)d.DayOfWeek + 1;                            int dow = daysOfWeek.First();                            st = daysOfWeek.GetViewBetween(cDow, 9999999);                            if (st.Count > 0)                            {                                dow = st.First();                            }                            int daysToAdd = 0;                            if (cDow < dow)                            {                                daysToAdd = dow - cDow;                            }                            if (cDow > dow)                            {                                daysToAdd = dow + (7 - cDow);                            }                            int lDay = GetLastDayOfMonth(mon, d.Year);                            if (day + daysToAdd > lDay)                            {                                if (mon == 12)                                {                                    d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);                                }                                else                                {                                    d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);                                }                                continue;                            }                            if (daysToAdd > 0)                            {                                d = new DateTimeOffset(d.Year, mon, day + daysToAdd, 0, 0, 0, d.Offset);                                continue;                            }                        }                    }                    else                    {                        throw new FormatException("不支持同时指定星期日和月日参数。");                    }                    d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, d.Minute, d.Second, d.Offset);                    mon = d.Month;                    int year = d.Year;                    t = -1;                    if (year > MaxYear)                    {                        return null;                    }                    st = months.GetViewBetween(mon, 9999999);                    if (st.Count > 0)                    {                        t = mon;                        mon = st.First();                    }                    else                    {                        mon = months.First();                        year++;                    }                    if (mon != t)                    {                        d = new DateTimeOffset(year, mon, 1, 0, 0, 0, d.Offset);                        continue;                    }                    d = new DateTimeOffset(d.Year, mon, d.Day, d.Hour, d.Minute, d.Second, d.Offset);                    year = d.Year;                    t = -1;                    st = years.GetViewBetween(year, 9999999);                    if (st.Count > 0)                    {                        t = year;                        year = st.First();                    }                    else                    {                        return null;                    }                    if (year != t)                    {                        d = new DateTimeOffset(year, 1, 1, 0, 0, 0, d.Offset);                        continue;                    }                    d = new DateTimeOffset(year, d.Month, d.Day, d.Hour, d.Minute, d.Second, d.Offset);                    //为此日期应用适当的偏移量                    d = new DateTimeOffset(d.DateTime, timeZoneInfo.BaseUtcOffset);                    gotOne = true;                }                return d.ToUniversalTime();            }            /// <summary>            /// Creates the date time without milliseconds.            /// </summary>            /// <param name="time">The time.</param>            /// <returns></returns>            private static DateTimeOffset CreateDateTimeWithoutMillis(DateTimeOffset time)            {                return new DateTimeOffset(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Offset);            }            /// <summary>            /// Advance the calendar to the particular hour paying particular attention            /// to daylight saving problems.            /// </summary>            /// <param name="date">The date.</param>            /// <param name="hour">The hour.</param>            /// <returns></returns>            private static DateTimeOffset SetCalendarHour(DateTimeOffset date, int hour)            {                int hourToSet = hour;                if (hourToSet == 24)                {                    hourToSet = 0;                }                DateTimeOffset d = new DateTimeOffset(date.Year, date.Month, date.Day, hourToSet, date.Minute, date.Second, date.Millisecond, date.Offset);                if (hour == 24)                {                    d = d.AddDays(1);                }                return d;            }            /// <summary>            /// Gets the last day of month.            /// </summary>            /// <param name="monthNum">The month num.</param>            /// <param name="year">The year.</param>            /// <returns></returns>            private static int GetLastDayOfMonth(int monthNum, int year)            {                return DateTime.DaysInMonth(year, monthNum);            }            private class ValueSet            {                public int theValue;                public int pos;            }        }    }}
View Code

 

CronHelper 中 CronExpression 的函数计算逻辑是从 Quart.NET 借鉴的,支持标准的 7位 cron 表达式,在需要生成Cron 表达式时可以直接使用网络上的各种 Cron 表达式在线生成

CronHelper 里面我们主要用到的功能就是 通过 Cron 表达式,解析下一次的执行时间。

 

服务运行这块我们采用微软的 BackgroundService 后台服务,这里还要用到一个后台服务批量注入的逻辑 关于后台逻辑批量注入可以看我之前写的一篇博客,这里就不展开介绍了

.NET 使用自带 DI 批量注入服务(Service)和 后台服务(BackgroundService) https://www.cnblogs.com/berkerdong/p/16496232.html

 

接下来看一下我这里写的一个DemoTask,代码如下:

using DistributedLock;using Repository.Database;using TaskService.Libraries;namespace TaskService.Tasks{    public class DemoTask : BackgroundService    {        private readonly IServiceProvider serviceProvider;        private readonly ILogger logger;        public DemoTask(IServiceProvider serviceProvider, ILogger<DemoTask> logger)        {            this.serviceProvider = serviceProvider;            this.logger = logger;        }        protected override async Task ExecuteAsync(CancellationToken stoppingToken)        {            CronSchedule.BatchBuilder(stoppingToken, this);            await Task.Delay(-1, stoppingToken);        }        [CronSchedule(Cron = "0/1 * * * * ?")]        public void ClearLog()        {            try            {                using var scope = serviceProvider.CreateScope();                var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();                //省略业务代码                Console.WriteLine("ClearLog:" + DateTime.Now);            }            catch (Exception ex)            {                logger.LogError(ex, "DemoTask.ClearLog");            }        }        [CronSchedule(Cron = "0/5 * * * * ?")]        public void ClearCache()        {            try            {                using var scope = serviceProvider.CreateScope();                var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();                var distLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();                //省略业务代码                Console.WriteLine("ClearCache:" + DateTime.Now);            }            catch (Exception ex)            {                logger.LogError(ex, "DemoTask.ClearCache");            }        }    }}

 

该Task中有两个方法 ClearLog 和 ClearCache 他们分别会每1秒和每5秒执行一次。需要注意在后台服务中对于 Scope 生命周期的服务在获取是需要手动 CreateScope();

实现的关键点在于 服务执行 ExecuteAsync 中的 CronSchedule.BatchBuilder(stoppingToken, this); 我们这里将代码有 CronSchedule 标记头的方法全部循环进行了启动,该方法的代码如下:

using Common;using System.Reflection;namespace TaskService.Libraries{    public class CronSchedule    {     public static void BatchBuilder(CancellationToken stoppingToken, object context)        {            var taskList = context.GetType().GetMethods().Where(t => t.GetCustomAttributes(typeof(CronScheduleAttribute), false).Length > 0).ToList();            foreach (var t in taskList)            {                string cron = t.CustomAttributes.Where(t => t.AttributeType == typeof(CronScheduleAttribute)).FirstOrDefault()!.NamedArguments.Where(t => t.MemberName == "Cron" && t.TypedValue.Value != null).Select(t => t.TypedValue.Value!.ToString()).FirstOrDefault()!;                Builder(stoppingToken, cron, t, context);            }        }        private static async void Builder(CancellationToken stoppingToken, string cronExpression, MethodInfo action, object context)        {            var nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));            while (!stoppingToken.IsCancellationRequested)            {                var nowTime = DateTime.Parse(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"));                if (nextTime == nowTime)                {                    _ = Task.Run(() =>                    {                        action.Invoke(context, null);                    });                    nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));                }                else if (nextTime < nowTime)                {                    nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));                }                await Task.Delay(1000, stoppingToken);            }        }    }    [AttributeUsage(AttributeTargets.Method)]    public class CronScheduleAttribute : Attribute    {        public string Cron { get; set; }    }}

 

主要就是利用反射获取当前类中所有带有 CronSchedule 标记的方法,然后解析对应的 Cron 表达式获取下一次的执行时间,如果执行时间等于当前时间则执行一次方法,否则等待1秒钟循环重复这个逻辑。

然后启动我们的项目就可以看到如下的运行效果:

 

 ClearLog 每1秒钟执行一次,ClearCache 每 5秒钟执行一次

 

至此 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 就讲解完了,有任何不明白的,可以在文章下面评论或者私信我,欢迎大家积极的讨论交流,有兴趣的朋友可以关注我目前在维护的一个 .NET 基础框架项目,项目地址如下
https://github.com/berkerdong/NetEngine.git
https://gitee.com/berkerdong/NetEngine.git
posted @ 2022-08-24 12:58 张晓栋 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    张三

    张三 (王者 段位)

    821 积分 (2)粉丝 (41)源码

     

    温馨提示

    亦奇源码

    最新会员