1、介绍:Quartz.Net主要是用来做一些周期性的工作,或者定时工作。比如每天凌晨2点执行某个方法或者调用某个接口。
Quartz项目地址:https://github.com/quartz-scheduler/quartz
参考https://www.cnblogs.com/yilezhu/p/12644208.html
.Net Core可以通过HostedService很好的实现后台任务,HostedService跟随.Net Core应用启动而在后台启动。创建一个Quartz.NET的HostedService可以使用Quartz
Quartz可以通过其Cron表达式实现复杂定时规则的定时任务。
Quartz.NET有两个主要概念:
- Job。这是您要按某个特定时间表运行的后台任务。
- Scheduler。这是负责基于触发器,基于时间的计划运行作业
2、在NuGet 包管理器中安装,搜索Quartz 安装
3、创建一个IJob
我们将通过向注入的ILogger<>中写入“ hello world”来进行实现进而向控制台输出结果)。您必须实现包含单个异步Execute()方法的Quartz接口IJob。请注意,这里我们使用依赖注入将日志记录器注入到构造函数中。
/// <summary>
/// 该属性可防止Quartz.NET尝试同时运行同一作业。告诉Quartz不要同时执行给定Job定义(引用给定Job类)的多个实例
/// </summary>
[DisallowConcurrentExecution]
public class SignalRJob : IJob
{
private readonly ILogger<SignalRJob> _logger;
public SignalRJob(ILogger<SignalRJob> logger)
{
_logger = logger;
} public Task Execute(IJobExecutionContext context)
{
_logger.LogWarning("Hello world!");
return Task.CompletedTask;
}
}
4.创建一个IJobFactory
默认情况下,Quartz将使用Activator.CreateInstance创建作业实例,从而有效的调用new HelloWorldJob()。不幸的是,由于我们使用构造函数注入,因此无法正常工作。相反,我们可以提供一个自定义的IJobFactory挂钩到ASP.NET Core依赖项注入容器(IServiceProvider)中:
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实现是安全的。
5.配置作业
我在IJob这里仅显示一个实现,但是我们希望Quartz托管服务是适用于任何数量作业的通用实现。为了解决这个问题,我们创建了一个简单的DTO JobSchedule,用于定义给定作业类型的计时器计划:
/// <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()中配置其时间表
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了。
6.创建QuartzHostedService
该QuartzHostedService是IHostedService的一个实现,设置了Quartz调度程序,并且启用它并在后台运行。由于Quartz的设计,我们可以在IHostedService中直接实现它,而不是从基BackgroundService类派生更常见的方法。该服务的完整代码在下面列出,稍后我将对其进行详细描述。
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控制,是不是就实现了一个可视化的后台调度了呢?自己想象吧~)。
您可以使用AddHostedService()扩展方法在托管服务Startup.ConfigureServices中注入我们的后台服务:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHostedService<QuartzHostedService>();
}
7.在作业中使用作用域服务
这篇文章中描述的实现存在一个大问题:您只能创建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实战”公众号第一时间获取更新。
考虑到IIS服务器的回收机制,需要设置一下IIS回收
这样子,一般情况下程序池就不会被自动回收了,后台一些简单的计算线程就会正常工作