1. 程式人生 > 實用技巧 >.NET Core使用Quartz執行定時任務

.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

。由於需要部署成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大致分為這麼幾部

  1. 建立排程器Scheduler
  2. 建立Job例項
  3. 建立觸發器來控制Job的執行策略
  4. 將Job例項和觸發器例項配對註冊進排程器中
  5. 啟動排程器

 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 }

在此執行除錯,現在一旦到達我們在觸發器中設定的時間點,SendMsgJobExecute方法便會成功觸發。

開發完成後,現在剩下的任務就是如何將專案釋出成一個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服務了。