.NET Core使用Quartz執行定時任務
最近工作上新專案還比較忙,回家之後就不太想碰程式碼了,閒暇之餘修煉下廚藝,也是三個多月沒水過部落格了。最近的專案也是主要為團隊提供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
首先定義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
[SC] CreateService 成功
此時開啟服務面板,便可以看到剛剛部署好的WindowsServiceDemo服務了。