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

  • .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件已关闭评论
  • 161 次浏览
  • A+
所属分类:.NET技术
摘要

常用的定时任务组件有 Quartz.Net 和 Hangfire 两种,这两种是使用人数比较多的定时任务组件,个人以前也是使用的 Hangfire ,慢慢的发现自己想要的其实只是一个能够根据 Cron 表达式来定时执行函数的功能,Quartz.Net 和 Hangfire 虽然都能实现这个目的,但是他们都只用来实现 Cron表达式解析定时执行函数就显得太笨重了,所以想着以 解析 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展开;                 }                  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展开;                 }                  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;             }          }      }  }

 

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秒钟循环重复这个逻辑。

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

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

 

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

 

至此 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 就讲解完了,有任何不明白的,可以在文章下面评论或者私信我,欢迎大家积极的讨论交流,有兴趣的朋友可以关注我目前在维护的一个 .NET 基础框架项目,项目地址如下