最近工作上新項目還比較忙,回家之后就不太想碰代碼了,閑暇之余修煉下廚藝,新賽季沖了一波分,也是三個多月沒水過博客了。最近的項目也是主要為團隊提供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 服務了。