8

在 ASP.NET Core和Worker Service中使用Quartz.Net

 3 years ago
source link: https://segmentfault.com/a/1190000038753018
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在 ASP.NET Core和Worker Service中使用Quartz.Net

现在有了一个官方包Quartz.Extensions.Hosting实现使用Quartz.Net运行后台任务,所以把Quartz.Net添加到ASP.NET Core或Worker Service要简单得多。

我将展示如何把Quartz.Net HostedService添加到你的应用,如何创建一个简单的IJob,以及如何注册它与trigger。

简介——什么是Quartz.Net

Quartz.Net是一个功能齐全的开源作业调度系统,可以在最小规模的应用程序到大型企业系统使用。

有许多ASP.NET的钉子户,他们以一种可靠的、集群的方式在定时器上运行后台任务。使用在ASP.NET Core中使用的Quartz.Net支持了.NET Standar 2.0,因此你可以轻松地在应用程序中使用它。

Quartz.Net有三个主要概念:

  1. job。这是你想要运行的后台任务。
  2. trigger。trigger控制job何时运行,通常按某种调度规则触发。
  3. scheduler。它负责协调job和trigger,根据trigger的要求执行job。

ASP.NET Core很好地支持通过hosted services(托管服务)运行“后台任务”。当你的ASP.NET Core应用程序启动,托管服务也启动,并在应用程序的生命周期中在后台运行。Quartz.Net 3.2.0通过Quartz.Extensions.Hosting引入了对该模式的直接支持。Quartz.Extensions.Hosting即可以用在ASP.NET Core应用程序,也可以用在基于“通用主机”的Worker Service。

虽然可以创建一个“定时”后台服务(例如,每10分钟运行一个任务),但Quartz.NET提供了一个更加健壮的解决方案。通过使用Cron trigger,你可以确保任务只在一天的特定时间(例如凌晨2:30)运行,或者只在特定的日子运行,或者这些时间的任意组合运行。Quartz.Net还允许你以集群的方式运行应用程序的多个实例,以便在任何时候只有一个实例可以运行给定的任务。

Quartz.Net托管服务负责Quartz的调度。它将在应用程序的后台运行,检查正在执行的触发器,并在必要时运行相关的作业。你需要配置调度程序,但不需要担心启动或停止它,IHostedService会为你管理。

在这篇文章中,我将展示创建Quartz.Net job的基础知识。并将其调度到托管服务中的定时器上运行。

安装Quartz.Net

Quartz.Net是一个.NET Standar 2.0的NuGet包,所以它很容易安装在你的应用程序中。对于这个测试,我创建了一个Worker Service项目。你可以通过使用dotnet add package Quartz.Extensions.Hosting命令安装Quartz.Net托管包。如果你查看项目的.csproj,它应该是这样的:

<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>dotnet-QuartzWorkerService-9D4BFFBE-BE06-4490-AE8B-8AF1466778FD</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
    <PackageReference Include="Quartz.Extensions.Hosting" Version="3.2.3" />
  </ItemGroup>
</Project>

这将添加托管服务包,从而引入Quartz.Net。接下来,我们需要在应用程序中注册Quartz.Net的服务和 IHostedService。

添加Quartz.Net托管服务

注册Quartz.Net需要做两件事:

  1. 注册Quartz.Net需要的DI容器服务。
  2. 注册托管服务。

在ASP.NET Core中,通常会在Startup.ConfigureServices()方法中完成这两项操作。Worker Services不使用Startup类,所以我们在Program.cs中的IHostBuilder的ConfigureServices方法中注册它们:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                // Add the required Quartz.NET services
                services.AddQuartz(q =>
                {
                    // Use a Scoped container to create jobs. I'll touch on this later
                    q.UseMicrosoftDependencyInjectionScopedJobFactory();
                });

                // Add the Quartz.NET hosted service

                services.AddQuartzHostedService(
                    q => q.WaitForJobsToComplete = true);

                // other config
            });
}

这里有几个有趣的点:

  1. UseMicrosoftDependencyInjectionScopedJobFactory:它告诉Quartz.NET注册一个IJobFactory,该IJobFactory通过从DI容器中创建job。方法中的Scoped部分意味着你的作业可以使用scoped服务,而不仅仅是single或transient服务。
  2. WaitForJobsToComplete:此设置确保当请求关闭时,Quartz.NET在退出之前等待作业优雅地结束。

如果你现在运行应用程序,将看到Quartz服务启动,并将大量日志转储到控制台:

info: Quartz.Core.SchedulerSignalerImpl[0]
      Initialized Scheduler Signaller of type: Quartz.Core.SchedulerSignalerImpl
info: Quartz.Core.QuartzScheduler[0]
      Quartz Scheduler v.3.2.3.0 created.
info: Quartz.Core.QuartzScheduler[0]
      JobFactory set to: Quartz.Simpl.MicrosoftDependencyInjectionJobFactory
info: Quartz.Simpl.RAMJobStore[0]
      RAMJobStore initialized.
info: Quartz.Core.QuartzScheduler[0]
      Scheduler meta-data: Quartz Scheduler (v3.2.3.0) 'QuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'Quartz.Core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'Quartz.Simpl.DefaultThreadPool' - with 10 threads.
  Using job-store 'Quartz.Simpl.RAMJobStore' - which does not support persistence. and is not clustered.

info: Quartz.Impl.StdSchedulerFactory[0]
      Quartz scheduler 'QuartzScheduler' initialized
info: Quartz.Impl.StdSchedulerFactory[0]
      Quartz scheduler version: 3.2.3.0
info: Quartz.Core.QuartzScheduler[0]
      Scheduler QuartzScheduler_$_NON_CLUSTERED started.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down....

此时,你已经让Quartz作为托管服务在你的应用程序中运行,但是没有任何job让它运行。在下一节中,我们将创建并注册一个简单的job。

创建job

对于我们正在调度的实际后台工作,我们将使用一个"hello world"实现它写入一个ILogger<T>。你应该实现Quartz.NET的接口IJob,它包含一个异步的Execute()方法。注意,我们在这里使用依赖注入将日志程序注入到构造函数中。

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]属性装饰了job。此属性防止Quartz.NET试图同时运行相同的作业。

现在我们已经创建了作业,我们需要将它与trigger一起注册到DI容器中。

配置job

Quartz.NET为运行job提供了一些简单的schedule,但最常见的方法之一是使用Quartz.NET Cron表达式。Cron表达式允许复杂的计时器调度,所以你可以设置规则,比如“每个月的5号和20号,在早上8点到10点之间每半小时触发一次”。使用时请确保检查示例文档,因为不同系统使用的所有Cron表达式都是可互换的。

下面的示例展示了如何使用每5秒运行一次的trggier来注册HelloWorldJob:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddQuartz(q =>  
            {
                q.UseMicrosoftDependencyInjectionScopedJobFactory();

                // Create a "key" for the job
                var jobKey = new JobKey("HelloWorldJob");

                // Register the job with the DI container
                q.AddJob<HelloWorldJob>(opts => opts.WithIdentity(jobKey));

                // Create a trigger for the job
                q.AddTrigger(opts => opts
                    .ForJob(jobKey) // link to the HelloWorldJob
                    .WithIdentity("HelloWorldJob-trigger") // give the trigger a unique name
                    .WithCronSchedule("0/5 * * * * ?")); // run every 5 seconds

            });
            services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
            // ...
        });

在本代码中,我们:

  1. 为job创建唯一的JobKey。这用于将job与其trggier连接在一起。还有其他连接job和trggier的方法,但我认为这和其他方法一样好。
  2. 用AddJob<T>注册HelloWorldJob。这做了两件事—它将HelloWorldJob添加到DI容器中,这样就可以创建它;它在内部向Quartz注册job。
  3. 添加一个触发器,每5秒运行一次作业。我们使用JobKey将trigger与一个job关联起来,并为trigger提供唯一的名称(在本例中不是必需的,但如果你在集群模式下运行quartz,这很重要)。最后,我们为trigger设置了Cron调度,使作业每5秒运行一次。

这就实现了功能!不再需要创建自定义的IJobFactory,也不用担心是否支持scoped的服务。

默认的包为你处理所有这些问题——你可以在IJob中使用scoped的服务,它们将在job完成时被删除。

如果你现在运行你的应用程序,你会看到和以前一样的启动消息,然后每5秒你会看到HelloWorldJob写入控制台:
image
这就是启动和运行所需的全部内容,但是根据我的喜好,在ConfigureServices方法中添加了太多内容。你也不太可能想在应用程序中硬编码作业调度。例如,如果将其提取到配置中,可以在每个环境中使用不同的调度。

最起码,我们希望将Cron调度提取到配置中。例如,你可以在appsettings.json中添加以下内容:

{
  "Quartz": {
     "HelloWorldJob": "0/5 * * * * ?"
   }
 }

然后,你可以轻松地在不同环境中覆盖HelloWorldJob的触发器调度。

为了方便注册,我们可以创建一个扩展方法来封装在Quartz上注册IJob,并设置它的trigger调度。这段代码与前面的示例基本相同,但是它使用job的名称在IConfiguration中加载Cron调度。

public static class ServiceCollectionQuartzConfiguratorExtensions{
    public static void AddJobAndTrigger<T>(
        this IServiceCollectionQuartzConfigurator quartz,
        IConfiguration config)
        where T : IJob
    {
        // Use the name of the IJob as the appsettings.json key
        string jobName = typeof(T).Name;

        // Try and load the schedule from configuration
        var configKey = $"Quartz:{jobName}";
        var cronSchedule = config[configKey];

        // Some minor validation
        if (string.IsNullOrEmpty(cronSchedule))
        {
            throw new Exception($"No Quartz.NET Cron schedule found for job in configuration at {configKey}");
        }

        // register the job as before
        var jobKey = new JobKey(jobName);
        quartz.AddJob<T>(opts => opts.WithIdentity(jobKey));

        quartz.AddTrigger(opts => opts
            .ForJob(jobKey)
            .WithIdentity(jobName + "-trigger")
            .WithCronSchedule(cronSchedule)); // use the schedule from configuration
    }
}

现在我们可以使用扩展方法清理应用程序的Program.cs:

public class Program
{
    public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddQuartz(q =>
                {
                    q.UseMicrosoftDependencyInjectionScopedJobFactory();

                    // Register the job, loading the schedule from configuration
                    q.AddJobAndTrigger<HelloWorldJob>(hostContext.Configuration);
                });

                services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
            });
}

这本质上与我们的配置相同,但是我们已经使添加新job和调度的细节移到配置中变得更容易。

再次运行应用程序会给出相同的输出:job每5秒写一次输出。

image

总结

在这篇文章中,我介绍了Quartz.NET并展示了如何使用新的Quartz.Extensions.Hosting轻松添加一个ASP.NET Core托管服务运行Quartz调度器。我展示了如何使用trigger实现一个简单的job,以及如何将其注册到应用程序中,以便托管的服务按计划运行它。

 欢迎关注我的公众号,如果你有喜欢的外文技术文章,可以通过公众号留言推荐给我。

image


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK