在ASP.NET Core中建立基於Quartz.NET託管服務輕鬆實現做業調度

在這篇文章中,我將介紹如何使用ASP.NET Core託管服務運行Quartz.NET做業。這樣的好處是咱們能夠在應用程序啓動和中止時很方便的來控制咱們的Job的運行狀態。接下來我將演示如何建立一個簡單的 IJob,一個自定義的 IJobFactory和一個在應用程序運行時就開始運行的QuartzHostedService。我還將介紹一些須要注意的問題,即在單例類中使用做用域服務。html

做者:依樂祝數據庫

首發地址:http://www.javashuo.com/article/p-wewyzyaj-ep.html安全

參考英文地址:https://andrewlock.net/creating-a-quartz-net-hosted-service-with-asp-net-core/app

簡介-什麼是Quartz.NET?

在開始介紹什麼是Quartz.NET前先看一下下面這個圖,這個圖基本歸納了Quartz.NET的全部核心內容。框架

注:此圖爲百度上獲取,旨在學習交流使用,若有侵權,聯繫後刪除。異步

Quartz.NET

如下來自他們的網站的描述:async

Quartz.NET是功能齊全的開源做業調度系統,適用於從最小型的應用程序到大型企業系統。ide

對於許多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>();
            //添加咱們的Job
            services.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

QuartzHostedServiceIHostedService的一個實現,設置了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中配置的ISchedulerFactoryIJobFactory,還有一個就是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;
            ...
        }

接下來,咱們循環注入做業計劃,併爲每個做業使用在類的結尾處定義的CreateJobCreateTrigger輔助方法在建立一個Quartz的IJobDetailITrigger。若是您不喜歡這部分的工做方式,或者須要對配置進行更多控制,則能夠經過按需擴展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秒運行一次後臺任務並寫入控制檯中(或配置日誌記錄的任何地方)

image-20200406151153107

在做業中使用做用域服務

這篇文章中描述的實現存在一個大問題:您只能建立Singleton或Transient做業。這意味着您不能使用註冊爲做用域服務的任何依賴項。例如,您將沒法將EF Core的 DatabaseContext注入您的IJob實現中,由於您會遇到Captive Dependency問題。

解決這個問題也不是很難:您能夠注入IServiceProvider並建立本身的做用域。例如,若是您須要在HelloWorldJob中使用做用域服務,則可使用如下內容:

public class HelloWorldJob : IJob
{
    // 注入DI provider
    private 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實戰」公衆號第一時間獲取更新。

相關文章
相關標籤/搜索