在这篇文章中,我将介绍如何使用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的全部核心内容。框架
注:此图为百度上获取,旨在学习交流使用,若有侵权,联系后删除。异步
如下来自他们的网站的描述:async
Quartz.NET是功能齐全的开源做业调度系统,适用于从最小型的应用程序到大型企业系统。ide
对于许多ASP.NET开发人员来讲它是首选,用做在计时器上以可靠、集群的方式运行后台任务的方法。将Quartz.NET与ASP.NET Core一块儿使用也很是类似-由于Quartz.NET支持.NET Standard 2.0,所以您能够轻松地在应用程序中使用它。函数
Quartz.NET有两个主要概念:学习
ASP.NET Core经过托管服务对运行“后台任务”具备良好的支持。托管服务在ASP.NET Core应用程序启动时启动,并在应用程序生命周期内在后台运行。经过建立Quartz.NET托管服务,您可使用标准ASP.NET Core应用程序在后台运行任务。
虽然能够建立“定时”后台服务(例如,每10分钟运行一次任务),但Quartz.NET提供了更为强大的解决方案。经过使用Cron触发器,您能够确保任务仅在一天的特定时间(例如,凌晨2:30)运行,或仅在特定的几天运行,或任意组合运行。它还容许您以集群方式运行应用程序的多个实例,以便在任什么时候候只能运行一个实例(高可用)。
在本文中,我将介绍建立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>
对于咱们正在安排的实际后台工做,咱们将经过向注入的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尝试同时运行同一做业。
接下来,咱们须要告诉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
是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
。若是您不喜欢这部分的工做方式,或者须要对配置进行更多控制,则能够经过按需扩展JobSchedule
DTO 来轻松自定义它。
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 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实战”公众号第一时间获取更新。