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

打印 上一主题 下一主题

主题 902|帖子 902|积分 2706

常用的定时任务组件有 Quartz.Net 和 Hangfire 两种,这两种是使用人数比较多的定时任务组件,个人以前也是使用的 Hangfire ,慢慢的发现自己想要的其实只是一个能够根据 Cron 表达式来定时执行函数的功能,Quartz.Net 和 Hangfire 虽然都能实现这个目的,但是他们都只用来实现 Cron表达式解析定时执行函数就显得太笨重了,所以想着以 解析 Cron表达式定期执行函数为目的,编写了下面的一套逻辑。
首先为了解析 Cron表达式,我们需要一个CronHelper ,代码如下
  1. using System.Globalization;
  2. using System.Text;
  3. using System.Text.RegularExpressions;
  4. namespace Common
  5. {
  6.     public class CronHelper
  7.     {
  8.         /// <summary>
  9.         /// 获取当前时间之后下一次触发时间
  10.         /// </summary>
  11.         /// <param name="cronExpression"></param>
  12.         /// <returns></returns>
  13.         public static DateTimeOffset GetNextOccurrence(string cronExpression)
  14.         {
  15.             return GetNextOccurrence(cronExpression, DateTimeOffset.UtcNow);
  16.         }
  17.         /// <summary>
  18.         /// 获取给定时间之后下一次触发时间
  19.         /// </summary>
  20.         /// <param name="cronExpression"></param>
  21.         /// <param name="afterTimeUtc"></param>
  22.         /// <returns></returns>
  23.         public static DateTimeOffset GetNextOccurrence(string cronExpression, DateTimeOffset afterTimeUtc)
  24.         {
  25.             return new CronExpression(cronExpression).GetTimeAfter(afterTimeUtc)!.Value;
  26.         }
  27.         /// <summary>
  28.         /// 获取当前时间之后N次触发时间
  29.         /// </summary>
  30.         /// <param name="cronExpression"></param>
  31.         /// <param name="count"></param>
  32.         /// <returns></returns>
  33.         public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, int count)
  34.         {
  35.             return GetNextOccurrences(cronExpression, DateTimeOffset.UtcNow, count);
  36.         }
  37.         /// <summary>
  38.         /// 获取给定时间之后N次触发时间
  39.         /// </summary>
  40.         /// <param name="cronExpression"></param>
  41.         /// <param name="afterTimeUtc"></param>
  42.         /// <returns></returns>
  43.         public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, DateTimeOffset afterTimeUtc, int count)
  44.         {
  45.             CronExpression cron = new(cronExpression);
  46.             List<DateTimeOffset> dateTimeOffsets = new();
  47.             for (int i = 0; i < count; i++)
  48.             {
  49.                 afterTimeUtc = cron.GetTimeAfter(afterTimeUtc)!.Value;
  50.                 dateTimeOffsets.Add(afterTimeUtc);
  51.             }
  52.             return dateTimeOffsets;
  53.         }
  54.         private class CronExpression
  55.         {
  56.             private const int Second = 0;
  57.             private const int Minute = 1;
  58.             private const int Hour = 2;
  59.             private const int DayOfMonth = 3;
  60.             private const int Month = 4;
  61.             private const int DayOfWeek = 5;
  62.             private const int Year = 6;
  63.             private const int AllSpecInt = 99;
  64.             private const int NoSpecInt = 98;
  65.             private const int AllSpec = AllSpecInt;
  66.             private const int NoSpec = NoSpecInt;
  67.             private SortedSet<int> seconds = null!;
  68.             private SortedSet<int> minutes = null!;
  69.             private SortedSet<int> hours = null!;
  70.             private SortedSet<int> daysOfMonth = null!;
  71.             private SortedSet<int> months = null!;
  72.             private SortedSet<int> daysOfWeek = null!;
  73.             private SortedSet<int> years = null!;
  74.             private bool lastdayOfWeek;
  75.             private int everyNthWeek;
  76.             private int nthdayOfWeek;
  77.             private bool lastdayOfMonth;
  78.             private bool nearestWeekday;
  79.             private int lastdayOffset;
  80.             private static readonly Dictionary<string, int> monthMap = new Dictionary<string, int>(20);
  81.             private static readonly Dictionary<string, int> dayMap = new Dictionary<string, int>(60);
  82.             private static readonly int MaxYear = DateTime.Now.Year + 100;
  83.             private static readonly char[] splitSeparators = { ' ', '\t', '\r', '\n' };
  84.             private static readonly char[] commaSeparator = { ',' };
  85.             private static readonly Regex regex = new Regex("^L-[0-9]*[W]?", RegexOptions.Compiled);
  86.             private static readonly TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local;
  87.             public CronExpression(string cronExpression)
  88.             {
  89.                 if (monthMap.Count == 0)
  90.                 {
  91.                     monthMap.Add("JAN", 0);
  92.                     monthMap.Add("FEB", 1);
  93.                     monthMap.Add("MAR", 2);
  94.                     monthMap.Add("APR", 3);
  95.                     monthMap.Add("MAY", 4);
  96.                     monthMap.Add("JUN", 5);
  97.                     monthMap.Add("JUL", 6);
  98.                     monthMap.Add("AUG", 7);
  99.                     monthMap.Add("SEP", 8);
  100.                     monthMap.Add("OCT", 9);
  101.                     monthMap.Add("NOV", 10);
  102.                     monthMap.Add("DEC", 11);
  103.                     dayMap.Add("SUN", 1);
  104.                     dayMap.Add("MON", 2);
  105.                     dayMap.Add("TUE", 3);
  106.                     dayMap.Add("WED", 4);
  107.                     dayMap.Add("THU", 5);
  108.                     dayMap.Add("FRI", 6);
  109.                     dayMap.Add("SAT", 7);
  110.                 }
  111.                 if (cronExpression == null)
  112.                 {
  113.                     throw new ArgumentException("cronExpression 不能为空");
  114.                 }
  115.                 CronExpressionString = CultureInfo.InvariantCulture.TextInfo.ToUpper(cronExpression);
  116.                 BuildExpression(CronExpressionString);
  117.             }
  118.             /// <summary>
  119.             /// 构建表达式
  120.             /// </summary>
  121.             /// <param name="expression"></param>
  122.             /// <exception cref="FormatException"></exception>
  123.             private void BuildExpression(string expression)
  124.             {
  125.                 try
  126.                 {
  127.                     seconds ??= new SortedSet<int>();
  128.                     minutes ??= new SortedSet<int>();
  129.                     hours ??= new SortedSet<int>();
  130.                     daysOfMonth ??= new SortedSet<int>();
  131.                     months ??= new SortedSet<int>();
  132.                     daysOfWeek ??= new SortedSet<int>();
  133.                     years ??= new SortedSet<int>();
  134.                     int exprOn = Second;
  135.                     string[] exprsTok = expression.Split(splitSeparators, StringSplitOptions.RemoveEmptyEntries);
  136.                     foreach (string exprTok in exprsTok)
  137.                     {
  138.                         string expr = exprTok.Trim();
  139.                         if (expr.Length == 0)
  140.                         {
  141.                             continue;
  142.                         }
  143.                         if (exprOn > Year)
  144.                         {
  145.                             break;
  146.                         }
  147.                         if (exprOn == DayOfMonth && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0)
  148.                         {
  149.                             throw new FormatException("不支持在月份的其他日期指定“L”和“LW”");
  150.                         }
  151.                         if (exprOn == DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0)
  152.                         {
  153.                             throw new FormatException("不支持在一周的其他日期指定“L”");
  154.                         }
  155.                         if (exprOn == DayOfWeek && expr.IndexOf('#') != -1 && expr.IndexOf('#', expr.IndexOf('#') + 1) != -1)
  156.                         {
  157.                             throw new FormatException("不支持指定多个“第N”天。");
  158.                         }
  159.                         string[] vTok = expr.Split(commaSeparator);
  160.                         foreach (string v in vTok)
  161.                         {
  162.                             StoreExpressionVals(0, v, exprOn);
  163.                         }
  164.                         exprOn++;
  165.                     }
  166.                     if (exprOn <= DayOfWeek)
  167.                     {
  168.                         throw new FormatException("表达式意料之外的结束。");
  169.                     }
  170.                     if (exprOn <= Year)
  171.                     {
  172.                         StoreExpressionVals(0, "*", Year);
  173.                     }
  174.                     var dow = GetSet(DayOfWeek);
  175.                     var dom = GetSet(DayOfMonth);
  176.                     bool dayOfMSpec = !dom.Contains(NoSpec);
  177.                     bool dayOfWSpec = !dow.Contains(NoSpec);
  178.                     if (dayOfMSpec && !dayOfWSpec)
  179.                     {
  180.                         // skip
  181.                     }
  182.                     else if (dayOfWSpec && !dayOfMSpec)
  183.                     {
  184.                         // skip
  185.                     }
  186.                     else
  187.                     {
  188.                         throw new FormatException("不支持同时指定星期和日参数。");
  189.                     }
  190.                 }
  191.                 catch (FormatException)
  192.                 {
  193.                     throw;
  194.                 }
  195.                 catch (Exception e)
  196.                 {
  197.                     throw new FormatException($"非法的 cron 表达式格式 ({e.Message})", e);
  198.                 }
  199.             }
  200.             /// <summary>
  201.             /// Stores the expression values.
  202.             /// </summary>
  203.             /// <param name="pos">The position.</param>
  204.             /// <param name="s">The string to traverse.</param>
  205.             /// <param name="type">The type of value.</param>
  206.             /// <returns></returns>
  207.             private int StoreExpressionVals(int pos, string s, int type)
  208.             {
  209.                 int incr = 0;
  210.                 int i = SkipWhiteSpace(pos, s);
  211.                 if (i >= s.Length)
  212.                 {
  213.                     return i;
  214.                 }
  215.                 char c = s[i];
  216.                 if (c >= 'A' && c <= 'Z' && !s.Equals("L") && !s.Equals("LW") && !regex.IsMatch(s))
  217.                 {
  218.                     string sub = s.Substring(i, 3);
  219.                     int sval;
  220.                     int eval = -1;
  221.                     if (type == Month)
  222.                     {
  223.                         sval = GetMonthNumber(sub) + 1;
  224.                         if (sval <= 0)
  225.                         {
  226.                             throw new FormatException($"无效的月份值:'{sub}'");
  227.                         }
  228.                         if (s.Length > i + 3)
  229.                         {
  230.                             c = s[i + 3];
  231.                             if (c == '-')
  232.                             {
  233.                                 i += 4;
  234.                                 sub = s.Substring(i, 3);
  235.                                 eval = GetMonthNumber(sub) + 1;
  236.                                 if (eval <= 0)
  237.                                 {
  238.                                     throw new FormatException(
  239.                                         $"无效的月份值: '{sub}'");
  240.                                 }
  241.                             }
  242.                         }
  243.                     }
  244.                     else if (type == DayOfWeek)
  245.                     {
  246.                         sval = GetDayOfWeekNumber(sub);
  247.                         if (sval < 0)
  248.                         {
  249.                             throw new FormatException($"无效的星期几值: '{sub}'");
  250.                         }
  251.                         if (s.Length > i + 3)
  252.                         {
  253.                             c = s[i + 3];
  254.                             if (c == '-')
  255.                             {
  256.                                 i += 4;
  257.                                 sub = s.Substring(i, 3);
  258.                                 eval = GetDayOfWeekNumber(sub);
  259.                                 if (eval < 0)
  260.                                 {
  261.                                     throw new FormatException(
  262.                                         $"无效的星期几值: '{sub}'");
  263.                                 }
  264.                             }
  265.                             else if (c == '#')
  266.                             {
  267.                                 try
  268.                                 {
  269.                                     i += 4;
  270.                                     nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
  271.                                     if (nthdayOfWeek is < 1 or > 5)
  272.                                     {
  273.                                         throw new FormatException("周的第n天小于1或大于5");
  274.                                     }
  275.                                 }
  276.                                 catch (Exception)
  277.                                 {
  278.                                     throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面");
  279.                                 }
  280.                             }
  281.                             else if (c == '/')
  282.                             {
  283.                                 try
  284.                                 {
  285.                                     i += 4;
  286.                                     everyNthWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
  287.                                     if (everyNthWeek is < 1 or > 5)
  288.                                     {
  289.                                         throw new FormatException("每个星期<1或>5");
  290.                                     }
  291.                                 }
  292.                                 catch (Exception)
  293.                                 {
  294.                                     throw new FormatException("1 到 5 之间的数值必须跟在 '/' 选项后面");
  295.                                 }
  296.                             }
  297.                             else if (c == 'L')
  298.                             {
  299.                                 lastdayOfWeek = true;
  300.                                 i++;
  301.                             }
  302.                             else
  303.                             {
  304.                                 throw new FormatException($"此位置的非法字符:'{sub}'");
  305.                             }
  306.                         }
  307.                     }
  308.                     else
  309.                     {
  310.                         throw new FormatException($"此位置的非法字符:'{sub}'");
  311.                     }
  312.                     if (eval != -1)
  313.                     {
  314.                         incr = 1;
  315.                     }
  316.                     AddToSet(sval, eval, incr, type);
  317.                     return i + 3;
  318.                 }
  319.                 if (c == '?')
  320.                 {
  321.                     i++;
  322.                     if (i + 1 < s.Length && s[i] != ' ' && s[i + 1] != '\t')
  323.                     {
  324.                         throw new FormatException("'?' 后的非法字符: " + s[i]);
  325.                     }
  326.                     if (type != DayOfWeek && type != DayOfMonth)
  327.                     {
  328.                         throw new FormatException(
  329.                             "'?' 只能为月日或周日指定。");
  330.                     }
  331.                     if (type == DayOfWeek && !lastdayOfMonth)
  332.                     {
  333.                         int val = daysOfMonth.LastOrDefault();
  334.                         if (val == NoSpecInt)
  335.                         {
  336.                             throw new FormatException(
  337.                                 "'?' 只能为月日或周日指定。");
  338.                         }
  339.                     }
  340.                     AddToSet(NoSpecInt, -1, 0, type);
  341.                     return i;
  342.                 }
  343.                 var startsWithAsterisk = c == '*';
  344.                 if (startsWithAsterisk || c == '/')
  345.                 {
  346.                     if (startsWithAsterisk && i + 1 >= s.Length)
  347.                     {
  348.                         AddToSet(AllSpecInt, -1, incr, type);
  349.                         return i + 1;
  350.                     }
  351.                     if (c == '/' && (i + 1 >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t'))
  352.                     {
  353.                         throw new FormatException("'/' 后面必须跟一个整数。");
  354.                     }
  355.                     if (startsWithAsterisk)
  356.                     {
  357.                         i++;
  358.                     }
  359.                     c = s[i];
  360.                     if (c == '/')
  361.                     {
  362.                         // is an increment specified?
  363.                         i++;
  364.                         if (i >= s.Length)
  365.                         {
  366.                             throw new FormatException("字符串意外结束。");
  367.                         }
  368.                         incr = GetNumericValue(s, i);
  369.                         i++;
  370.                         if (incr > 10)
  371.                         {
  372.                             i++;
  373.                         }
  374.                         CheckIncrementRange(incr, type);
  375.                     }
  376.                     else
  377.                     {
  378.                         if (startsWithAsterisk)
  379.                         {
  380.                             throw new FormatException("星号后的非法字符:" + s);
  381.                         }
  382.                         incr = 1;
  383.                     }
  384.                     AddToSet(AllSpecInt, -1, incr, type);
  385.                     return i;
  386.                 }
  387.                 if (c == 'L')
  388.                 {
  389.                     i++;
  390.                     if (type == DayOfMonth)
  391.                     {
  392.                         lastdayOfMonth = true;
  393.                     }
  394.                     if (type == DayOfWeek)
  395.                     {
  396.                         AddToSet(7, 7, 0, type);
  397.                     }
  398.                     if (type == DayOfMonth && s.Length > i)
  399.                     {
  400.                         c = s[i];
  401.                         if (c == '-')
  402.                         {
  403.                             ValueSet vs = GetValue(0, s, i + 1);
  404.                             lastdayOffset = vs.theValue;
  405.                             if (lastdayOffset > 30)
  406.                             {
  407.                                 throw new FormatException("与最后一天的偏移量必须 <= 30");
  408.                             }
  409.                             i = vs.pos;
  410.                         }
  411.                         if (s.Length > i)
  412.                         {
  413.                             c = s[i];
  414.                             if (c == 'W')
  415.                             {
  416.                                 nearestWeekday = true;
  417.                                 i++;
  418.                             }
  419.                         }
  420.                     }
  421.                     return i;
  422.                 }
  423.                 if (c >= '0' && c <= '9')
  424.                 {
  425.                     int val = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  426.                     i++;
  427.                     if (i >= s.Length)
  428.                     {
  429.                         AddToSet(val, -1, -1, type);
  430.                     }
  431.                     else
  432.                     {
  433.                         c = s[i];
  434.                         if (c >= '0' && c <= '9')
  435.                         {
  436.                             ValueSet vs = GetValue(val, s, i);
  437.                             val = vs.theValue;
  438.                             i = vs.pos;
  439.                         }
  440.                         i = CheckNext(i, s, val, type);
  441.                         return i;
  442.                     }
  443.                 }
  444.                 else
  445.                 {
  446.                     throw new FormatException($"意外字符:{c}");
  447.                 }
  448.                 return i;
  449.             }
  450.             // ReSharper disable once UnusedParameter.Local
  451.             private static void CheckIncrementRange(int incr, int type)
  452.             {
  453.                 if (incr > 59 && (type == Second || type == Minute))
  454.                 {
  455.                     throw new FormatException($"增量 > 60 : {incr}");
  456.                 }
  457.                 if (incr > 23 && type == Hour)
  458.                 {
  459.                     throw new FormatException($"增量 > 24 : {incr}");
  460.                 }
  461.                 if (incr > 31 && type == DayOfMonth)
  462.                 {
  463.                     throw new FormatException($"增量 > 31 : {incr}");
  464.                 }
  465.                 if (incr > 7 && type == DayOfWeek)
  466.                 {
  467.                     throw new FormatException($"增量 > 7 : {incr}");
  468.                 }
  469.                 if (incr > 12 && type == Month)
  470.                 {
  471.                     throw new FormatException($"增量 > 12 : {incr}");
  472.                 }
  473.             }
  474.             /// <summary>
  475.             /// Checks the next value.
  476.             /// </summary>
  477.             /// <param name="pos">The position.</param>
  478.             /// <param name="s">The string to check.</param>
  479.             /// <param name="val">The value.</param>
  480.             /// <param name="type">The type to search.</param>
  481.             /// <returns></returns>
  482.             private int CheckNext(int pos, string s, int val, int type)
  483.             {
  484.                 int end = -1;
  485.                 int i = pos;
  486.                 if (i >= s.Length)
  487.                 {
  488.                     AddToSet(val, end, -1, type);
  489.                     return i;
  490.                 }
  491.                 char c = s[pos];
  492.                 if (c == 'L')
  493.                 {
  494.                     if (type == DayOfWeek)
  495.                     {
  496.                         if (val < 1 || val > 7)
  497.                         {
  498.                             throw new FormatException("星期日值必须介于1和7之间");
  499.                         }
  500.                         lastdayOfWeek = true;
  501.                     }
  502.                     else
  503.                     {
  504.                         throw new FormatException($"'L' 选项在这里无效。(位置={i})");
  505.                     }
  506.                     var data = GetSet(type);
  507.                     data.Add(val);
  508.                     i++;
  509.                     return i;
  510.                 }
  511.                 if (c == 'W')
  512.                 {
  513.                     if (type == DayOfMonth)
  514.                     {
  515.                         nearestWeekday = true;
  516.                     }
  517.                     else
  518.                     {
  519.                         throw new FormatException($"'W' 选项在这里无效。 (位置={i})");
  520.                     }
  521.                     if (val > 31)
  522.                     {
  523.                         throw new FormatException("'W' 选项对于大于 31 的值(一个月中的最大天数)没有意义");
  524.                     }
  525.                     var data = GetSet(type);
  526.                     data.Add(val);
  527.                     i++;
  528.                     return i;
  529.                 }
  530.                 if (c == '#')
  531.                 {
  532.                     if (type != DayOfWeek)
  533.                     {
  534.                         throw new FormatException($"'#' 选项在这里无效。 (位置={i})");
  535.                     }
  536.                     i++;
  537.                     try
  538.                     {
  539.                         nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture);
  540.                         if (nthdayOfWeek is < 1 or > 5)
  541.                         {
  542.                             throw new FormatException("周的第n天小于1或大于5");
  543.                         }
  544.                     }
  545.                     catch (Exception)
  546.                     {
  547.                         throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面");
  548.                     }
  549.                     var data = GetSet(type);
  550.                     data.Add(val);
  551.                     i++;
  552.                     return i;
  553.                 }
  554.                 if (c == 'C')
  555.                 {
  556.                     if (type == DayOfWeek)
  557.                     {
  558.                     }
  559.                     else if (type == DayOfMonth)
  560.                     {
  561.                     }
  562.                     else
  563.                     {
  564.                         throw new FormatException($"'C' 选项在这里无效。(位置={i})");
  565.                     }
  566.                     var data = GetSet(type);
  567.                     data.Add(val);
  568.                     i++;
  569.                     return i;
  570.                 }
  571.                 if (c == '-')
  572.                 {
  573.                     i++;
  574.                     c = s[i];
  575.                     int v = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  576.                     end = v;
  577.                     i++;
  578.                     if (i >= s.Length)
  579.                     {
  580.                         AddToSet(val, end, 1, type);
  581.                         return i;
  582.                     }
  583.                     c = s[i];
  584.                     if (c >= '0' && c <= '9')
  585.                     {
  586.                         ValueSet vs = GetValue(v, s, i);
  587.                         int v1 = vs.theValue;
  588.                         end = v1;
  589.                         i = vs.pos;
  590.                     }
  591.                     if (i < s.Length && s[i] == '/')
  592.                     {
  593.                         i++;
  594.                         c = s[i];
  595.                         int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  596.                         i++;
  597.                         if (i >= s.Length)
  598.                         {
  599.                             AddToSet(val, end, v2, type);
  600.                             return i;
  601.                         }
  602.                         c = s[i];
  603.                         if (c >= '0' && c <= '9')
  604.                         {
  605.                             ValueSet vs = GetValue(v2, s, i);
  606.                             int v3 = vs.theValue;
  607.                             AddToSet(val, end, v3, type);
  608.                             i = vs.pos;
  609.                             return i;
  610.                         }
  611.                         AddToSet(val, end, v2, type);
  612.                         return i;
  613.                     }
  614.                     AddToSet(val, end, 1, type);
  615.                     return i;
  616.                 }
  617.                 if (c == '/')
  618.                 {
  619.                     if (i + 1 >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t')
  620.                     {
  621.                         throw new FormatException("\'/\' 后面必须跟一个整数。");
  622.                     }
  623.                     i++;
  624.                     c = s[i];
  625.                     int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture);
  626.                     i++;
  627.                     if (i >= s.Length)
  628.                     {
  629.                         CheckIncrementRange(v2, type);
  630.                         AddToSet(val, end, v2, type);
  631.                         return i;
  632.                     }
  633.                     c = s[i];
  634.                     if (c >= '0' && c <= '9')
  635.                     {
  636.                         ValueSet vs = GetValue(v2, s, i);
  637.                         int v3 = vs.theValue;
  638.                         CheckIncrementRange(v3, type);
  639.                         AddToSet(val, end, v3, type);
  640.                         i = vs.pos;
  641.                         return i;
  642.                     }
  643.                     throw new FormatException($"意外的字符 '{c}' 后 '/'");
  644.                 }
  645.                 AddToSet(val, end, 0, type);
  646.                 i++;
  647.                 return i;
  648.             }
  649.             /// <summary>
  650.             /// Gets the cron expression string.
  651.             /// </summary>
  652.             /// <value>The cron expression string.</value>
  653.             private static string CronExpressionString;
  654.             /// <summary>
  655.             /// Skips the white space.
  656.             /// </summary>
  657.             /// <param name="i">The i.</param>
  658.             /// <param name="s">The s.</param>
  659.             /// <returns></returns>
  660.             private static int SkipWhiteSpace(int i, string s)
  661.             {
  662.                 for (; i < s.Length && (s[i] == ' ' || s[i] == '\t'); i++)
  663.                 {
  664.                 }
  665.                 return i;
  666.             }
  667.             /// <summary>
  668.             /// Finds the next white space.
  669.             /// </summary>
  670.             /// <param name="i">The i.</param>
  671.             /// <param name="s">The s.</param>
  672.             /// <returns></returns>
  673.             private static int FindNextWhiteSpace(int i, string s)
  674.             {
  675.                 for (; i < s.Length && (s[i] != ' ' || s[i] != '\t'); i++)
  676.                 {
  677.                 }
  678.                 return i;
  679.             }
  680.             /// <summary>
  681.             /// Adds to set.
  682.             /// </summary>
  683.             /// <param name="val">The val.</param>
  684.             /// <param name="end">The end.</param>
  685.             /// <param name="incr">The incr.</param>
  686.             /// <param name="type">The type.</param>
  687.             private void AddToSet(int val, int end, int incr, int type)
  688.             {
  689.                 var data = GetSet(type);
  690.                 if (type == Second || type == Minute)
  691.                 {
  692.                     if ((val < 0 || val > 59 || end > 59) && val != AllSpecInt)
  693.                     {
  694.                         throw new FormatException("分钟和秒值必须介于0和59之间");
  695.                     }
  696.                 }
  697.                 else if (type == Hour)
  698.                 {
  699.                     if ((val < 0 || val > 23 || end > 23) && val != AllSpecInt)
  700.                     {
  701.                         throw new FormatException("小时值必须介于0和23之间");
  702.                     }
  703.                 }
  704.                 else if (type == DayOfMonth)
  705.                 {
  706.                     if ((val < 1 || val > 31 || end > 31) && val != AllSpecInt
  707.                         && val != NoSpecInt)
  708.                     {
  709.                         throw new FormatException("月日值必须介于1和31之间");
  710.                     }
  711.                 }
  712.                 else if (type == Month)
  713.                 {
  714.                     if ((val < 1 || val > 12 || end > 12) && val != AllSpecInt)
  715.                     {
  716.                         throw new FormatException("月份值必须介于1和12之间");
  717.                     }
  718.                 }
  719.                 else if (type == DayOfWeek)
  720.                 {
  721.                     if ((val == 0 || val > 7 || end > 7) && val != AllSpecInt
  722.                         && val != NoSpecInt)
  723.                     {
  724.                         throw new FormatException("星期日值必须介于1和7之间");
  725.                     }
  726.                 }
  727.                 if ((incr == 0 || incr == -1) && val != AllSpecInt)
  728.                 {
  729.                     if (val != -1)
  730.                     {
  731.                         data.Add(val);
  732.                     }
  733.                     else
  734.                     {
  735.                         data.Add(NoSpec);
  736.                     }
  737.                     return;
  738.                 }
  739.                 int startAt = val;
  740.                 int stopAt = end;
  741.                 if (val == AllSpecInt && incr <= 0)
  742.                 {
  743.                     incr = 1;
  744.                     data.Add(AllSpec);
  745.                 }
  746.                 if (type == Second || type == Minute)
  747.                 {
  748.                     if (stopAt == -1)
  749.                     {
  750.                         stopAt = 59;
  751.                     }
  752.                     if (startAt == -1 || startAt == AllSpecInt)
  753.                     {
  754.                         startAt = 0;
  755.                     }
  756.                 }
  757.                 else if (type == Hour)
  758.                 {
  759.                     if (stopAt == -1)
  760.                     {
  761.                         stopAt = 23;
  762.                     }
  763.                     if (startAt == -1 || startAt == AllSpecInt)
  764.                     {
  765.                         startAt = 0;
  766.                     }
  767.                 }
  768.                 else if (type == DayOfMonth)
  769.                 {
  770.                     if (stopAt == -1)
  771.                     {
  772.                         stopAt = 31;
  773.                     }
  774.                     if (startAt == -1 || startAt == AllSpecInt)
  775.                     {
  776.                         startAt = 1;
  777.                     }
  778.                 }
  779.                 else if (type == Month)
  780.                 {
  781.                     if (stopAt == -1)
  782.                     {
  783.                         stopAt = 12;
  784.                     }
  785.                     if (startAt == -1 || startAt == AllSpecInt)
  786.                     {
  787.                         startAt = 1;
  788.                     }
  789.                 }
  790.                 else if (type == DayOfWeek)
  791.                 {
  792.                     if (stopAt == -1)
  793.                     {
  794.                         stopAt = 7;
  795.                     }
  796.                     if (startAt == -1 || startAt == AllSpecInt)
  797.                     {
  798.                         startAt = 1;
  799.                     }
  800.                 }
  801.                 else if (type == Year)
  802.                 {
  803.                     if (stopAt == -1)
  804.                     {
  805.                         stopAt = MaxYear;
  806.                     }
  807.                     if (startAt == -1 || startAt == AllSpecInt)
  808.                     {
  809.                         startAt = 1970;
  810.                     }
  811.                 }
  812.                 int max = -1;
  813.                 if (stopAt < startAt)
  814.                 {
  815.                     switch (type)
  816.                     {
  817.                         case Second:
  818.                             max = 60;
  819.                             break;
  820.                         case Minute:
  821.                             max = 60;
  822.                             break;
  823.                         case Hour:
  824.                             max = 24;
  825.                             break;
  826.                         case Month:
  827.                             max = 12;
  828.                             break;
  829.                         case DayOfWeek:
  830.                             max = 7;
  831.                             break;
  832.                         case DayOfMonth:
  833.                             max = 31;
  834.                             break;
  835.                         case Year:
  836.                             throw new ArgumentException("开始年份必须小于停止年份");
  837.                         default:
  838.                             throw new ArgumentException("遇到意外的类型");
  839.                     }
  840.                     stopAt += max;
  841.                 }
  842.                 for (int i = startAt; i <= stopAt; i += incr)
  843.                 {
  844.                     if (max == -1)
  845.                     {
  846.                         data.Add(i);
  847.                     }
  848.                     else
  849.                     {
  850.                         int i2 = i % max;
  851.                         if (i2 == 0 && (type == Month || type == DayOfWeek || type == DayOfMonth))
  852.                         {
  853.                             i2 = max;
  854.                         }
  855.                         data.Add(i2);
  856.                     }
  857.                 }
  858.             }
  859.             /// <summary>
  860.             /// Gets the set of given type.
  861.             /// </summary>
  862.             /// <param name="type">The type of set to get.</param>
  863.             /// <returns></returns>
  864.             private SortedSet<int> GetSet(int type)
  865.             {
  866.                 switch (type)
  867.                 {
  868.                     case Second:
  869.                         return seconds;
  870.                     case Minute:
  871.                         return minutes;
  872.                     case Hour:
  873.                         return hours;
  874.                     case DayOfMonth:
  875.                         return daysOfMonth;
  876.                     case Month:
  877.                         return months;
  878.                     case DayOfWeek:
  879.                         return daysOfWeek;
  880.                     case Year:
  881.                         return years;
  882.                     default:
  883.                         throw new ArgumentOutOfRangeException();
  884.                 }
  885.             }
  886.             /// <summary>
  887.             /// Gets the value.
  888.             /// </summary>
  889.             /// <param name="v">The v.</param>
  890.             /// <param name="s">The s.</param>
  891.             /// <param name="i">The i.</param>
  892.             /// <returns></returns>
  893.             private static ValueSet GetValue(int v, string s, int i)
  894.             {
  895.                 char c = s[i];
  896.                 StringBuilder s1 = new StringBuilder(v.ToString(CultureInfo.InvariantCulture));
  897.                 while (c >= '0' && c <= '9')
  898.                 {
  899.                     s1.Append(c);
  900.                     i++;
  901.                     if (i >= s.Length)
  902.                     {
  903.                         break;
  904.                     }
  905.                     c = s[i];
  906.                 }
  907.                 ValueSet val = new ValueSet();
  908.                 if (i < s.Length)
  909.                 {
  910.                     val.pos = i;
  911.                 }
  912.                 else
  913.                 {
  914.                     val.pos = i + 1;
  915.                 }
  916.                 val.theValue = Convert.ToInt32(s1.ToString(), CultureInfo.InvariantCulture);
  917.                 return val;
  918.             }
  919.             /// <summary>
  920.             /// Gets the numeric value from string.
  921.             /// </summary>
  922.             /// <param name="s">The string to parse from.</param>
  923.             /// <param name="i">The i.</param>
  924.             /// <returns></returns>
  925.             private static int GetNumericValue(string s, int i)
  926.             {
  927.                 int endOfVal = FindNextWhiteSpace(i, s);
  928.                 string val = s.Substring(i, endOfVal - i);
  929.                 return Convert.ToInt32(val, CultureInfo.InvariantCulture);
  930.             }
  931.             /// <summary>
  932.             /// Gets the month number.
  933.             /// </summary>
  934.             /// <param name="s">The string to map with.</param>
  935.             /// <returns></returns>
  936.             private static int GetMonthNumber(string s)
  937.             {
  938.                 if (monthMap.ContainsKey(s))
  939.                 {
  940.                     return monthMap[s];
  941.                 }
  942.                 return -1;
  943.             }
  944.             /// <summary>
  945.             /// Gets the day of week number.
  946.             /// </summary>
  947.             /// <param name="s">The s.</param>
  948.             /// <returns></returns>
  949.             private static int GetDayOfWeekNumber(string s)
  950.             {
  951.                 if (dayMap.ContainsKey(s))
  952.                 {
  953.                     return dayMap[s];
  954.                 }
  955.                 return -1;
  956.             }
  957.             /// <summary>
  958.             /// 在给定时间之后获取下一个触发时间。
  959.             /// </summary>
  960.             /// <param name="afterTimeUtc">开始搜索的 UTC 时间。</param>
  961.             /// <returns></returns>
  962.             public DateTimeOffset? GetTimeAfter(DateTimeOffset afterTimeUtc)
  963.             {
  964.                 // 向前移动一秒钟,因为我们正在计算时间*之后*
  965.                 afterTimeUtc = afterTimeUtc.AddSeconds(1);
  966.                 // CronTrigger 不处理毫秒
  967.                 DateTimeOffset d = CreateDateTimeWithoutMillis(afterTimeUtc);
  968.                 // 更改为指定时区
  969.                 d = TimeZoneInfo.ConvertTime(d, timeZoneInfo);
  970.                 bool gotOne = false;
  971.                 //循环直到我们计算出下一次,或者我们已经过了 endTime
  972.                 while (!gotOne)
  973.                 {
  974.                     SortedSet<int> st;
  975.                     int t;
  976.                     int sec = d.Second;
  977.                     st = seconds.GetViewBetween(sec, 9999999);
  978.                     if (st.Count > 0)
  979.                     {
  980.                         sec = st.First();
  981.                     }
  982.                     else
  983.                     {
  984.                         sec = seconds.First();
  985.                         d = d.AddMinutes(1);
  986.                     }
  987.                     d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, d.Minute, sec, d.Millisecond, d.Offset);
  988.                     int min = d.Minute;
  989.                     int hr = d.Hour;
  990.                     t = -1;
  991.                     st = minutes.GetViewBetween(min, 9999999);
  992.                     if (st.Count > 0)
  993.                     {
  994.                         t = min;
  995.                         min = st.First();
  996.                     }
  997.                     else
  998.                     {
  999.                         min = minutes.First();
  1000.                         hr++;
  1001.                     }
  1002.                     if (min != t)
  1003.                     {
  1004.                         d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, 0, d.Millisecond, d.Offset);
  1005.                         d = SetCalendarHour(d, hr);
  1006.                         continue;
  1007.                     }
  1008.                     d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, d.Second, d.Millisecond, d.Offset);
  1009.                     hr = d.Hour;
  1010.                     int day = d.Day;
  1011.                     t = -1;
  1012.                     st = hours.GetViewBetween(hr, 9999999);
  1013.                     if (st.Count > 0)
  1014.                     {
  1015.                         t = hr;
  1016.                         hr = st.First();
  1017.                     }
  1018.                     else
  1019.                     {
  1020.                         hr = hours.First();
  1021.                         day++;
  1022.                     }
  1023.                     if (hr != t)
  1024.                     {
  1025.                         int daysInMonth = DateTime.DaysInMonth(d.Year, d.Month);
  1026.                         if (day > daysInMonth)
  1027.                         {
  1028.                             d = new DateTimeOffset(d.Year, d.Month, daysInMonth, d.Hour, 0, 0, d.Millisecond, d.Offset).AddDays(day - daysInMonth);
  1029.                         }
  1030.                         else
  1031.                         {
  1032.                             d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, 0, 0, d.Millisecond, d.Offset);
  1033.                         }
  1034.                         d = SetCalendarHour(d, hr);
  1035.                         continue;
  1036.                     }
  1037.                     d = new DateTimeOffset(d.Year, d.Month, d.Day, hr, d.Minute, d.Second, d.Millisecond, d.Offset);
  1038.                     day = d.Day;
  1039.                     int mon = d.Month;
  1040.                     t = -1;
  1041.                     int tmon = mon;
  1042.                     bool dayOfMSpec = !daysOfMonth.Contains(NoSpec);
  1043.                     bool dayOfWSpec = !daysOfWeek.Contains(NoSpec);
  1044.                     if (dayOfMSpec && !dayOfWSpec)
  1045.                     {
  1046.                         // 逐月获取规则
  1047.                         st = daysOfMonth.GetViewBetween(day, 9999999);
  1048.                         bool found = st.Any();
  1049.                         if (lastdayOfMonth)
  1050.                         {
  1051.                             if (!nearestWeekday)
  1052.                             {
  1053.                                 t = day;
  1054.                                 day = GetLastDayOfMonth(mon, d.Year);
  1055.                                 day -= lastdayOffset;
  1056.                                 if (t > day)
  1057.                                 {
  1058.                                     mon++;
  1059.                                     if (mon > 12)
  1060.                                     {
  1061.                                         mon = 1;
  1062.                                         tmon = 3333; // 确保下面的 mon != tmon 测试失败
  1063.                                         d = d.AddYears(1);
  1064.                                     }
  1065.                                     day = 1;
  1066.                                 }
  1067.                             }
  1068.                             else
  1069.                             {
  1070.                                 t = day;
  1071.                                 day = GetLastDayOfMonth(mon, d.Year);
  1072.                                 day -= lastdayOffset;
  1073.                                 DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1074.                                 int ldom = GetLastDayOfMonth(mon, d.Year);
  1075.                                 DayOfWeek dow = tcal.DayOfWeek;
  1076.                                 if (dow == System.DayOfWeek.Saturday && day == 1)
  1077.                                 {
  1078.                                     day += 2;
  1079.                                 }
  1080.                                 else if (dow == System.DayOfWeek.Saturday)
  1081.                                 {
  1082.                                     day -= 1;
  1083.                                 }
  1084.                                 else if (dow == System.DayOfWeek.Sunday && day == ldom)
  1085.                                 {
  1086.                                     day -= 2;
  1087.                                 }
  1088.                                 else if (dow == System.DayOfWeek.Sunday)
  1089.                                 {
  1090.                                     day += 1;
  1091.                                 }
  1092.                                 DateTimeOffset nTime = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Millisecond, d.Offset);
  1093.                                 if (nTime.ToUniversalTime() < afterTimeUtc)
  1094.                                 {
  1095.                                     day = 1;
  1096.                                     mon++;
  1097.                                 }
  1098.                             }
  1099.                         }
  1100.                         else if (nearestWeekday)
  1101.                         {
  1102.                             t = day;
  1103.                             day = daysOfMonth.First();
  1104.                             DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1105.                             int ldom = GetLastDayOfMonth(mon, d.Year);
  1106.                             DayOfWeek dow = tcal.DayOfWeek;
  1107.                             if (dow == System.DayOfWeek.Saturday && day == 1)
  1108.                             {
  1109.                                 day += 2;
  1110.                             }
  1111.                             else if (dow == System.DayOfWeek.Saturday)
  1112.                             {
  1113.                                 day -= 1;
  1114.                             }
  1115.                             else if (dow == System.DayOfWeek.Sunday && day == ldom)
  1116.                             {
  1117.                                 day -= 2;
  1118.                             }
  1119.                             else if (dow == System.DayOfWeek.Sunday)
  1120.                             {
  1121.                                 day += 1;
  1122.                             }
  1123.                             tcal = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Offset);
  1124.                             if (tcal.ToUniversalTime() < afterTimeUtc)
  1125.                             {
  1126.                                 day = daysOfMonth.First();
  1127.                                 mon++;
  1128.                             }
  1129.                         }
  1130.                         else if (found)
  1131.                         {
  1132.                             t = day;
  1133.                             day = st.First();
  1134.                             //确保我们不会在短时间内跑得过快,比如二月
  1135.                             int lastDay = GetLastDayOfMonth(mon, d.Year);
  1136.                             if (day > lastDay)
  1137.                             {
  1138.                                 day = daysOfMonth.First();
  1139.                                 mon++;
  1140.                             }
  1141.                         }
  1142.                         else
  1143.                         {
  1144.                             day = daysOfMonth.First();
  1145.                             mon++;
  1146.                         }
  1147.                         if (day != t || mon != tmon)
  1148.                         {
  1149.                             if (mon > 12)
  1150.                             {
  1151.                                 d = new DateTimeOffset(d.Year, 12, day, 0, 0, 0, d.Offset).AddMonths(mon - 12);
  1152.                             }
  1153.                             else
  1154.                             {
  1155.                                 //这是为了避免从一个月移动时出现错误
  1156.                                 //有 30 或 31 天到一个月更少。 导致实例化无效的日期时间。
  1157.                                 int lDay = DateTime.DaysInMonth(d.Year, mon);
  1158.                                 if (day <= lDay)
  1159.                                 {
  1160.                                     d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1161.                                 }
  1162.                                 else
  1163.                                 {
  1164.                                     d = new DateTimeOffset(d.Year, mon, lDay, 0, 0, 0, d.Offset).AddDays(day - lDay);
  1165.                                 }
  1166.                             }
  1167.                             continue;
  1168.                         }
  1169.                     }
  1170.                     else if (dayOfWSpec && !dayOfMSpec)
  1171.                     {
  1172.                         // 获取星期几规则
  1173.                         if (lastdayOfWeek)
  1174.                         {
  1175.                             int dow = daysOfWeek.First();
  1176.                             int cDow = (int)d.DayOfWeek + 1;
  1177.                             int daysToAdd = 0;
  1178.                             if (cDow < dow)
  1179.                             {
  1180.                                 daysToAdd = dow - cDow;
  1181.                             }
  1182.                             if (cDow > dow)
  1183.                             {
  1184.                                 daysToAdd = dow + (7 - cDow);
  1185.                             }
  1186.                             int lDay = GetLastDayOfMonth(mon, d.Year);
  1187.                             if (day + daysToAdd > lDay)
  1188.                             {
  1189.                                 if (mon == 12)
  1190.                                 {
  1191.                                     d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);
  1192.                                 }
  1193.                                 else
  1194.                                 {
  1195.                                     d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);
  1196.                                 }
  1197.                                 continue;
  1198.                             }
  1199.                             // 查找本月这一天最后一次出现的日期...
  1200.                             while (day + daysToAdd + 7 <= lDay)
  1201.                             {
  1202.                                 daysToAdd += 7;
  1203.                             }
  1204.                             day += daysToAdd;
  1205.                             if (daysToAdd > 0)
  1206.                             {
  1207.                                 d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1208.                                 continue;
  1209.                             }
  1210.                         }
  1211.                         else if (nthdayOfWeek != 0)
  1212.                         {
  1213.                             int dow = daysOfWeek.First();
  1214.                             int cDow = (int)d.DayOfWeek + 1;
  1215.                             int daysToAdd = 0;
  1216.                             if (cDow < dow)
  1217.                             {
  1218.                                 daysToAdd = dow - cDow;
  1219.                             }
  1220.                             else if (cDow > dow)
  1221.                             {
  1222.                                 daysToAdd = dow + (7 - cDow);
  1223.                             }
  1224.                             bool dayShifted = daysToAdd > 0;
  1225.                             day += daysToAdd;
  1226.                             int weekOfMonth = day / 7;
  1227.                             if (day % 7 > 0)
  1228.                             {
  1229.                                 weekOfMonth++;
  1230.                             }
  1231.                             daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
  1232.                             day += daysToAdd;
  1233.                             if (daysToAdd < 0 || day > GetLastDayOfMonth(mon, d.Year))
  1234.                             {
  1235.                                 if (mon == 12)
  1236.                                 {
  1237.                                     d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);
  1238.                                 }
  1239.                                 else
  1240.                                 {
  1241.                                     d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);
  1242.                                 }
  1243.                                 continue;
  1244.                             }
  1245.                             if (daysToAdd > 0 || dayShifted)
  1246.                             {
  1247.                                 d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1248.                                 continue;
  1249.                             }
  1250.                         }
  1251.                         else if (everyNthWeek != 0)
  1252.                         {
  1253.                             int cDow = (int)d.DayOfWeek + 1;
  1254.                             int dow = daysOfWeek.First();
  1255.                             st = daysOfWeek.GetViewBetween(cDow, 9999999);
  1256.                             if (st.Count > 0)
  1257.                             {
  1258.                                 dow = st.First();
  1259.                             }
  1260.                             int daysToAdd = 0;
  1261.                             if (cDow < dow)
  1262.                             {
  1263.                                 daysToAdd = (dow - cDow) + (7 * (everyNthWeek - 1));
  1264.                             }
  1265.                             if (cDow > dow)
  1266.                             {
  1267.                                 daysToAdd = (dow + (7 - cDow)) + (7 * (everyNthWeek - 1));
  1268.                             }
  1269.                             if (daysToAdd > 0)
  1270.                             {
  1271.                                 d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset);
  1272.                                 d = d.AddDays(daysToAdd);
  1273.                                 continue;
  1274.                             }
  1275.                         }
  1276.                         else
  1277.                         {
  1278.                             int cDow = (int)d.DayOfWeek + 1;
  1279.                             int dow = daysOfWeek.First();
  1280.                             st = daysOfWeek.GetViewBetween(cDow, 9999999);
  1281.                             if (st.Count > 0)
  1282.                             {
  1283.                                 dow = st.First();
  1284.                             }
  1285.                             int daysToAdd = 0;
  1286.                             if (cDow < dow)
  1287.                             {
  1288.                                 daysToAdd = dow - cDow;
  1289.                             }
  1290.                             if (cDow > dow)
  1291.                             {
  1292.                                 daysToAdd = dow + (7 - cDow);
  1293.                             }
  1294.                             int lDay = GetLastDayOfMonth(mon, d.Year);
  1295.                             if (day + daysToAdd > lDay)
  1296.                             {
  1297.                                 if (mon == 12)
  1298.                                 {
  1299.                                     d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1);
  1300.                                 }
  1301.                                 else
  1302.                                 {
  1303.                                     d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset);
  1304.                                 }
  1305.                                 continue;
  1306.                             }
  1307.                             if (daysToAdd > 0)
  1308.                             {
  1309.                                 d = new DateTimeOffset(d.Year, mon, day + daysToAdd, 0, 0, 0, d.Offset);
  1310.                                 continue;
  1311.                             }
  1312.                         }
  1313.                     }
  1314.                     else
  1315.                     {
  1316.                         throw new FormatException("不支持同时指定星期日和月日参数。");
  1317.                     }
  1318.                     d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, d.Minute, d.Second, d.Offset);
  1319.                     mon = d.Month;
  1320.                     int year = d.Year;
  1321.                     t = -1;
  1322.                     if (year > MaxYear)
  1323.                     {
  1324.                         return null;
  1325.                     }
  1326.                     st = months.GetViewBetween(mon, 9999999);
  1327.                     if (st.Count > 0)
  1328.                     {
  1329.                         t = mon;
  1330.                         mon = st.First();
  1331.                     }
  1332.                     else
  1333.                     {
  1334.                         mon = months.First();
  1335.                         year++;
  1336.                     }
  1337.                     if (mon != t)
  1338.                     {
  1339.                         d = new DateTimeOffset(year, mon, 1, 0, 0, 0, d.Offset);
  1340.                         continue;
  1341.                     }
  1342.                     d = new DateTimeOffset(d.Year, mon, d.Day, d.Hour, d.Minute, d.Second, d.Offset);
  1343.                     year = d.Year;
  1344.                     t = -1;
  1345.                     st = years.GetViewBetween(year, 9999999);
  1346.                     if (st.Count > 0)
  1347.                     {
  1348.                         t = year;
  1349.                         year = st.First();
  1350.                     }
  1351.                     else
  1352.                     {
  1353.                         return null;
  1354.                     }
  1355.                     if (year != t)
  1356.                     {
  1357.                         d = new DateTimeOffset(year, 1, 1, 0, 0, 0, d.Offset);
  1358.                         continue;
  1359.                     }
  1360.                     d = new DateTimeOffset(year, d.Month, d.Day, d.Hour, d.Minute, d.Second, d.Offset);
  1361.                     //为此日期应用适当的偏移量
  1362.                     d = new DateTimeOffset(d.DateTime, timeZoneInfo.BaseUtcOffset);
  1363.                     gotOne = true;
  1364.                 }
  1365.                 return d.ToUniversalTime();
  1366.             }
  1367.             /// <summary>
  1368.             /// Creates the date time without milliseconds.
  1369.             /// </summary>
  1370.             /// <param name="time">The time.</param>
  1371.             /// <returns></returns>
  1372.             private static DateTimeOffset CreateDateTimeWithoutMillis(DateTimeOffset time)
  1373.             {
  1374.                 return new DateTimeOffset(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Offset);
  1375.             }
  1376.             /// <summary>
  1377.             /// Advance the calendar to the particular hour paying particular attention
  1378.             /// to daylight saving problems.
  1379.             /// </summary>
  1380.             /// <param name="date">The date.</param>
  1381.             /// <param name="hour">The hour.</param>
  1382.             /// <returns></returns>
  1383.             private static DateTimeOffset SetCalendarHour(DateTimeOffset date, int hour)
  1384.             {
  1385.                 int hourToSet = hour;
  1386.                 if (hourToSet == 24)
  1387.                 {
  1388.                     hourToSet = 0;
  1389.                 }
  1390.                 DateTimeOffset d = new DateTimeOffset(date.Year, date.Month, date.Day, hourToSet, date.Minute, date.Second, date.Millisecond, date.Offset);
  1391.                 if (hour == 24)
  1392.                 {
  1393.                     d = d.AddDays(1);
  1394.                 }
  1395.                 return d;
  1396.             }
  1397.             /// <summary>
  1398.             /// Gets the last day of month.
  1399.             /// </summary>
  1400.             /// <param name="monthNum">The month num.</param>
  1401.             /// <param name="year">The year.</param>
  1402.             /// <returns></returns>
  1403.             private static int GetLastDayOfMonth(int monthNum, int year)
  1404.             {
  1405.                 return DateTime.DaysInMonth(year, monthNum);
  1406.             }
  1407.             private class ValueSet
  1408.             {
  1409.                 public int theValue;
  1410.                 public int pos;
  1411.             }
  1412.         }
  1413.     }
  1414. }
复制代码
 
CronHelper 中 CronExpression 的函数计算逻辑是从 Quart.NET 借鉴的,支持标准的 7位 cron 表达式,在需要生成Cron 表达式时可以直接使用网络上的各种 Cron 表达式在线生成
CronHelper 里面我们主要用到的功能就是 通过 Cron 表达式,解析下一次的执行时间。
 
服务运行这块我们采用微软的 BackgroundService 后台服务,这里还要用到一个后台服务批量注入的逻辑 关于后台逻辑批量注入可以看我之前写的一篇博客,这里就不展开介绍了
.NET 使用自带 DI 批量注入服务(Service)和 后台服务(BackgroundService) https://www.cnblogs.com/berkerdong/p/16496232.html
 
接下来看一下我这里写的一个DemoTask,代码如下:
  1. using DistributedLock;
  2. using Repository.Database;
  3. using TaskService.Libraries;
  4. namespace TaskService.Tasks
  5. {
  6.     public class DemoTask : BackgroundService
  7.     {
  8.         private readonly IServiceProvider serviceProvider;
  9.         private readonly ILogger logger;
  10.         public DemoTask(IServiceProvider serviceProvider, ILogger<DemoTask> logger)
  11.         {
  12.             this.serviceProvider = serviceProvider;
  13.             this.logger = logger;
  14.         }
  15.         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  16.         {
  17.             CronSchedule.BatchBuilder(stoppingToken, this);
  18.             await Task.Delay(-1, stoppingToken);
  19.         }
  20.         [CronSchedule(Cron = "0/1 * * * * ?")]
  21.         public void ClearLog()
  22.         {
  23.             try
  24.             {
  25.                 using var scope = serviceProvider.CreateScope();
  26.                 var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
  27.                 //省略业务代码
  28.                 Console.WriteLine("ClearLog:" + DateTime.Now);
  29.             }
  30.             catch (Exception ex)
  31.             {
  32.                 logger.LogError(ex, "DemoTask.ClearLog");
  33.             }
  34.         }
  35.         [CronSchedule(Cron = "0/5 * * * * ?")]
  36.         public void ClearCache()
  37.         {
  38.             try
  39.             {
  40.                 using var scope = serviceProvider.CreateScope();
  41.                 var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
  42.                 var distLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>();
  43.                 //省略业务代码
  44.                 Console.WriteLine("ClearCache:" + DateTime.Now);
  45.             }
  46.             catch (Exception ex)
  47.             {
  48.                 logger.LogError(ex, "DemoTask.ClearCache");
  49.             }
  50.         }
  51.     }
  52. }
复制代码
 
该Task中有两个方法 ClearLog 和 ClearCache 他们分别会每1秒和每5秒执行一次。需要注意在后台服务中对于 Scope 生命周期的服务在获取是需要手动 CreateScope();
实现的关键点在于 服务执行 ExecuteAsync 中的 CronSchedule.BatchBuilder(stoppingToken, this); 我们这里将代码有 CronSchedule 标记头的方法全部循环进行了启动,该方法的代码如下:
  1. using Common;
  2. using System.Reflection;
  3. namespace TaskService.Libraries
  4. {
  5.     public class CronSchedule
  6.     {
  7.      public static void BatchBuilder(CancellationToken stoppingToken, object context)
  8.         {
  9.             var taskList = context.GetType().GetMethods().Where(t => t.GetCustomAttributes(typeof(CronScheduleAttribute), false).Length > 0).ToList();
  10.             foreach (var t in taskList)
  11.             {
  12.                 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()!;
  13.                 Builder(stoppingToken, cron, t, context);
  14.             }
  15.         }
  16.         private static async void Builder(CancellationToken stoppingToken, string cronExpression, MethodInfo action, object context)
  17.         {
  18.             var nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));
  19.             while (!stoppingToken.IsCancellationRequested)
  20.             {
  21.                 var nowTime = DateTime.Parse(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"));
  22.                 if (nextTime == nowTime)
  23.                 {
  24.                     _ = Task.Run(() =>
  25.                     {
  26.                         action.Invoke(context, null);
  27.                     });
  28.                     nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));
  29.                 }
  30.                 else if (nextTime < nowTime)
  31.                 {
  32.                     nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss"));
  33.                 }
  34.                 await Task.Delay(1000, stoppingToken);
  35.             }
  36.         }
  37.     }
  38.     [AttributeUsage(AttributeTargets.Method)]
  39.     public class CronScheduleAttribute : Attribute
  40.     {
  41.         public string Cron { get; set; }
  42.     }
  43. }
复制代码
 
主要就是利用反射获取当前类中所有带有 CronSchedule 标记的方法,然后解析对应的 Cron 表达式获取下一次的执行时间,如果执行时间等于当前时间则执行一次方法,否则等待1秒钟循环重复这个逻辑。
然后启动我们的项目就可以看到如下的运行效果:

 
 ClearLog 每1秒钟执行一次,ClearCache 每 5秒钟执行一次
 
至此 .NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 就讲解完了,有任何不明白的,可以在文章下面评论或者私信我,欢迎大家积极的讨论交流,有兴趣的朋友可以关注我目前在维护的一个 .NET 基础框架项目,项目地址如下https://github.com/berkerdong/NetEngine.githttps://gitee.com/berkerdong/NetEngine.git
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

乌市泽哥

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表