最近工作上新项目还比较忙,回家之后就不太想碰代码了,闲暇之余修炼下厨艺,新赛季冲了一波分,也是三个多月没水过博客了。最近的项目也是主要为团队提供API接口,大多都是处理常规的业务逻辑上的事。过程中有个需求是需要每日定时定点执行一些推送消息的任务,一开始也没多想就将定时任务写到了API的项目里,部署完测试下人傻了,日志没有任何执行了任务的痕迹,调试时候没毛病。回头一想,IIS这个懒东西应该是休眠了,直接把我的任务一起回收掉了。淡定的我捋了捋思绪查了查方案,可以更改IIS设置修改定时回收的模式,可以通过访问站点来唤醒,觉得不是很合适,既然是WindowsServer,那我干脆弄一个WindowsService来定时执行任务再好不过了鸭,而且之前也没用过.net core写过WindowsService,正好吃个螃蟹。
一开始我是直接弄了个控制台程序,按照之前.NET Framework的写法来写。后来发现.NET Core专门为这种后台服务(长时间运行的服务)设计了项目模板,称之为Worker Service。为了满足在每日的固定时间点执行,这里选择老牌的Quartz来实现。简单描述一下Demo要实现的需求:每日定点向一个API接口中发送信息。接下来详细记录一下实现过程,Demo的源码:https://github.com/Xuhy0826/WindowsServiceDemo。
使用Visual Studio(我是使用的VS2019)创建项目,选择Worker Service(如下图),姑且就命名为WindowsServiceDemo。
项目创建完成之后里面的内容很简单,一个Program.cs和另一个Work.cs,Work类继承BackgroundService,并重写其ExecuteAsync方法。显而易见,ExecuteAsync方法就是执行后台任务的入口。
Program.cs中,依旧是类型的通过创建一个IHost并启动运行。为了方便进行依赖注入,可以创建一个IServiceCollection的扩展方法来进行服务的注册,接下来一步步介绍。
进行服务注册之前,先将需要引用的包通过Nuget安装一下。安装 Quartz 来实现定时执行任务。另外由于需求需要调用api接口即需要使用HttpClient发送请求,所以还需要另外引入包 Microsoft.Extentsions.Http 。由于需要部署成WindowService,需要引入包 Microsoft.Extensions.Hosting.WindowsServices 。
首先定义Job,即执行任务的具体业务逻辑。创建一个SendMsgJob类,继承IJob接口,并实现Execute方法。Execute方法就是到了设定好的时间点时执行的方法。这里即是实现了使用注册的HttpClient来发送消息的过程。
1 public class SendMsgJob : IJob 2 { 3 private readonly AppSettings _appSettings; 4 private const string ApiClientName = "ApiClient"; 5 private readonly IHttpClientFactory _httpClientFactory; 6 private readonly ILogger<SendMsgJob> _logger; 7 8 public SendMsgJob(IHttpClientFactory httpClientFactory, IOptions<AppSettings> appSettings, ILogger<SendMsgJob> logger) 9 { 10 _httpClientFactory = httpClientFactory; 11 _logger = logger; 12 _appSettings = appSettings.Value; 13 } 14 15 /// <summary> 16 /// 定时执行 17 /// </summary> 18 /// <param name="context"></param> 19 /// <returns></returns> 20 public async Task Execute(IJobExecutionContext context) 21 { 22 _logger.LogInformation($"开始执行定时任务"); 23 //从httpClientFactory获取我们注册的named-HttpClient 24 using var client = _httpClientFactory.CreateClient(ApiClientName); 25 var message = new 26 { 27 title = "今日消息", 28 content = _appSettings.MessageNeedToSend 29 }; 30 //发送消息 31 var response = await client.PostAsync("/msg", new JsonContent(message)); 32 if (response.IsSuccessStatusCode) 33 { 34 _logger.LogInformation($"消息发送成功"); 35 } 36 } 37 }
创建好Job之后,便是设置它让其定时执行即可。来到Work.cs,替换掉原来的默认演示代码,换之配置Job执行策略的代码。使用Quartz配置Job大致分为这么几部
- 创建调度器 Scheduler
- 创建Job实例
- 创建触发器来控制Job的执行策略
- 将Job实例和触发器实例配对注册进调度器中
- 启动调度器
1 public class Worker : BackgroundService 2 { 3 private readonly ILogger<Worker> _logger; 4 5 public Worker(ILogger<Worker> logger) 6 { 7 _logger = logger; 8 } 9 10 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 11 { 12 _logger.LogInformation("服务启动"); 13 14 //创建一个调度器 15 var scheduler = await StdSchedulerFactory.GetDefaultScheduler(stoppingToken); 16 //创建Job 17 var sendMsgJob = JobBuilder.Create<SendMsgJob>() 18 .WithIdentity(nameof(SendMsgJob), nameof(Worker)) 19 .Build(); 20 //创建触发器 21 var sendMsgTrigger = TriggerBuilder.Create() 22 .WithIdentity("trigger-" + nameof(SendMsgJob), "trigger-group-" + nameof(Worker)) 23 .StartNow() 24 .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(08, 30)) //每日的08:30执行 25 .Build(); 26 27 await scheduler.Start(stoppingToken); 28 //把Job和触发器放入调度器中 29 await scheduler.ScheduleJob(sendMsgJob, sendMsgTrigger, stoppingToken); 30 } 31 }
关于定时任务的配置告一段落,接下来将所需的服务注册到服务容器中。根据之前所说的,我们创建一个扩展方法来管理我们需要注册的服务。
1 public static class DependencyInject 2 { 3 /// <summary> 4 /// 定义扩展方法,注册服务 5 /// </summary> 6 public static IServiceCollection AddMyServices(this IServiceCollection services, IConfiguration config) 7 { 8 //配置文件 9 services.Configure<AppSettings>(config); 10 11 //注册“命名HttpClient”,并为其配置拦截器 12 services.AddHttpClient("ApiClient", client => 13 { 14 client.BaseAddress = new Uri(config["ApiBaseUrl"]); 15 }).AddHttpMessageHandler(_ => new AuthenticRequestDelegatingHandler()); 16 17 //注册任务 18 services.AddSingleton<SendMsgJob>(); 19 20 return services; 21 } 22 }
修改Program.cs,调用新增的扩展方法
1 namespace WindowsServiceDemo 2 { 3 public class Program 4 { 5 public static void Main(string[] args) 6 { 7 CreateHostBuilder(args).Build().Run(); 8 } 9 10 public static IHostBuilder CreateHostBuilder(string[] args) => 11 Host.CreateDefaultBuilder(args) 12 .ConfigureServices((hostContext, services) => 13 { 14 //注册服务 15 services.AddMyServices(hostContext.Configuration) 16 .AddHostedService<Worker>(); 17 }); 18 } 19 }
到此,主要的代码就介绍完了。为了调试,可以修改设定好的定时执行时间(比如一分钟之后),来测试是否能够成功。修改完触发器的触发时间后,直接运行项目。但是遗憾的是,任务并没有定时触发。这是什么原因呢?其实是因为虽然我们将我们自定义的Job注入的服务容器,但是调度器创建Job实例时,并不是从我们的服务容器去取的,而是调度器自己走默认的实例化。解决方法是我们为调度器指定JobFactory来重写实例化Job类型的规则。
首先创建一个MyJobFactory并继承IJobFactory接口,实现方法 NewJob ,这个方法便是工厂实例化Job的方法,我们可以在这里将实例化Job的方式改写成从服务容器中获取实例的方式。
1 namespace WindowsServiceDemo 2 { 3 /// <summary> 4 /// Job工厂,从服务容器中取Job 5 /// </summary> 6 public class MyJobFactory : IJobFactory 7 { 8 protected readonly IServiceProvider _serviceProvider; 9 public MyJobFactory(IServiceProvider serviceProvider) 10 { 11 _serviceProvider = serviceProvider; 12 } 13 14 public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) 15 { 16 var jobType = bundle.JobDetail.JobType; 17 try 18 { 19 var job = _serviceProvider.GetService(jobType) as IJob; 20 return job; 21 } 22 catch (Exception e) 23 { 24 Console.WriteLine(e); 25 throw; 26 } 27 } 28 29 public void ReturnJob(IJob job) 30 { 31 var disposable = job as IDisposable; 32 disposable?.Dispose(); 33 } 34 } 35 }
随后将 MyJobFactory 也注册到服务容器中,即在 AddMyServices 扩展方法中添加
1 //添加Job工厂 2 services.AddSingleton<MyJobFactory>();
接下来将调度器的Factory替换成 MyJobFactory ,修改Work.cs代码如下。
1 public class Worker : BackgroundService 2 { 3 private readonly ILogger<Worker> _logger; 4 private readonly MyJobFactory _jobFactory; 5 6 public Worker(ILogger<Worker> logger, MyJobFactory jobFactory) 7 { 8 _logger = logger; 9 _jobFactory = jobFactory; 10 } 11 12 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 13 { 14 _logger.LogInformation("服务启动"); 15 16 //创建一个调度器 17 var scheduler = await StdSchedulerFactory.GetDefaultScheduler(stoppingToken); 18 19 //指定自定义的JobFactory 20 scheduler.JobFactory = _jobFactory; 21 22 //创建Job 23 var sendMsgJob = JobBuilder.Create<SendMsgJob>() 24 .WithIdentity(nameof(SendMsgJob), nameof(Worker)) 25 .Build(); 26 //创建触发器 27 var sendMsgTrigger = TriggerBuilder.Create() 28 .WithIdentity("trigger-" + nameof(SendMsgJob), "trigger-group-" + nameof(Worker)) 29 .StartNow() 30 .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(08, 30)) //每日的08:30执行 31 .Build(); 32 33 await scheduler.Start(stoppingToken); 34 //把Job和触发器放入调度器中 35 await scheduler.ScheduleJob(sendMsgJob, sendMsgTrigger, stoppingToken); 36 } 37 }
在此执行调试,现在一旦到达我们在触发器中设置的时间点, SendMsgJob 的 Execute 方法便会成功触发。
开发完成后,现在剩下的任务就是如何将项目发布成一个WindowsService。来到 Program.cs 下,需要进行一些改动
1 public static IHostBuilder CreateHostBuilder(string[] args) => 2 Host.CreateDefaultBuilder(args) 3 .UseWindowsService() //按照Windows Service运行 4 .ConfigureServices((hostContext, services) => 5 { 6 //注册服务 7 services.AddMyServices(hostContext.Configuration) 8 .AddHostedService<Worker>(); 9 });
重新编译项目成功后,我们便可以使用sc.exe来部署成为windows服务。以管理员身份启动命令行,执行
> sc.exe create WindowsServiceDemo binPath="D:\workspace\WindowsServiceDemo\WindowsServiceDemo\bin\Debug\netcoreapp3.1\WindowsServiceDemo.exe"
[SC] CreateService 成功
此时打开服务面板,便可以看到刚刚部署好的 WindowsServiceDemo 服务了。