

.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 - 张晓栋
source link: https://www.cnblogs.com/berkerdong/p/16619415.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件
常用的定时任务组件有 Quartz.Net 和 Hangfire 两种,这两种是使用人数比较多的定时任务组件,个人以前也是使用的 Hangfire ,慢慢的发现自己想要的其实只是一个能够根据 Cron 表达式来定时执行函数的功能,Quartz.Net 和 Hangfire 虽然都能实现这个目的,但是他们都只用来实现 Cron表达式解析定时执行函数就显得太笨重了,所以想着以 解析 Cron表达式定期执行函数为目的,编写了下面的一套逻辑。
首先为了解析 Cron表达式,我们需要一个CronHelper ,代码如下
using System.Globalization; using System.Text; using System.Text.RegularExpressions; namespace Common { public class CronHelper { /// <summary> /// 获取当前时间之后下一次触发时间 /// </summary> /// <param name="cronExpression"></param> /// <returns></returns> public static DateTimeOffset GetNextOccurrence(string cronExpression) { return GetNextOccurrence(cronExpression, DateTimeOffset.UtcNow); } /// <summary> /// 获取给定时间之后下一次触发时间 /// </summary> /// <param name="cronExpression"></param> /// <param name="afterTimeUtc"></param> /// <returns></returns> public static DateTimeOffset GetNextOccurrence(string cronExpression, DateTimeOffset afterTimeUtc) { return new CronExpression(cronExpression).GetTimeAfter(afterTimeUtc)!.Value; } /// <summary> /// 获取当前时间之后N次触发时间 /// </summary> /// <param name="cronExpression"></param> /// <param name="count"></param> /// <returns></returns> public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, int count) { return GetNextOccurrences(cronExpression, DateTimeOffset.UtcNow, count); } /// <summary> /// 获取给定时间之后N次触发时间 /// </summary> /// <param name="cronExpression"></param> /// <param name="afterTimeUtc"></param> /// <returns></returns> public static List<DateTimeOffset> GetNextOccurrences(string cronExpression, DateTimeOffset afterTimeUtc, int count) { CronExpression cron = new(cronExpression); List<DateTimeOffset> dateTimeOffsets = new(); for (int i = 0; i < count; i++) { afterTimeUtc = cron.GetTimeAfter(afterTimeUtc)!.Value; dateTimeOffsets.Add(afterTimeUtc); } return dateTimeOffsets; } private class CronExpression { private const int Second = 0; private const int Minute = 1; private const int Hour = 2; private const int DayOfMonth = 3; private const int Month = 4; private const int DayOfWeek = 5; private const int Year = 6; private const int AllSpecInt = 99; private const int NoSpecInt = 98; private const int AllSpec = AllSpecInt; private const int NoSpec = NoSpecInt; private SortedSet<int> seconds = null!; private SortedSet<int> minutes = null!; private SortedSet<int> hours = null!; private SortedSet<int> daysOfMonth = null!; private SortedSet<int> months = null!; private SortedSet<int> daysOfWeek = null!; private SortedSet<int> years = null!; private bool lastdayOfWeek; private int everyNthWeek; private int nthdayOfWeek; private bool lastdayOfMonth; private bool nearestWeekday; private int lastdayOffset; private static readonly Dictionary<string, int> monthMap = new Dictionary<string, int>(20); private static readonly Dictionary<string, int> dayMap = new Dictionary<string, int>(60); private static readonly int MaxYear = DateTime.Now.Year + 100; private static readonly char[] splitSeparators = { ' ', '\t', '\r', '\n' }; private static readonly char[] commaSeparator = { ',' }; private static readonly Regex regex = new Regex("^L-[0-9]*[W]?", RegexOptions.Compiled); private static readonly TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local; public CronExpression(string cronExpression) { if (monthMap.Count == 0) { monthMap.Add("JAN", 0); monthMap.Add("FEB", 1); monthMap.Add("MAR", 2); monthMap.Add("APR", 3); monthMap.Add("MAY", 4); monthMap.Add("JUN", 5); monthMap.Add("JUL", 6); monthMap.Add("AUG", 7); monthMap.Add("SEP", 8); monthMap.Add("OCT", 9); monthMap.Add("NOV", 10); monthMap.Add("DEC", 11); dayMap.Add("SUN", 1); dayMap.Add("MON", 2); dayMap.Add("TUE", 3); dayMap.Add("WED", 4); dayMap.Add("THU", 5); dayMap.Add("FRI", 6); dayMap.Add("SAT", 7); } if (cronExpression == null) { throw new ArgumentException("cronExpression 不能为空"); } CronExpressionString = CultureInfo.InvariantCulture.TextInfo.ToUpper(cronExpression); BuildExpression(CronExpressionString); } /// <summary> /// 构建表达式 /// </summary> /// <param name="expression"></param> /// <exception cref="FormatException"></exception> private void BuildExpression(string expression) { try { seconds ??= new SortedSet<int>(); minutes ??= new SortedSet<int>(); hours ??= new SortedSet<int>(); daysOfMonth ??= new SortedSet<int>(); months ??= new SortedSet<int>(); daysOfWeek ??= new SortedSet<int>(); years ??= new SortedSet<int>(); int exprOn = Second; string[] exprsTok = expression.Split(splitSeparators, StringSplitOptions.RemoveEmptyEntries); foreach (string exprTok in exprsTok) { string expr = exprTok.Trim(); if (expr.Length == 0) { continue; } if (exprOn > Year) { break; } if (exprOn == DayOfMonth && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0) { throw new FormatException("不支持在月份的其他日期指定“L”和“LW”"); } if (exprOn == DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(",", StringComparison.Ordinal) >= 0) { throw new FormatException("不支持在一周的其他日期指定“L”"); } if (exprOn == DayOfWeek && expr.IndexOf('#') != -1 && expr.IndexOf('#', expr.IndexOf('#') + 1) != -1) { throw new FormatException("不支持指定多个“第N”天。"); } string[] vTok = expr.Split(commaSeparator); foreach (string v in vTok) { StoreExpressionVals(0, v, exprOn); } exprOn++; } if (exprOn <= DayOfWeek) { throw new FormatException("表达式意料之外的结束。"); } if (exprOn <= Year) { StoreExpressionVals(0, "*", Year); } var dow = GetSet(DayOfWeek); var dom = GetSet(DayOfMonth); bool dayOfMSpec = !dom.Contains(NoSpec); bool dayOfWSpec = !dow.Contains(NoSpec); if (dayOfMSpec && !dayOfWSpec) { // skip } else if (dayOfWSpec && !dayOfMSpec) { // skip } else { throw new FormatException("不支持同时指定星期和日参数。"); } } catch (FormatException) { throw; } catch (Exception e) { throw new FormatException($"非法的 cron 表达式格式 ({e.Message})", e); } } /// <summary> /// Stores the expression values. /// </summary> /// <param name="pos">The position.</param> /// <param name="s">The string to traverse.</param> /// <param name="type">The type of value.</param> /// <returns></returns> private int StoreExpressionVals(int pos, string s, int type) { int incr = 0; int i = SkipWhiteSpace(pos, s); if (i >= s.Length) { return i; } char c = s[i]; if (c >= 'A' && c <= 'Z' && !s.Equals("L") && !s.Equals("LW") && !regex.IsMatch(s)) { string sub = s.Substring(i, 3); int sval; int eval = -1; if (type == Month) { sval = GetMonthNumber(sub) + 1; if (sval <= 0) { throw new FormatException($"无效的月份值:'{sub}'"); } if (s.Length > i + 3) { c = s[i + 3]; if (c == '-') { i += 4; sub = s.Substring(i, 3); eval = GetMonthNumber(sub) + 1; if (eval <= 0) { throw new FormatException( $"无效的月份值: '{sub}'"); } } } } else if (type == DayOfWeek) { sval = GetDayOfWeekNumber(sub); if (sval < 0) { throw new FormatException($"无效的星期几值: '{sub}'"); } if (s.Length > i + 3) { c = s[i + 3]; if (c == '-') { i += 4; sub = s.Substring(i, 3); eval = GetDayOfWeekNumber(sub); if (eval < 0) { throw new FormatException( $"无效的星期几值: '{sub}'"); } } else if (c == '#') { try { i += 4; nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture); if (nthdayOfWeek is < 1 or > 5) { throw new FormatException("周的第n天小于1或大于5"); } } catch (Exception) { throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面"); } } else if (c == '/') { try { i += 4; everyNthWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture); if (everyNthWeek is < 1 or > 5) { throw new FormatException("每个星期<1或>5"); } } catch (Exception) { throw new FormatException("1 到 5 之间的数值必须跟在 '/' 选项后面"); } } else if (c == 'L') { lastdayOfWeek = true; i++; } else { throw new FormatException($"此位置的非法字符:'{sub}'"); } } } else { throw new FormatException($"此位置的非法字符:'{sub}'"); } if (eval != -1) { incr = 1; } AddToSet(sval, eval, incr, type); return i + 3; } if (c == '?') { i++; if (i + 1 < s.Length && s[i] != ' ' && s[i + 1] != '\t') { throw new FormatException("'?' 后的非法字符: " + s[i]); } if (type != DayOfWeek && type != DayOfMonth) { throw new FormatException( "'?' 只能为月日或周日指定。"); } if (type == DayOfWeek && !lastdayOfMonth) { int val = daysOfMonth.LastOrDefault(); if (val == NoSpecInt) { throw new FormatException( "'?' 只能为月日或周日指定。"); } } AddToSet(NoSpecInt, -1, 0, type); return i; } var startsWithAsterisk = c == '*'; if (startsWithAsterisk || c == '/') { if (startsWithAsterisk && i + 1 >= s.Length) { AddToSet(AllSpecInt, -1, incr, type); return i + 1; } if (c == '/' && (i + 1 >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t')) { throw new FormatException("'/' 后面必须跟一个整数。"); } if (startsWithAsterisk) { i++; } c = s[i]; if (c == '/') { // is an increment specified? i++; if (i >= s.Length) { throw new FormatException("字符串意外结束。"); } incr = GetNumericValue(s, i); i++; if (incr > 10) { i++; } CheckIncrementRange(incr, type); } else { if (startsWithAsterisk) { throw new FormatException("星号后的非法字符:" + s); } incr = 1; } AddToSet(AllSpecInt, -1, incr, type); return i; } if (c == 'L') { i++; if (type == DayOfMonth) { lastdayOfMonth = true; } if (type == DayOfWeek) { AddToSet(7, 7, 0, type); } if (type == DayOfMonth && s.Length > i) { c = s[i]; if (c == '-') { ValueSet vs = GetValue(0, s, i + 1); lastdayOffset = vs.theValue; if (lastdayOffset > 30) { throw new FormatException("与最后一天的偏移量必须 <= 30"); } i = vs.pos; } if (s.Length > i) { c = s[i]; if (c == 'W') { nearestWeekday = true; i++; } } } return i; } if (c >= '0' && c <= '9') { int val = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture); i++; if (i >= s.Length) { AddToSet(val, -1, -1, type); } else { c = s[i]; if (c >= '0' && c <= '9') { ValueSet vs = GetValue(val, s, i); val = vs.theValue; i = vs.pos; } i = CheckNext(i, s, val, type); return i; } } else { throw new FormatException($"意外字符:{c}"); } return i; } // ReSharper disable once UnusedParameter.Local private static void CheckIncrementRange(int incr, int type) { if (incr > 59 && (type == Second || type == Minute)) { throw new FormatException($"增量 > 60 : {incr}"); } if (incr > 23 && type == Hour) { throw new FormatException($"增量 > 24 : {incr}"); } if (incr > 31 && type == DayOfMonth) { throw new FormatException($"增量 > 31 : {incr}"); } if (incr > 7 && type == DayOfWeek) { throw new FormatException($"增量 > 7 : {incr}"); } if (incr > 12 && type == Month) { throw new FormatException($"增量 > 12 : {incr}"); } } /// <summary> /// Checks the next value. /// </summary> /// <param name="pos">The position.</param> /// <param name="s">The string to check.</param> /// <param name="val">The value.</param> /// <param name="type">The type to search.</param> /// <returns></returns> private int CheckNext(int pos, string s, int val, int type) { int end = -1; int i = pos; if (i >= s.Length) { AddToSet(val, end, -1, type); return i; } char c = s[pos]; if (c == 'L') { if (type == DayOfWeek) { if (val < 1 || val > 7) { throw new FormatException("星期日值必须介于1和7之间"); } lastdayOfWeek = true; } else { throw new FormatException($"'L' 选项在这里无效。(位置={i})"); } var data = GetSet(type); data.Add(val); i++; return i; } if (c == 'W') { if (type == DayOfMonth) { nearestWeekday = true; } else { throw new FormatException($"'W' 选项在这里无效。 (位置={i})"); } if (val > 31) { throw new FormatException("'W' 选项对于大于 31 的值(一个月中的最大天数)没有意义"); } var data = GetSet(type); data.Add(val); i++; return i; } if (c == '#') { if (type != DayOfWeek) { throw new FormatException($"'#' 选项在这里无效。 (位置={i})"); } i++; try { nthdayOfWeek = Convert.ToInt32(s.Substring(i), CultureInfo.InvariantCulture); if (nthdayOfWeek is < 1 or > 5) { throw new FormatException("周的第n天小于1或大于5"); } } catch (Exception) { throw new FormatException("1 到 5 之间的数值必须跟在“#”选项后面"); } var data = GetSet(type); data.Add(val); i++; return i; } if (c == 'C') { if (type == DayOfWeek) { } else if (type == DayOfMonth) { } else { throw new FormatException($"'C' 选项在这里无效。(位置={i})"); } var data = GetSet(type); data.Add(val); i++; return i; } if (c == '-') { i++; c = s[i]; int v = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture); end = v; i++; if (i >= s.Length) { AddToSet(val, end, 1, type); return i; } c = s[i]; if (c >= '0' && c <= '9') { ValueSet vs = GetValue(v, s, i); int v1 = vs.theValue; end = v1; i = vs.pos; } if (i < s.Length && s[i] == '/') { i++; c = s[i]; int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture); i++; if (i >= s.Length) { AddToSet(val, end, v2, type); return i; } c = s[i]; if (c >= '0' && c <= '9') { ValueSet vs = GetValue(v2, s, i); int v3 = vs.theValue; AddToSet(val, end, v3, type); i = vs.pos; return i; } AddToSet(val, end, v2, type); return i; } AddToSet(val, end, 1, type); return i; } if (c == '/') { if (i + 1 >= s.Length || s[i + 1] == ' ' || s[i + 1] == '\t') { throw new FormatException("\'/\' 后面必须跟一个整数。"); } i++; c = s[i]; int v2 = Convert.ToInt32(c.ToString(), CultureInfo.InvariantCulture); i++; if (i >= s.Length) { CheckIncrementRange(v2, type); AddToSet(val, end, v2, type); return i; } c = s[i]; if (c >= '0' && c <= '9') { ValueSet vs = GetValue(v2, s, i); int v3 = vs.theValue; CheckIncrementRange(v3, type); AddToSet(val, end, v3, type); i = vs.pos; return i; } throw new FormatException($"意外的字符 '{c}' 后 '/'"); } AddToSet(val, end, 0, type); i++; return i; } /// <summary> /// Gets the cron expression string. /// </summary> /// <value>The cron expression string.</value> private static string CronExpressionString; /// <summary> /// Skips the white space. /// </summary> /// <param name="i">The i.</param> /// <param name="s">The s.</param> /// <returns></returns> private static int SkipWhiteSpace(int i, string s) { for (; i < s.Length && (s[i] == ' ' || s[i] == '\t'); i++) { } return i; } /// <summary> /// Finds the next white space. /// </summary> /// <param name="i">The i.</param> /// <param name="s">The s.</param> /// <returns></returns> private static int FindNextWhiteSpace(int i, string s) { for (; i < s.Length && (s[i] != ' ' || s[i] != '\t'); i++) { } return i; } /// <summary> /// Adds to set. /// </summary> /// <param name="val">The val.</param> /// <param name="end">The end.</param> /// <param name="incr">The incr.</param> /// <param name="type">The type.</param> private void AddToSet(int val, int end, int incr, int type) { var data = GetSet(type); if (type == Second || type == Minute) { if ((val < 0 || val > 59 || end > 59) && val != AllSpecInt) { throw new FormatException("分钟和秒值必须介于0和59之间"); } } else if (type == Hour) { if ((val < 0 || val > 23 || end > 23) && val != AllSpecInt) { throw new FormatException("小时值必须介于0和23之间"); } } else if (type == DayOfMonth) { if ((val < 1 || val > 31 || end > 31) && val != AllSpecInt && val != NoSpecInt) { throw new FormatException("月日值必须介于1和31之间"); } } else if (type == Month) { if ((val < 1 || val > 12 || end > 12) && val != AllSpecInt) { throw new FormatException("月份值必须介于1和12之间"); } } else if (type == DayOfWeek) { if ((val == 0 || val > 7 || end > 7) && val != AllSpecInt && val != NoSpecInt) { throw new FormatException("星期日值必须介于1和7之间"); } } if ((incr == 0 || incr == -1) && val != AllSpecInt) { if (val != -1) { data.Add(val); } else { data.Add(NoSpec); } return; } int startAt = val; int stopAt = end; if (val == AllSpecInt && incr <= 0) { incr = 1; data.Add(AllSpec); } if (type == Second || type == Minute) { if (stopAt == -1) { stopAt = 59; } if (startAt == -1 || startAt == AllSpecInt) { startAt = 0; } } else if (type == Hour) { if (stopAt == -1) { stopAt = 23; } if (startAt == -1 || startAt == AllSpecInt) { startAt = 0; } } else if (type == DayOfMonth) { if (stopAt == -1) { stopAt = 31; } if (startAt == -1 || startAt == AllSpecInt) { startAt = 1; } } else if (type == Month) { if (stopAt == -1) { stopAt = 12; } if (startAt == -1 || startAt == AllSpecInt) { startAt = 1; } } else if (type == DayOfWeek) { if (stopAt == -1) { stopAt = 7; } if (startAt == -1 || startAt == AllSpecInt) { startAt = 1; } } else if (type == Year) { if (stopAt == -1) { stopAt = MaxYear; } if (startAt == -1 || startAt == AllSpecInt) { startAt = 1970; } } int max = -1; if (stopAt < startAt) { switch (type) { case Second: max = 60; break; case Minute: max = 60; break; case Hour: max = 24; break; case Month: max = 12; break; case DayOfWeek: max = 7; break; case DayOfMonth: max = 31; break; case Year: throw new ArgumentException("开始年份必须小于停止年份"); default: throw new ArgumentException("遇到意外的类型"); } stopAt += max; } for (int i = startAt; i <= stopAt; i += incr) { if (max == -1) { data.Add(i); } else { int i2 = i % max; if (i2 == 0 && (type == Month || type == DayOfWeek || type == DayOfMonth)) { i2 = max; } data.Add(i2); } } } /// <summary> /// Gets the set of given type. /// </summary> /// <param name="type">The type of set to get.</param> /// <returns></returns> private SortedSet<int> GetSet(int type) { switch (type) { case Second: return seconds; case Minute: return minutes; case Hour: return hours; case DayOfMonth: return daysOfMonth; case Month: return months; case DayOfWeek: return daysOfWeek; case Year: return years; default: throw new ArgumentOutOfRangeException(); } } /// <summary> /// Gets the value. /// </summary> /// <param name="v">The v.</param> /// <param name="s">The s.</param> /// <param name="i">The i.</param> /// <returns></returns> private static ValueSet GetValue(int v, string s, int i) { char c = s[i]; StringBuilder s1 = new StringBuilder(v.ToString(CultureInfo.InvariantCulture)); while (c >= '0' && c <= '9') { s1.Append(c); i++; if (i >= s.Length) { break; } c = s[i]; } ValueSet val = new ValueSet(); if (i < s.Length) { val.pos = i; } else { val.pos = i + 1; } val.theValue = Convert.ToInt32(s1.ToString(), CultureInfo.InvariantCulture); return val; } /// <summary> /// Gets the numeric value from string. /// </summary> /// <param name="s">The string to parse from.</param> /// <param name="i">The i.</param> /// <returns></returns> private static int GetNumericValue(string s, int i) { int endOfVal = FindNextWhiteSpace(i, s); string val = s.Substring(i, endOfVal - i); return Convert.ToInt32(val, CultureInfo.InvariantCulture); } /// <summary> /// Gets the month number. /// </summary> /// <param name="s">The string to map with.</param> /// <returns></returns> private static int GetMonthNumber(string s) { if (monthMap.ContainsKey(s)) { return monthMap[s]; } return -1; } /// <summary> /// Gets the day of week number. /// </summary> /// <param name="s">The s.</param> /// <returns></returns> private static int GetDayOfWeekNumber(string s) { if (dayMap.ContainsKey(s)) { return dayMap[s]; } return -1; } /// <summary> /// 在给定时间之后获取下一个触发时间。 /// </summary> /// <param name="afterTimeUtc">开始搜索的 UTC 时间。</param> /// <returns></returns> public DateTimeOffset? GetTimeAfter(DateTimeOffset afterTimeUtc) { // 向前移动一秒钟,因为我们正在计算时间*之后* afterTimeUtc = afterTimeUtc.AddSeconds(1); // CronTrigger 不处理毫秒 DateTimeOffset d = CreateDateTimeWithoutMillis(afterTimeUtc); // 更改为指定时区 d = TimeZoneInfo.ConvertTime(d, timeZoneInfo); bool gotOne = false; //循环直到我们计算出下一次,或者我们已经过了 endTime while (!gotOne) { SortedSet<int> st; int t; int sec = d.Second; st = seconds.GetViewBetween(sec, 9999999); if (st.Count > 0) { sec = st.First(); } else { sec = seconds.First(); d = d.AddMinutes(1); } d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, d.Minute, sec, d.Millisecond, d.Offset); int min = d.Minute; int hr = d.Hour; t = -1; st = minutes.GetViewBetween(min, 9999999); if (st.Count > 0) { t = min; min = st.First(); } else { min = minutes.First(); hr++; } if (min != t) { d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, 0, d.Millisecond, d.Offset); d = SetCalendarHour(d, hr); continue; } d = new DateTimeOffset(d.Year, d.Month, d.Day, d.Hour, min, d.Second, d.Millisecond, d.Offset); hr = d.Hour; int day = d.Day; t = -1; st = hours.GetViewBetween(hr, 9999999); if (st.Count > 0) { t = hr; hr = st.First(); } else { hr = hours.First(); day++; } if (hr != t) { int daysInMonth = DateTime.DaysInMonth(d.Year, d.Month); if (day > daysInMonth) { d = new DateTimeOffset(d.Year, d.Month, daysInMonth, d.Hour, 0, 0, d.Millisecond, d.Offset).AddDays(day - daysInMonth); } else { d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, 0, 0, d.Millisecond, d.Offset); } d = SetCalendarHour(d, hr); continue; } d = new DateTimeOffset(d.Year, d.Month, d.Day, hr, d.Minute, d.Second, d.Millisecond, d.Offset); day = d.Day; int mon = d.Month; t = -1; int tmon = mon; bool dayOfMSpec = !daysOfMonth.Contains(NoSpec); bool dayOfWSpec = !daysOfWeek.Contains(NoSpec); if (dayOfMSpec && !dayOfWSpec) { // 逐月获取规则 st = daysOfMonth.GetViewBetween(day, 9999999); bool found = st.Any(); if (lastdayOfMonth) { if (!nearestWeekday) { t = day; day = GetLastDayOfMonth(mon, d.Year); day -= lastdayOffset; if (t > day) { mon++; if (mon > 12) { mon = 1; tmon = 3333; // 确保下面的 mon != tmon 测试失败 d = d.AddYears(1); } day = 1; } } else { t = day; day = GetLastDayOfMonth(mon, d.Year); day -= lastdayOffset; DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset); int ldom = GetLastDayOfMonth(mon, d.Year); DayOfWeek dow = tcal.DayOfWeek; if (dow == System.DayOfWeek.Saturday && day == 1) { day += 2; } else if (dow == System.DayOfWeek.Saturday) { day -= 1; } else if (dow == System.DayOfWeek.Sunday && day == ldom) { day -= 2; } else if (dow == System.DayOfWeek.Sunday) { day += 1; } DateTimeOffset nTime = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Millisecond, d.Offset); if (nTime.ToUniversalTime() < afterTimeUtc) { day = 1; mon++; } } } else if (nearestWeekday) { t = day; day = daysOfMonth.First(); DateTimeOffset tcal = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset); int ldom = GetLastDayOfMonth(mon, d.Year); DayOfWeek dow = tcal.DayOfWeek; if (dow == System.DayOfWeek.Saturday && day == 1) { day += 2; } else if (dow == System.DayOfWeek.Saturday) { day -= 1; } else if (dow == System.DayOfWeek.Sunday && day == ldom) { day -= 2; } else if (dow == System.DayOfWeek.Sunday) { day += 1; } tcal = new DateTimeOffset(tcal.Year, mon, day, hr, min, sec, d.Offset); if (tcal.ToUniversalTime() < afterTimeUtc) { day = daysOfMonth.First(); mon++; } } else if (found) { t = day; day = st.First(); //确保我们不会在短时间内跑得过快,比如二月 int lastDay = GetLastDayOfMonth(mon, d.Year); if (day > lastDay) { day = daysOfMonth.First(); mon++; } } else { day = daysOfMonth.First(); mon++; } if (day != t || mon != tmon) { if (mon > 12) { d = new DateTimeOffset(d.Year, 12, day, 0, 0, 0, d.Offset).AddMonths(mon - 12); } else { //这是为了避免从一个月移动时出现错误 //有 30 或 31 天到一个月更少。 导致实例化无效的日期时间。 int lDay = DateTime.DaysInMonth(d.Year, mon); if (day <= lDay) { d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset); } else { d = new DateTimeOffset(d.Year, mon, lDay, 0, 0, 0, d.Offset).AddDays(day - lDay); } } continue; } } else if (dayOfWSpec && !dayOfMSpec) { // 获取星期几规则 if (lastdayOfWeek) { int dow = daysOfWeek.First(); int cDow = (int)d.DayOfWeek + 1; int daysToAdd = 0; if (cDow < dow) { daysToAdd = dow - cDow; } if (cDow > dow) { daysToAdd = dow + (7 - cDow); } int lDay = GetLastDayOfMonth(mon, d.Year); if (day + daysToAdd > lDay) { if (mon == 12) { d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1); } else { d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset); } continue; } // 查找本月这一天最后一次出现的日期... while (day + daysToAdd + 7 <= lDay) { daysToAdd += 7; } day += daysToAdd; if (daysToAdd > 0) { d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset); continue; } } else if (nthdayOfWeek != 0) { int dow = daysOfWeek.First(); int cDow = (int)d.DayOfWeek + 1; int daysToAdd = 0; if (cDow < dow) { daysToAdd = dow - cDow; } else if (cDow > dow) { daysToAdd = dow + (7 - cDow); } bool dayShifted = daysToAdd > 0; day += daysToAdd; int weekOfMonth = day / 7; if (day % 7 > 0) { weekOfMonth++; } daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; day += daysToAdd; if (daysToAdd < 0 || day > GetLastDayOfMonth(mon, d.Year)) { if (mon == 12) { d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1); } else { d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset); } continue; } if (daysToAdd > 0 || dayShifted) { d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset); continue; } } else if (everyNthWeek != 0) { int cDow = (int)d.DayOfWeek + 1; int dow = daysOfWeek.First(); st = daysOfWeek.GetViewBetween(cDow, 9999999); if (st.Count > 0) { dow = st.First(); } int daysToAdd = 0; if (cDow < dow) { daysToAdd = (dow - cDow) + (7 * (everyNthWeek - 1)); } if (cDow > dow) { daysToAdd = (dow + (7 - cDow)) + (7 * (everyNthWeek - 1)); } if (daysToAdd > 0) { d = new DateTimeOffset(d.Year, mon, day, 0, 0, 0, d.Offset); d = d.AddDays(daysToAdd); continue; } } else { int cDow = (int)d.DayOfWeek + 1; int dow = daysOfWeek.First(); st = daysOfWeek.GetViewBetween(cDow, 9999999); if (st.Count > 0) { dow = st.First(); } int daysToAdd = 0; if (cDow < dow) { daysToAdd = dow - cDow; } if (cDow > dow) { daysToAdd = dow + (7 - cDow); } int lDay = GetLastDayOfMonth(mon, d.Year); if (day + daysToAdd > lDay) { if (mon == 12) { d = new DateTimeOffset(d.Year, mon - 11, 1, 0, 0, 0, d.Offset).AddYears(1); } else { d = new DateTimeOffset(d.Year, mon + 1, 1, 0, 0, 0, d.Offset); } continue; } if (daysToAdd > 0) { d = new DateTimeOffset(d.Year, mon, day + daysToAdd, 0, 0, 0, d.Offset); continue; } } } else { throw new FormatException("不支持同时指定星期日和月日参数。"); } d = new DateTimeOffset(d.Year, d.Month, day, d.Hour, d.Minute, d.Second, d.Offset); mon = d.Month; int year = d.Year; t = -1; if (year > MaxYear) { return null; } st = months.GetViewBetween(mon, 9999999); if (st.Count > 0) { t = mon; mon = st.First(); } else { mon = months.First(); year++; } if (mon != t) { d = new DateTimeOffset(year, mon, 1, 0, 0, 0, d.Offset); continue; } d = new DateTimeOffset(d.Year, mon, d.Day, d.Hour, d.Minute, d.Second, d.Offset); year = d.Year; t = -1; st = years.GetViewBetween(year, 9999999); if (st.Count > 0) { t = year; year = st.First(); } else { return null; } if (year != t) { d = new DateTimeOffset(year, 1, 1, 0, 0, 0, d.Offset); continue; } d = new DateTimeOffset(year, d.Month, d.Day, d.Hour, d.Minute, d.Second, d.Offset); //为此日期应用适当的偏移量 d = new DateTimeOffset(d.DateTime, timeZoneInfo.BaseUtcOffset); gotOne = true; } return d.ToUniversalTime(); } /// <summary> /// Creates the date time without milliseconds. /// </summary> /// <param name="time">The time.</param> /// <returns></returns> private static DateTimeOffset CreateDateTimeWithoutMillis(DateTimeOffset time) { return new DateTimeOffset(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Offset); } /// <summary> /// Advance the calendar to the particular hour paying particular attention /// to daylight saving problems. /// </summary> /// <param name="date">The date.</param> /// <param name="hour">The hour.</param> /// <returns></returns> private static DateTimeOffset SetCalendarHour(DateTimeOffset date, int hour) { int hourToSet = hour; if (hourToSet == 24) { hourToSet = 0; } DateTimeOffset d = new DateTimeOffset(date.Year, date.Month, date.Day, hourToSet, date.Minute, date.Second, date.Millisecond, date.Offset); if (hour == 24) { d = d.AddDays(1); } return d; } /// <summary> /// Gets the last day of month. /// </summary> /// <param name="monthNum">The month num.</param> /// <param name="year">The year.</param> /// <returns></returns> private static int GetLastDayOfMonth(int monthNum, int year) { return DateTime.DaysInMonth(year, monthNum); } private class ValueSet { public int theValue; public int pos; } } } }
CronHelper 中 CronExpression 的函数计算逻辑是从 Quart.NET 借鉴的,支持标准的 7位 cron 表达式,在需要生成Cron 表达式时可以直接使用网络上的各种 Cron 表达式在线生成
CronHelper 里面我们主要用到的功能就是 通过 Cron 表达式,解析下一次的执行时间。
服务运行这块我们采用微软的 BackgroundService 后台服务,这里还要用到一个后台服务批量注入的逻辑 关于后台逻辑批量注入可以看我之前写的一篇博客,这里就不展开介绍了
.NET 使用自带 DI 批量注入服务(Service)和 后台服务(BackgroundService) https://www.cnblogs.com/berkerdong/p/16496232.html
接下来看一下我这里写的一个DemoTask,代码如下:
using DistributedLock; using Repository.Database; using TaskService.Libraries; namespace TaskService.Tasks { public class DemoTask : BackgroundService { private readonly IServiceProvider serviceProvider; private readonly ILogger logger; public DemoTask(IServiceProvider serviceProvider, ILogger<DemoTask> logger) { this.serviceProvider = serviceProvider; this.logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { CronSchedule.BatchBuilder(stoppingToken, this); await Task.Delay(-1, stoppingToken); } [CronSchedule(Cron = "0/1 * * * * ?")] public void ClearLog() { try { using var scope = serviceProvider.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>(); //省略业务代码 Console.WriteLine("ClearLog:" + DateTime.Now); } catch (Exception ex) { logger.LogError(ex, "DemoTask.ClearLog"); } } [CronSchedule(Cron = "0/5 * * * * ?")] public void ClearCache() { try { using var scope = serviceProvider.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>(); var distLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>(); //省略业务代码 Console.WriteLine("ClearCache:" + DateTime.Now); } catch (Exception ex) { logger.LogError(ex, "DemoTask.ClearCache"); } } } }
该Task中有两个方法 ClearLog 和 ClearCache 他们分别会每1秒和每5秒执行一次。需要注意在后台服务中对于 Scope 生命周期的服务在获取是需要手动 CreateScope();
实现的关键点在于 服务执行 ExecuteAsync 中的 CronSchedule.BatchBuilder(stoppingToken, this); 我们这里将代码有 CronSchedule 标记头的方法全部循环进行了启动,该方法的代码如下:
using Common; using System.Reflection; namespace TaskService.Libraries { public class CronSchedule { public static void BatchBuilder(CancellationToken stoppingToken, object context) { var taskList = context.GetType().GetMethods().Where(t => t.GetCustomAttributes(typeof(CronScheduleAttribute), false).Length > 0).ToList(); foreach (var t in taskList) { string cron = t.CustomAttributes.Where(t => t.AttributeType == typeof(CronScheduleAttribute)).FirstOrDefault()!.NamedArguments.Where(t => t.MemberName == "Cron" && t.TypedValue.Value != null).Select(t => t.TypedValue.Value!.ToString()).FirstOrDefault()!; Builder(stoppingToken, cron, t, context); } } private static async void Builder(CancellationToken stoppingToken, string cronExpression, MethodInfo action, object context) { var nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss")); while (!stoppingToken.IsCancellationRequested) { var nowTime = DateTime.Parse(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")); if (nextTime == nowTime) { _ = Task.Run(() => { action.Invoke(context, null); }); nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss")); } else if (nextTime < nowTime) { nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(cronExpression).ToString("yyyy-MM-dd HH:mm:ss")); } await Task.Delay(1000, stoppingToken); } } } [AttributeUsage(AttributeTargets.Method)] public class CronScheduleAttribute : Attribute { public string Cron { get; set; } } }
主要就是利用反射获取当前类中所有带有 CronSchedule 标记的方法,然后解析对应的 Cron 表达式获取下一次的执行时间,如果执行时间等于当前时间则执行一次方法,否则等待1秒钟循环重复这个逻辑。
然后启动我们的项目就可以看到如下的运行效果:

ClearLog 每1秒钟执行一次,ClearCache 每 5秒钟执行一次
Recommend
-
77
本实例将创建目录放置于/mnt目录下,可根据具体情况放置于其他目录: cd /mnt mkdir dbback pwd /mnt/dbback 创建shell脚本 脚本名称可根据自己规范进行自定义: vim bcmysql.sh
-
46
k8s 中有许多优秀的包都可以在平时的开发中借鉴与使用,比如,任务的定时轮询、高可用的实现、日志处理、缓存使用等都是独立的包,可以直接引用。本篇文章会介绍 k8s 中定时任务的实现,k8s 中定时任务都是通过 wait 包实现的,wait 包在...
-
54
k8s中有许多优秀的包都可以在平时的开发中借鉴与使用,比如,任务的定时轮询、高可用的实现、日志处理、缓存使用等都是独立的包,可以直接引用。本篇文章会介绍k8s中定时任务的实现,k8s中定时任务都是通过wait包实现的,wait包在k8s的多个组件中都有用到,以下是w...
-
13
定时任务,可以说是业务系统必不可少的一个部分,今天我们就一起来了解一下 JDK 定时任务实现及原理分析。 在很多业务的系统中,我们常常需要定时的执行一些任务,例如定时发短信、定时变更数据、定时发起促销活动等等。
-
3
在计算机的使用过程中,经常会有一些计划中的任务需要在将来的某个时间执行,linux中提供了一些方法来设定定时任务。 1、at 命令at从文件或标准输入中读取命令并在将来的一个时间执行,只执行一次。
-
8
V2EX › Python Flask 中怎样用 while 写一个定时执行的任务?(定时查询最新数据) miniyao...
-
9
爱码爱生活 java定时任务Spring Boot提供了@EnableSch...
-
5
Django配置Celery执行异步任务和定时任务 原生Celery,非djcelery模块,所有演示均基于Django2.0 celery是一个基于python开发的简单、灵活且可靠的分布式任务队列框架,支持使用任务队列的方式在分布式的...
-
7
.NET 纯原生实现 Cron 定时任务执行,未依赖第三方组件 (Timer 优化版) ...
-
9
基于 Ubuntu 服务器配置原生的 Socks5 网关代理服务器 ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK