根据他们的网站:
Quartz.NET是功能齐全的开源作业调度系统,可用于最小的应用程序到大型企业系统。
它是许多ASP.NET开发人员的主要资料,被用作以可靠的群集方式在计时器上运行后台任务的方式。将Quartz.NET与ASP.NET Core一起使用非常相似-Quartz.NET支持.NET Standard 2.0,因此您可以轻松地在应用程序中使用它。
“通用主机”也可以使用这种非HTTP方案,但是由于种种原因,我目前通常不使用那些。希望通过在这些非HTTP方案中进行额外的投资,可以在ASP.NET Core 3.0中改善这一点。
虽然可以创建“定时”后台服务(例如,每10分钟运行一次任务),但Quartz.NET提供了更为强大的解决方案。通过使用Cron触发器,您可以确保任务仅在一天的特定时间(例如,凌晨2:30)运行,或仅在特定的几天运行,或任意组合运行。它还允许您以集群方式运行应用程序的多个实例,以便在任何时候只能运行一个实例。
在本文中,我将介绍创建Quartz.NET作业并将其调度为在托管服务中的计时器上运行的基础知识。
Quartz.NET是一个.NET Standard 2.0 NuGet软件包,因此应该易于安装在您的应用程序中。对于此测试,我创建了一个ASP.NET Core项目并选择了Empty模板。您可以使用安装Quartz.NET软件包dotnet add package Quartz
。如果您查看该项目的.csproj,它应该看起来像这样:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Quartz" Version="3.0.7" />
</ItemGroup>
</Project>
对于我们正在安排的实际后台工作,我们将使用“ hello world”实现,该实现写入并ILogger<>
(进而写入控制台)。您应该实现IJob
包含单个异步Execute()
方法的Quartz接口。请注意,这里我们使用依赖注入将记录器注入到构造函数中。
using Microsoft.Extensions.Logging;
using Quartz;
using System.Threading.Tasks;
[DisallowConcurrentExecution]
public class HelloWorldJob : IJob
{
private readonly ILogger<HelloWorldJob> _logger;
public HelloWorldJob(ILogger<HelloWorldJob> logger)
{
_logger = logger;
}
public Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Hello world!");
return Task.CompletedTask;
}
}
我还用[DisallowConcurrentExecution]
属性装饰了工作。该属性可防止Quartz.NET尝试同时运行同一作业。
DisallowConcurrentExecution
- 此标记用在实现Job的类上面,意思是不允许并发执行,按照我之前的理解是 不允许调度框架在同一时刻调用Job类,后来经过测试发现并不是这样,而是Job(任务)的执行时间[比如需要10秒]大于任务的时间间隔[Interval(5秒)],那么默认情况下,调度框架为了能让
任务按照我们预定的时间间隔执行,会马上启用新的线程执行任务。否则的话会等待任务执行完毕以后
再重新执行!(这样会导致任务的执行不是按照我们预先定义的时间间隔执行)- 测试代码,这是官方提供的例子。设定的时间间隔为3秒,但job执行时间是5秒,设置@DisallowConcurrentExecution以后程序会等任务执行完毕以后再去执行,否则会在3秒时再启用新的线程执行
接下来,我们需要告诉Quartz如何创建的实例IJob
。默认情况下,Quartz将使用Activator.CreateInstance
有效地调用尝试并“更新”作业实例new HelloWorldJob()
。不幸的是,由于我们正在使用构造函数注入,因此无法正常工作。相反,我们可以提供一个IJobFactory
挂钩到ASP.NET Core依赖项注入容器(IServiceProvider)
的自定义:
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;
using System;
public class SingletonJobFactory : IJobFactory
{
private readonly IServiceScope _serviceScope;
public SingletonJobFactory(IServiceProvider serviceProvider)
{
_serviceScope = serviceProvider.CreateScope();
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceScope.ServiceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job)
{
(job as IDisposable)?.Dispose();
}
}
该工厂将一个IServiceProviderin
作为构造函数,并实现IJobFactory
接口。重要的方法是NewJob()
工厂必须返回IJobQuartz
调度程序所请求的方法。在此实现中,我们直接委派给IServiceProvider
,并让容器找到所需的实例。由于IJob
必须在非通用版本中GetRequiredService
返回,因此必须在结尾处强制转换为object
。
该ReturnJob
方法是调度程序尝试返回(即销毁)工厂创建的作业的地方。不幸的是,没有内置的机制可以做到这一点IServiceProvider
。我们无法创建IScopeService
适合所需的Quartz API的新产品,因此我们只能创建单例作业。
这个很重要。使用上述实现,仅创建Singletons(或transient)的
IJob
实现是安全的。
我在IJob
这里仅显示一个实现,但是我们希望Quartz托管服务是适用于任何数量作业的通用实现。为了解决这个问题,我们创建了一个简单的DTO JobSchedule
,用于定义给定作业类型的计时器计划:
using System;
public class JobSchedule
{
public JobSchedule(Type jobType, string cronExpression)
{
JobType = jobType;
CronExpression = cronExpression;
}
public Type JobType { get; }
public string CronExpression { get; }
}
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 Quartz;
using Quartz.Impl;
using Quartz.Spi;
public void ConfigureServices(IServiceCollection services)
{
// Add Quartz services
services.AddSingleton<IJobFactory, SingletonJobFactory>();
// Add our job
services.AddSingleton<HelloWorldJob>();
services.AddSingleton(new JobSchedule(
jobType: typeof(HelloWorldJob),
cronExpression: "0/5 * * * * ?")); // run every 5 seconds
}
此代码将四个内容作为单例添加到DI容器:
SingletonJobFactory
前面所示,用于创建工作情况。ISchedulerFactory
,内置的StdSchedulerFactory
,它可以处理调度和管理作业HelloWorldJob
工作本身JobSchedule
为HelloWorldJob
与Cron表达式每5秒运行一次。现在只需要将它们组合在一起QuartzHostedService
。
该QuartzHostedService
是的实现IHostedService
,设置了Quartz调度程序,并启动它在后台运行。由于Quartz的设计,我们可以IHostedService
直接实现,而不是基于BackgroundService
类派生的更常见的方法。该服务的完整代码在下面列出,稍后我将讨论。
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.Spi;
public class QuartzHostedService : IHostedService
{
private readonly IJobFactory _jobFactory;
private readonly IEnumerable<JobSchedule> _jobSchedules;
public QuartzHostedService(IJobFactory jobFactory,
IEnumerable<JobSchedule> jobSchedules)
{
_jobSchedules = jobSchedules;
_jobFactory = jobFactory;
}
public IScheduler Scheduler { get; set; }
public async Task StartAsync(CancellationToken cancellationToken)
{
Scheduler = await StdSchedulerFactory.GetDefaultScheduler(cancellationToken);
Scheduler.JobFactory = _jobFactory;
foreach (var jobSchedule in _jobSchedules)
{
var job = CreateJob(jobSchedule);
var trigger = CreateTrigger(jobSchedule);
await Scheduler.ScheduleJob(job, trigger, cancellationToken);
}
await Scheduler.Start(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Scheduler?.Shutdown(cancellationToken);
}
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();
}
}
您可以使用AddHostedService()
扩展方法在中注册托管服务Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHostedService<QuartzHostedService>();
}