在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度
在這篇文章中,我將介紹如何使用ASP.NET Core托管服務運行Quartz.NET作業。這樣的好處是我們可以在應用程序啟動和停止時很方便的來控制我們的Job的運行狀態。接下來我將演示如何創建一個簡單的?IJob,一個自定義的?IJobFactory和一個在應用程序運行時就開始運行的QuartzHostedService。我還將介紹一些需要注意的問題,即在單例類中使用作用域服務。
簡介-什么是Quartz.NET?
在開始介紹什么是Quartz.NET前先看一下下面這個圖,這個圖基本概括了Quartz.NET的所有核心內容。
注:此圖為百度上獲取,旨在學習交流使用,如有侵權,聯系后刪除。
以下來自他們的網站的描述:
Quartz.NET是功能齊全的開源作業調度系統,適用于從最小型的應用程序到大型企業系統。
對于許多ASP.NET開發人員來說它是首選,用作在計時器上以可靠、集群的方式運行后臺任務的方法。將Quartz.NET與ASP.NET Core一起使用也非常相似-因為Quartz.NET支持.NET Standard 2.0,因此您可以輕松地在應用程序中使用它。
Quartz.NET有兩個主要概念:
Job。這是您要按某個特定時間表運行的后臺任務。
Scheduler。這是負責基于觸發器,基于時間的計劃運行作業。
ASP.NET Core通過托管服務對運行“后臺任務”具有良好的支持。托管服務在ASP.NET Core應用程序啟動時啟動,并在應用程序生命周期內在后臺運行。通過創建Quartz.NET托管服務,您可以使用標準ASP.NET Core應用程序在后臺運行任務。
雖然可以創建“定時”后臺服務(例如,每10分鐘運行一次任務),但Quartz.NET提供了更為強大的解決方案。通過使用Cron觸發器,您可以確保任務僅在一天的特定時間(例如,凌晨2:30)運行,或僅在特定的幾天運行,或任意組合運行。它還允許您以集群方式運行應用程序的多個實例,以便在任何時候只能運行一個實例(高可用)。
在本文中,我將介紹創建Quartz.NET作業的基本知識并將其調度為在托管服務中的計時器上運行。
安裝Quartz.NET
Quartz.NET是.NET Standard 2.0 NuGet軟件包,因此非常易于安裝在您的應用程序中。對于此測試,我創建了一個ASP.NET Core項目并選擇了Empty模板。您可以使用dotnet add package Quartz來安裝Quartz.NET軟件包。這時候查看該項目的.csproj,應如下所示:
<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>netcoreapp3.1</TargetFramework></PropertyGroup><ItemGroup><PackageReference Include="Quartz" Version="3.0.7" /></ItemGroup> </Project>創建一個IJob
對于我們正在安排的實際后臺工作,我們將通過向注入的ILogger<>中寫入“ hello world”來進行實現進而向控制臺輸出結果)。您必須實現包含單個異步Execute()方法的Quartz接口IJob。請注意,這里我們使用依賴注入將日志記錄器注入到構造函數中。
using Microsoft.Extensions.Logging; using Quartz; using System; using System.Threading.Tasks; namespace QuartzHostedService {[DisallowConcurrentExecution]public class HelloWorldJob : IJob{private readonly ILogger<HelloWorldJob> _logger;public HelloWorldJob(ILogger<HelloWorldJob> logger){_logger = logger ?? throw new ArgumentNullException(nameof(logger));}public Task Execute(IJobExecutionContext context){_logger.LogInformation("Hello world by yilezhu at {0}!",DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));return Task.CompletedTask;}} }我還用[DisallowConcurrentExecution]屬性裝飾了該作業。該屬性可防止Quartz.NET嘗試同時運行同一作業。
創建一個IJobFactory
接下來,我們需要告訴Quartz如何創建IJob的實例。默認情況下,Quartz將使用Activator.CreateInstance創建作業實例,從而有效的調用new HelloWorldJob()。不幸的是,由于我們使用構造函數注入,因此無法正常工作。相反,我們可以提供一個自定義的IJobFactory掛鉤到ASP.NET Core依賴項注入容器(IServiceProvider)中:
using Microsoft.Extensions.DependencyInjection; using Quartz; using Quartz.Spi; using System; namespace QuartzHostedService {public class SingletonJobFactory : IJobFactory{private readonly IServiceProvider _serviceProvider;public SingletonJobFactory(IServiceProvider serviceProvider){_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));}public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler){return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;}public void ReturnJob(IJob job){}} }該工廠將一個IServiceProvider傳入構造函數中,并實現IJobFactory接口。這里最重要的方法是NewJob()方法。在這個方法中工廠必須返回Quartz調度程序所請求的IJob。在此實現中,我們直接委托給IServiceProvider,并讓DI容器找到所需的實例。由于GetRequiredService的非泛型版本返回的是一個對象,因此我們必須在末尾將其強制轉換成IJob。
該ReturnJob方法是調度程序嘗試返回(即銷毀)工廠創建的作業的地方。不幸的是,使用內置的IServiceProvider沒有這樣做的機制。我們無法創建適合Quartz API所需的新的IScopeService,因此我們只能創建單例作業。
這個很重要。使用上述實現,僅對創建單例(或瞬態)的IJob實現是安全的。
配置作業
我在IJob這里僅顯示一個實現,但是我們希望Quartz托管服務是適用于任何數量作業的通用實現。為了解決這個問題,我們創建了一個簡單的DTO JobSchedule,用于定義給定作業類型的計時器計劃:
using System; using System.ComponentModel; namespace QuartzHostedService {/// <summary>/// Job調度中間對象/// </summary>public class JobSchedule{public JobSchedule(Type jobType, string cronExpression){this.JobType = jobType ?? throw new ArgumentNullException(nameof(jobType));CronExpression = cronExpression ?? throw new ArgumentNullException(nameof(cronExpression));}/// <summary>/// Job類型/// </summary>public Type JobType { get; private set; }/// <summary>/// Cron表達式/// </summary>public string CronExpression { get; private set; }/// <summary>/// Job狀態/// </summary>public JobStatus JobStatu { get; set; } = JobStatus.Init;}/// <summary>/// Job運行狀態/// </summary>public enum JobStatus:byte{[Description("初始化")]Init=0,[Description("運行中")]Running=1,[Description("調度中")]Scheduling = 2,[Description("已停止")]Stopped = 3,} }這里的JobType是該作業的.NET類型(在我們的例子中就是HelloWorldJob),并且CronExpression是一個Quartz.NET的Cron表達。Cron表達式允許復雜的計時器調度,因此您可以設置下面復雜的規則,例如“每月5號和20號在上午8點至10點之間每半小時觸發一次”。只需確保檢查文檔即可,因為并非所有操作系統所使用的Cron表達式都是可以互換的。
我們將作業添加到DI并在Startup.ConfigureServices()中配置其時間表:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Quartz; using Quartz.Impl; using Quartz.Spi; namespace QuartzHostedService {public class Startup{public void ConfigureServices(IServiceCollection services){//添加Quartz服務services.AddSingleton<IJobFactory, SingletonJobFactory>();services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();//添加我們的Jobservices.AddSingleton<HelloWorldJob>();services.AddSingleton(new JobSchedule(jobType: typeof(HelloWorldJob), cronExpression: "0/5 * * * * ?"));}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){......}} }此代碼將四個內容作為單例添加到DI容器:
SingletonJobFactory?是前面介紹的,用于創建作業實例。
一個ISchedulerFactory的實現,使用內置的StdSchedulerFactory,它可以處理調度和管理作業
該HelloWorldJob作業本身
一個類型為HelloWorldJob,并包含一個五秒鐘運行一次的Cron表達式的JobSchedule的實例化對象。
現在我們已經完成了大部分基礎工作,只缺少一個將他們組合在一起的、QuartzHostedService了。
創建QuartzHostedService
該QuartzHostedService是IHostedService的一個實現,設置了Quartz調度程序,并且啟用它并在后臺運行。由于Quartz的設計,我們可以在IHostedService中直接實現它,而不是從基BackgroundService類派生更常見的方法。該服務的完整代碼在下面列出,稍后我將對其進行詳細描述。
using Microsoft.Extensions.Hosting; using Quartz; using Quartz.Spi; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace QuartzHostedService {public class QuartzHostedService : IHostedService{private readonly ISchedulerFactory _schedulerFactory;private readonly IJobFactory _jobFactory;private readonly IEnumerable<JobSchedule> _jobSchedules;public QuartzHostedService(ISchedulerFactory schedulerFactory, IJobFactory jobFactory, IEnumerable<JobSchedule> jobSchedules){_schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));_jobFactory = jobFactory ?? throw new ArgumentNullException(nameof(jobFactory));_jobSchedules = jobSchedules ?? throw new ArgumentNullException(nameof(jobSchedules));}public IScheduler Scheduler { get; set; }public async Task StartAsync(CancellationToken cancellationToken){Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);Scheduler.JobFactory = _jobFactory;foreach (var jobSchedule in _jobSchedules){var job = CreateJob(jobSchedule);var trigger = CreateTrigger(jobSchedule);await Scheduler.ScheduleJob(job, trigger, cancellationToken);jobSchedule.JobStatu = JobStatus.Scheduling;}await Scheduler.Start(cancellationToken);foreach (var jobSchedule in _jobSchedules){jobSchedule.JobStatu = JobStatus.Running;}}public async Task StopAsync(CancellationToken cancellationToken){await Scheduler?.Shutdown(cancellationToken);foreach (var jobSchedule in _jobSchedules){jobSchedule.JobStatu = JobStatus.Stopped;}}private static IJobDetail CreateJob(JobSchedule schedule){var jobType = schedule.JobType;return JobBuilder.Create(jobType).WithIdentity(jobType.FullName).WithDescription(jobType.Name).Build();}private static ITrigger CreateTrigger(JobSchedule schedule){return TriggerBuilder.Create().WithIdentity($"{schedule.JobType.FullName}.trigger").WithCronSchedule(schedule.CronExpression).WithDescription(schedule.CronExpression).Build();}} }該QuartzHostedService有三個依存依賴項:我們在Startup中配置的ISchedulerFactory和IJobFactory,還有一個就是IEnumerable<JobSchedule>。我們僅向DI容器中添加了一個JobSchedule對象(即HelloWorldJob),但是如果您在DI容器中注冊更多的工作計劃,它們將全部注入此處(當然,你也可以通過數據庫來進行獲取,再加以UI控制,是不是就實現了一個可視化的后臺調度了呢?自己想象吧~)。
StartAsync方法將在應用程序啟動時被調用,因此這里就是我們配置Quartz的地方。我們首先一個IScheduler的實例,將其分配給屬性以供后面使用,然后將注入的JobFactory實例設置給調度程序:
public async Task StartAsync(CancellationToken cancellationToken){Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);Scheduler.JobFactory = _jobFactory;...}接下來,我們循環注入作業計劃,并為每一個作業使用在類的結尾處定義的CreateJob和CreateTrigger輔助方法在創建一個Quartz的IJobDetail和ITrigger。如果您不喜歡這部分的工作方式,或者需要對配置進行更多控制,則可以通過按需擴展JobScheduleDTO 來輕松自定義它。
public async Task StartAsync(CancellationToken cancellationToken) {// ...foreach (var jobSchedule in _jobSchedules){var job = CreateJob(jobSchedule);var trigger = CreateTrigger(jobSchedule);await Scheduler.ScheduleJob(job, trigger, cancellationToken);jobSchedule.JobStatu = JobStatus.Scheduling;}// ... } private static IJobDetail CreateJob(JobSchedule schedule) {var jobType = schedule.JobType;return JobBuilder.Create(jobType).WithIdentity(jobType.FullName).WithDescription(jobType.Name).Build(); } private static ITrigger CreateTrigger(JobSchedule schedule) {return TriggerBuilder.Create().WithIdentity($"{schedule.JobType.FullName}.trigger").WithCronSchedule(schedule.CronExpression).WithDescription(schedule.CronExpression).Build(); }最后,一旦所有作業都被安排好,您就可以調用它的Scheduler.Start()來在后臺實際開始Quartz.NET計劃程序的處理。當應用程序關閉時,框架將調用StopAsync(),此時您可以調用Scheduler.Stop()以安全地關閉調度程序進程。
public async Task StopAsync(CancellationToken cancellationToken) {await Scheduler?.Shutdown(cancellationToken); }您可以使用AddHostedService()擴展方法在托管服務Startup.ConfigureServices中注入我們的后臺服務:
public void ConfigureServices(IServiceCollection services) {// ...services.AddHostedService<QuartzHostedService>(); }如果運行該應用程序,則應該看到每隔5秒運行一次后臺任務并寫入控制臺中(或配置日志記錄的任何地方)
在作業中使用作用域服務
這篇文章中描述的實現存在一個大問題:您只能創建Singleton或Transient作業。這意味著您不能使用注冊為作用域服務的任何依賴項。例如,您將無法將EF Core的?DatabaseContext注入您的IJob實現中,因為您會遇到Captive Dependency問題。
解決這個問題也不是很難:您可以注入IServiceProvider并創建自己的作用域。例如,如果您需要在HelloWorldJob中使用作用域服務,則可以使用以下內容:
public class HelloWorldJob : IJob {// 注入DI providerprivate readonly IServiceProvider _provider;public HelloWorldJob( IServiceProvider provider){_provider = provider;}public Task Execute(IJobExecutionContext context){// 創建一個新的作用域using(var scope = _provider.CreateScope()){// 解析你的作用域服務var service = scope.ServiceProvider.GetService<IScopedService>();_logger.LogInformation("Hello world by yilezhu at {0}!",DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));}return Task.CompletedTask;} }這樣可以確保在每次運行作業時都創建一個新的作用域,因此您可以在IJob中檢索(并處理)作用域服務。糟糕的是,這樣的寫法確實有些混亂。在下一篇文章中,我將展示另一種比較優雅的實現方式,它更簡潔,有興趣的可以關注下“DotNetCore實戰”公眾號第一時間獲取更新。
總結
在這篇文章中,我介紹了Quartz.NET,并展示了如何使用它在ASP.NET Core中的IHostedService中來調度后臺作業。這篇文章中顯示的示例最適合單例或瞬時作業,這并不理想,因為使用作用域服務顯得很笨拙。在下一篇文章中,我將展示另一種比較優雅的實現方式,它更簡潔,并使得使用作用域服務更容易,有興趣的可以關注下“DotNetCore實戰”公眾號第一時間獲取更新。
首發地址:https://www.yuque.com/yilezhu/etg3w3/aspnetcore-hostservice-quartz-net-1
參考英文地址:https://andrewlock.net/creating-a-quartz-net-hosted-service-with-asp-net-core/
往期精彩回顧
【推薦】.NET Core開發實戰視頻課程?★★★
.NET Core實戰項目之CMS 第一章 入門篇-開篇及總體規劃
【.NET Core微服務實戰-統一身份認證】開篇及目錄索引
Redis基本使用及百億數據量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個接口多種實現的依賴注入與動態選擇看這篇就夠了
10個小技巧助您寫出高性能的ASP.NET Core代碼
用abp vNext快速開發Quartz.NET定時任務管理界面
現身說法:實際業務出發分析百億數據量下的多表查詢優化
關于C#異步編程你應該了解的幾點建議
C#異步編程看這篇就夠了
給我好看 您看此文用??·?秒,轉發只需1秒呦~ 好看你就點點我總結
以上是生活随笔為你收集整理的在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CDN加速小水管动态应用技巧
- 下一篇: 分享一些支持多租户的开源框架