30

ASP.NET Core托管运行Quartz.NET作业调度详解

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MjM5MjQwMDUzMw%3D%3D&%3Bmid=2247484089&%3Bidx=1&%3Bsn=3a5a7f9a3f1fc18278396a79d4de26ec
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.
neoserver,ios ssh client

Quartz.NET这么NB的作业调度系统,不会还行?

今天介绍一下Quartz.NET的托管运行,官网传送门。

一、前言

Quartz.NET,按官网上的说法,是一款功能齐全的任务调度系统,从小型应用到大型企业级系统都能适用。在众多项目中,Quartz.NET以可靠、集群的方式,被用作在定时器上运行后台任务的一种方式。

Quartz.NET主要完成两个方面的内容:

  1. 基于时间计划的后台作业;

  2. 基于因时间计划的触发的任务运行。

ASP.NET Core本身对于通过托管服务运行后台任务就支持的很好。当ASP.NET启动托管服务时,应用程序启动,并在生命周期内在后台运行。通过创建Quartz.NET托管服务,可以使用标准的.Net Core托管服务,在后台运行任务。

Quartz.NET可以创建定时的任务,例如每十分钟运行一个任务。除此之外,Quartz.NET还可以通过Cron触发器,定义任务在特定的日子或特定的时间运行,例如每天凌晨两点执行一个任务。它还允许以集群的方式运行应用程序的多个实例,以便在任何时间确保只有一个实例运行给定的任务。

下面,就针对这些特性和功能,进行详细的说明。

二、安装Quartz.NET

Quartz.NET提供了NuGet包,所以安装很简单:

% dotnet add package quartz

这是个司机就知道,不详说了。

看一下安装后的 .csproj 文件内容:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="quartz" Version="3.2.2" />
  </ItemGroup>
</Project>

三、通过IJob创建任务类

我们用个例子来说明 - 创建一个 Demo 的实现。它将写入 ILogger<> 。我们会使用Quartz.NET的接口 IJob 来实现,并使用依赖注入将日志注入到构造函数中。

[DisallowConcurrentExecution]
public class DemoJob : IJob
{
    private readonly ILogger<DemoJob> _logger;
    public DemoJob(ILogger<DemoJob> logger)
    {
        _logger = logger;
    }

    public Task Execute(IJobExecutionContext context)
    {
        _logger.LogInformation("Demo !");
        return Task.CompletedTask;
    }
}

在类的前面,我用了一个 DisallowConcurrentExecution 属性。这个属性可以防止Quartz.NET同时运行相同的作业。

四、通过IJobFactory创建任务工厂

通常情况下,Quartz.NET会使用 Activator.CreateInstance 来创建作业的实例。

在我们这个例子里,我们换一种方式。使用 IJobFactory 实现,并与ASP.NET Core的依赖注入容器挂钩。

public class SingletonJobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;
    public SingletonJobFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
    }

    public void ReturnJob(IJob job)
    {
    }
}

这个 IJobFactory 的实现,在构造函数中引入 IServiceProvider ,并实现接口。

接口中,最重要的是 NewJob() 方法。这个方法需要返回Quartz.NET调度器请求的 IJob 。在我们的例子中,我们直接委托给 IServiceProvider ,并让DI容器找到所需的实例。

ReturnJob() 方法是调度程序返回和销毁 IJobFactory 创建的作业的地方。不过,因为我们使用了 IServiceProvider ,而它没有提供这样的处理。所以,从安全的角度,应该使用单例作业。

五、配置作业

在第三节中,我们创建了一个 IJob 的实现。这个实现直接使用就可以。

但是,我们这儿要加点内容。我们把 Quartz 的托管服务做成一个通用实现,来调度任意的作业。因此,我们创建一个简单的DTO,并使用它来定义一个给定作业类型的时间器调度:

public class JobSchedule
{
    public JobSchedule(Type jobType, string cronExpression)
    {
        JobType = jobType;
        CronExpression = cronExpression;
    }

    public Type JobType { get; }
    public string CronExpression { get; }
}

在这个类中, JobType 是一个作业的类型,例如本例子中的 DemoJobCronExpression 是一个 Cron 的表达式。

Cron表达式允许复杂的计时器调度,所以可以设置规则,比如每个月的5号和20号,在早上8点到10点之间每半小时触发一次。

关于 Quartz.NETCron 表达式,可以在这儿查到。

注意:不同操作系统使用的Cron表达式有一定的区别,写表达式的时候一定要注意这一点。

然后,我们把作业添加到 Startup.ConfigureServices() 中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddSingleton<IJobFactory, SingletonJobFactory>();
    services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();

    services.AddSingleton<DemoJob>();
    services.AddSingleton(new JobSchedule(
        jobType: typeof(DemoJob),
        cronExpression: "0/5 * * * * ?")); // 每5秒运行一次
}

这段代码向DI添加了四个单例对象:

  1. SingletonJobFactory,第四节的类,用于创建作业实例;

  2. ISchedulerFactory的一个实现,是内置的StdSchedulerFactory,用于处理调度和管理作业;

  3. DemoJob作业本身;

  4. DemoJob的一个JobSchedule实例,该实例具有每5秒运行一次的Cron表达式。

现在,主要代码已经完成,就差 Quartz 托管服务了。

六、创建Quartz托管服务

QuartzHostedService 是自己创建的 Quartz 托管服务,继承于 IHostedService ,用于设置 Quartz 调度器,并在后台启动它。

先看一下完整的代码:

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;
        _jobSchedules = jobSchedules;
        _jobFactory = jobFactory;
    }

    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);
        }

        await Scheduler.Start(cancellationToken);
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await Scheduler?.Shutdown(cancellationToken);
    }

    private ITrigger CreateTrigger(JobSchedule schedule)
    {
        return TriggerBuilder
        .Create()
        .WithIdentity($"{schedule.JobType.FullName}.trigger")
        .WithCronSchedule(schedule.CronExpression)
        .WithDescription(schedule.CronExpression)
        .Build();
    }

    private IJobDetail CreateJob(JobSchedule schedule)
    {
        var jobType = schedule.JobType;
        return JobBuilder
            .Create(jobType)
            .WithIdentity(jobType.FullName)
            .WithDescription(jobType.Name)
            .Build();
    }
}

解释一下这段代码:

这段代码中, QuartzHostedService 有三个依赖项: Startup.ConfigureServices() 中注入的 ISchedulerFactoryIJobFactory ,以及一个 IEnumerable 。在第五节的代码中,我们只向DI添加了一个 JobSchedule ,就是 DemoJob 。我们也可以添加多个 JobSchedule ,他们都会在这个 IEnumerable 中被注入到托管服务中。

StartAsync 在应用程序启动时被调用,它是我们配置 Quartz 的地方。我们首先创建 IScheduler 的一个实例,为它分配一个属性供以后使用,并将调度程序的 JobFactory 设置为注入的实例:

public async Task StartAsync(CancellationToken cancellationToken)
{
    Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
    Scheduler.JobFactory = _jobFactory; 
    //... 
}

然后,循环注入的作业调度,并在类的最后使用 CreateJobCreateTrigger 方法为每个作业创建一个 IJobDetailITrigger 。实际应用中如果有别的需要,也可以通过扩展 JobSchedule DTO来定制它。

最后,在调度了所有作业之后,调用 Scheduler.Start() 来实际在后台启动Quartz.NET调度器。当应用程序关闭时,框架将调用 StopAsync() ,此时可以调用 Scheduler.Shutdown() 来安全地关闭调度程序进程。

全部完成后,我们启动 QuartzHostedService

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddHostedService<QuartzHostedService>();
}

运行程序,可以看到结果:

demo.DemoJob: Information: Demo !
info: demo.DemoJob[0]
      Demo !
demo.DemoJob: Information: Demo !
info: demo.DemoJob[0]
      Demo !
demo.DemoJob: Information: Demo !
info: demo.DemoJob[0]
      Demo !
demo.DemoJob: Information: Demo !
info: demo.DemoJob[0]
      Demo !

本文的代码,在https://github.com/humornif/Demo-Code/tree/master/0029/demo

喜欢就来个三连,让更多人因你而受益


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK