.NET Core開發Windows服務:使用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 }

在此執行調試,現在一旦到達我們在觸發器中設置的時間點, 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 服務了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM