asp.net core 下,新增了一个BackgroundService用来实现能在后台跑一个长久运行的任务,因此,也可以用来替换掉原来使用的static的Timer组件,
Timer组件主要有以下几个麻烦的地方
1.如果是需要长时间跑的定时任务,需要定义为static,,在asp.net core下,无法利用到DI,无法从DI中获取DbContext之类的
2.启动定时器的时候,需要在start.cs自己手动启动
3.Timer是传入处理函数的方式,如果有好几个定时器,拼在一起,代码会看起来比较乱
4.使用Timer也无法实现corn表达式
5.Timer中也无法使用async异步处理
首先,我们来实现一个简单的定时器功能,用来替换掉Timer类:
1.TimerHostedService 定时器的基类
1 /// <summary> 2 /// 用于在后台执行一个定时任务,用于取代TimeEx,在asp.net core的环境下使用,继承该类后,使用 services.AddHostedService<当前类类型>();后,自动在后台启动当前定时任务 3 /// </summary> 4 public abstract class TimerHostedService : BackgroundService 5 { 6 private IServiceProvider _provider = null; 7 8 protected TimerHostedService(IServiceProvider provider) 9 { 10 _provider = provider; 11 } 12 13 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 14 { 15 if (Enabled && Internal>0) //如果启动服务的时候,Enabled为false或者不设置间隔时间,则该定时器永久不启动 16 { 17 while (!stoppingToken.IsCancellationRequested) //如果站点触发停止,则会使用stoppingToken的IsCancellactionRequest判断是否由IIS之类的停止应用 18 { 19 await Task.Delay(Internal, stoppingToken); 20 21 if (!stoppingToken.IsCancellationRequested) 22 { 23 using (var scope = _provider.CreateScope()) 24 { 25 try 26 { 27 await Run(scope.ServiceProvider, stoppingToken); 28 } 29 catch (Exception e) 30 { 31 32 } 33 } 34 } 35 36 37 } 38 } 39 40 41 42 return; 43 } 44 45 /// <summary> 46 /// 实际执行的定时器处理函数 47 /// </summary> 48 /// <param name="serviceScope">当次的Ioc容器,可获取当前程序中用于注入的容器内的类</param> 49 /// <param name="stoppingToken">是否暂停</param> 50 /// <returns></returns> 51 protected abstract Task Run(IServiceProvider serviceProvider, CancellationToken stoppingToken); 52 53 /// <summary> 54 /// 定时器间隔触发时间,单位是ms 55 /// </summary> 56 protected abstract int Internal { get; } 57 58 /// <summary> 59 /// 当前定时器是否启用,true为定时器有效,false为停用 60 /// </summary> 61 public virtual bool Enabled { set; get; } = true; 62 }
2.实现一个定时器:
1 /// <summary> 2 /// 检查订单是否完成的后台任务 3 /// </summary> 4 public class CheckOrderCompleteTask:TimerHostedService 5 { 6 public CheckOrderCompleteTask(IServiceProvider provider) : base(provider) 7 { 8 this.Enabled = CustomConfigManager.Default["Timer:CheckBlessCompleted"].ToBool(); 9 } 10 11 protected override async Task Run(IServiceProvider serviceProvider, CancellationToken stoppingToken) 12 { 13 var s = (XXXService) serviceProvider.GetService(typeof(XXXService)); //可以从DI容器中获取service或者dbcontext 17 await s.CheckXXX(stoppingToken); //此处为实际执行的定时任务处理函数 18 } 19 20 protected override int Internal { get; } = 1000 * 60 * 5; 21 }
在Start.cs中,注册该任务:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 //.....其他代码 4 services.AddHostedService<CheckOrderCompleteTask>(); //此处将服务注册后,即由asp.net core在启动后,自动启动该服务 5 }
这样,就会有asp.net core自动启动该任务
由于上面定义的是一个定时器,有时候需要比如半夜12点,或者中午12点运行,这种场景下,就需要使用到计划任务的组件了,,.net下,常用的,一般有hangfire跟Quartz.Net,,这两个组件功能比较完善,而且也带有管理功能,but,..就是有时候复杂了点,通常有些不复杂的计划任务,比如又不想直接引入那么复杂的组件,那么可以根据上面的定时组件,变化出一个简单的计划任务组件:
/// <summary> /// 一个简单的corn模式的计划任务<br/>用于在一些已知的计划时间执行某些任务的情况下使用,Cron属性在服务启动后,变无法修改,如需配置运行时可修改,请使用Hangfire之类的其他第三方框架 /// </summary> public abstract class SimpleScheduledTaskService : BackgroundService { private IServiceProvider _provider = null; private CrontabSchedule _crontab = null; private string _cron; private bool _enabled=true; private bool _isInited = false; protected SimpleScheduledTaskService(IServiceProvider provider) { _provider = provider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { _crontab = CrontabSchedule.Parse(_cron); //解析cron字符串 } catch (Exception e) { throw; } while (Enabled && _crontab != null && !stoppingToken.IsCancellationRequested) { var nextDt = _crontab.GetNextOccurrence(DateTime.Now.AddSeconds(2)); var interval = (nextDt - DateTime.Now); await Task.Delay(interval, stoppingToken); var logger = (ILogger)_provider.GetService(typeof(ILogger)); try { logger?.Log(LogLevel.Trace, $"启动计划任务:{this.GetType().Name}"); await Run(_provider, stoppingToken); logger?.Log(LogLevel.Trace, $"完成计划任务:{this.GetType().Name}"); } catch (Exception e) { logger?.Log(LogLevel.Error, e, $"计划任务执行异常:{e.Message}"); } } } protected abstract Task Run(IServiceProvider provider, CancellationToken stoppingToken); /// <summary> /// 计划任务的Cron配置字符串,可使用在线生成器生成后,填入 /// </summary> public virtual string Cron { get => _cron; } /// <summary> /// 计划任务是否启动 /// </summary> public virtual bool Enabled { set => _enabled = value; get => _enabled; } }
上述使用类似Timer的方式,,通过计算cron表达式计算后的结果与当前时间差,delay指定时间后触发,这个功能,一般只能用在一些不是特别重要的定时任务,并且不需要补偿的环境下
通常我都是用比如:https://cron.qqe2.com/之类的在线生成cron表达式的网站生成
计划任务使用的第三方组件为:
NCrontab : https://github.com/atifaziz/NCrontab
上述源码地址:
TimerHostedService: https://github.com/kugarliyifan/Kugar.Core/blob/master/Kugar.Core.NetCore/Services/TimerHostedService.cs
SimpleScheduledTaskService : https://github.com/kugarliyifan/Kugar.Core/blob/master/Kugar.Core.NetCore/Services/ScheduledTaskService.cs