• .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
        {
    
    
            /// 
            /// 获取当前时间之后下一次触发时间
            /// 
            /// 
            /// 
            public static DateTimeOffset GetNextOccurrence(string cronExpression)
            {
                return GetNextOccurrence(cronExpression, DateTimeOffset.UtcNow);
            }
    
    
    
            /// 
            /// 获取给定时间之后下一次触发时间
            /// 
            /// 
            /// 
            /// 
            public static DateTimeOffset GetNextOccurrence(string cronExpression, DateTimeOffset afterTimeUtc)
            {
                return new CronExpression(cronExpression).GetTimeAfter(afterTimeUtc)!.Value;
            }
    
    
    
            /// 
            /// 获取当前时间之后N次触发时间
            /// 
            /// 
            /// 
            /// 
            public static List GetNextOccurrences(string cronExpression, int count)
            {
                return GetNextOccurrences(cronExpression, DateTimeOffset.UtcNow, count);
            }
    
    
    
            /// 
            /// 获取给定时间之后N次触发时间
            /// 
            /// 
            /// 
            /// 
            public static List GetNextOccurrences(string cronExpression, DateTimeOffset afterTimeUtc, int count)
            {
                CronExpression cron = new(cronExpression);
    
                List 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);
                }
    
    
    
    
    
                /// 
                /// 构建表达式
                /// 
                /// 
                /// 
                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);
                    }
                }
    
    
    
                /// 
                /// Stores the expression values.
                /// 
                /// The position.
                /// The string to traverse.
                /// The type of value.
                /// 
                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}");
                    }
                }
    
    
    
                /// 
                /// Checks the next value.
                /// 
                /// The position.
                /// The string to check.
                /// The value.
                /// The type to search.
                /// 
                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;
                }
    
    
    
                /// 
                /// Gets the cron expression string.
                /// 
                /// The cron expression string.
                private static string CronExpressionString;
    
    
    
    
                /// 
                /// Skips the white space.
                /// 
                /// The i.
                /// The s.
                /// 
                private static int SkipWhiteSpace(int i, string s)
                {
                    for (; i < s.Length && (s[i] == ' ' || s[i] == '\t'); i++)
                    {
                    }
    
                    return i;
                }
    
    
    
                /// 
                /// Finds the next white space.
                /// 
                /// The i.
                /// The s.
                /// 
                private static int FindNextWhiteSpace(int i, string s)
                {
                    for (; i < s.Length && (s[i] != ' ' || s[i] != '\t'); i++)
                    {
                    }
    
                    return i;
                }
    
    
    
                /// 
                /// Adds to set.
                /// 
                /// The val.
                /// The end.
                /// The incr.
                /// The type.
                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);
                        }
                    }
                }
    
    
    
                /// 
                /// Gets the set of given type.
                /// 
                /// The type of set to get.
                /// 
                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();
                    }
                }
    
    
    
                /// 
                /// Gets the value.
                /// 
                /// The v.
                /// The s.
                /// The i.
                /// 
                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;
                }
    
    
    
                /// 
                /// Gets the numeric value from string.
                /// 
                /// The string to parse from.
                /// The i.
                /// 
                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);
                }
    
    
    
                /// 
                /// Gets the month number.
                /// 
                /// The string to map with.
                /// 
                private static int GetMonthNumber(string s)
                {
                    if (monthMap.ContainsKey(s))
                    {
                        return monthMap[s];
                    }
    
                    return -1;
                }
    
    
    
                /// 
                /// Gets the day of week number.
                /// 
                /// The s.
                /// 
                private static int GetDayOfWeekNumber(string s)
                {
                    if (dayMap.ContainsKey(s))
                    {
                        return dayMap[s];
                    }
    
                    return -1;
                }
    
    
    
                /// 
                /// 在给定时间之后获取下一个触发时间。
                /// 
                /// 开始搜索的 UTC 时间。
                /// 
                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();
                }
    
    
    
                /// 
                /// Creates the date time without milliseconds.
                /// 
                /// The time.
                /// 
                private static DateTimeOffset CreateDateTimeWithoutMillis(DateTimeOffset time)
                {
                    return new DateTimeOffset(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Offset);
                }
    
    
    
                /// 
                /// Advance the calendar to the particular hour paying particular attention
                /// to daylight saving problems.
                /// 
                /// The date.
                /// The hour.
                /// 
                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;
                }
    
    
    
                /// 
                /// Gets the last day of month.
                /// 
                /// The month num.
                /// The year.
                /// 
                private static int GetLastDayOfMonth(int monthNum, int year)
                {
                    return DateTime.DaysInMonth(year, monthNum);
                }
    
    
                private class ValueSet
                {
                    public int theValue;
    
                    public int pos;
                }
    
            }
    
        }
    
    }
    复制代码

     

    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 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();
    
                    //省略业务代码
                    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();
                    var distLock = scope.ServiceProvider.GetRequiredService();
    
                    //省略业务代码
                    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 基础框架项目,项目地址如下
  • 相关阅读:
    Mybatis中的动态SQL
    辅助驾驶功能开发-功能规范篇(24)-1-影子模式功能触发规范
    论软件的可靠性设计
    SpringCloud中服务间通信方式以及Ribbon、Openfeign组件的使用
    【python第三方库】easydict的使用
    IDEA插件Apifox,一键自动生成接口文档!
    从零开始的Docker Desktop使用,Docker快速上手 ( ̄︶ ̄) Docker介绍和基础使用
    GitHub神坛变动,10W字Spring Cloud Alibaba笔记,30W星标登顶第一
    C++的类型转换
    docker和虚拟机的异同
  • 原文地址:https://www.cnblogs.com/berkerdong/p/16619415.html