Quartz,中文是石英的意思,由此联想到石英钟,利用石英的某些特性制成的钟表。
时间这种东西可以被高精度的计时器具切分为很小份。在每份时间中,我们都可以做一些事情,而怎么安排每份时间做什么事情,就是时间调度。
因此正如上图里描述的那样,Quartz(.NET)是一个.NET开源的调度系统。它原本是流行的Java开源框架,移植到.NET就成Quartz.NET了。当然,这丝毫不影响它的强大。
随便摘几句官网对它的描述,
Quartz. NET is a full-featured, open source job scheduling system that can be used from smallest apps to large scale enterprise systems.
小到应用程序,大到企业系统都可以使用它。所以你不用担心,Quartz到底适不适用于你的程序,放心,只要是程序,都可以用它。
这边介绍说它是调度系统,那它可以用来干些什么呢?
嵌入到程序中使用,作业调度,作业执行,作业持久化,集群,监听&插件。
说这些对于初学者来讲可能扯太远了,因为初学者往往只想要简单使用它。
于我而言,只需要它完成一个定时器的功能,比如在每周的指定时间做一些操作即可。(该功能直接用定时器或者开个线程循环判断也可以实现,只需要循环判断当前时间即可,但这一点也不优雅,而且当这类任务数量庞大时,自己管理起来会很复杂,所以还是有必要学习使用Quartz)。
NuGet里搜就好了,直接安装即可。
这部分很重要!Quartz.NET是一个可配置的库。它有两种主要的配置方式(两者不冲突):
这部分很重要是官方文档说的,但是这部分实际上又没那么重要。
如果你和我一样,只是简单地使用它,完全可以不用配置,因为Quartz默认的配置就够用了,
可以直接跳到下一节示例程序。
可以用C# fluent API,或通过向包含配置键和值的调度器工厂(scheduler factory)提供NameValueCollection参数来配置调度器(scheduler)。
Fluent API是C#中的一种写法设计,
底层细节实现就不展开讲了,大家可以找网上的内容看看,不难理解的。
这种写法的优点正如其名一样——流畅的接口,
可以一口气调用一串方法,一气呵成。
写出来的代码往往是这样子:a.b(condition1).c(condition2).d(condition3)…
具体可以看下面例子。
// you can have base properties
var properties = new NameValueCollection();
// and override values via builder
IScheduler scheduler = await SchedulerBuilder.Create(properties)
// default max concurrency is 10
.UseDefaultThreadPool(x => x.MaxConcurrency = 5)
// this is the default
// .WithMisfireThreshold(TimeSpan.FromSeconds(60))
.UsePersistentStore(x =>
{
// force job data map values to be considered as strings
// prevents nasty surprises if object is accidentally serialized and then
// serialization format breaks, defaults to false
x.UseProperties = true;
x.UseClustering();
// there are other SQL providers supported too
x.UseSqlServer("my connection string");
// this requires Quartz.Serialization.Json NuGet package
x.UseJsonSerializer();
})
// job initialization plugin handles our xml reading, without it defaults are used
// requires Quartz.Plugins NuGet package
.UseXmlSchedulingConfiguration(x =>
{
x.Files = new[] { "~/quartz_jobs.xml" };
// this is the default
x.FailOnFileNotFound = true;
// this is not the default
x.FailOnSchedulingError = true;
})
.BuildScheduler();
await scheduler.Start();
下列文件用于配置属性:
可用属性的完整文档在Quartz Configuration Reference获取。
要快速上手,一个基本的quartz.config看起来是这样的:
quartz.scheduler.instanceName = MyScheduler
quartz.jobStore.type = Quartz.Simpl.RAMJobStore, Quartz
quartz.threadPool.maxConcurrency = 3
记得在VS文件属性页上设置“拷贝到输出目录”来使值“始终复制”。否则,如果配置不在构建目录中,将无法被看到。
由该配置创建的调度器具有以下特征:
Tip
事实上,如果你不想定义这些属性,那你就可以不定义它们。Quartz.NET自带健全的默认配置。
不想了解太多有关Quartz的事情,而是想直接上手使用,那看这节就够了。
甚至这节当中的日志打印也不用管。
你需要做的步骤为:
- new一个StdSchedulerFactory
- 从factory中获取一个scheduler
- 启用scheduler,即scheduler.Start()
- 创建你的作业JobDetail(这个作业就相当于你要调度执行的方法的类,该类中有个Execute方法就是你想要执行的方法)
- 创建一个触发器Trigger(给触发器做一些配置,什么时候触发)
- 将触发器和作业绑定
ok,现在你的方法就会按触发器的配置执行了。
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = await factory.GetScheduler();
await scheduler.Start();
// 周报表
IJobDetail weekReportJob1 = JobBuilder.Create<WeekReportJob>()
.WithIdentity("weekReportJob1", "reportGroup")
.Build();
ITrigger weekReportTrigger = TriggerBuilder.Create()
.WithIdentity("weekReportTrigger", "reportGroup")
.WithSchedule(CronScheduleBuilder.AtHourAndMinuteOnGivenDaysOfWeek(0, 0, DayOfWeek.Monday))
//.WithSimpleSchedule(x => x.WithIntervalInSeconds(60).RepeatForever())
//.StartNow()
.Build();
await scheduler.ScheduleJob(weekReportJob1, weekReportTrigger);
下列代码先获取了一个调度器实例,然后启动了它,接着关闭了它。
using System;
using System.Threading.Tasks;
using Quartz;
using Quartz.Impl;
namespace QuartzSampleApp
{
public class Program
{
private static async Task Main(string[] args)
{
// Grab the Scheduler instance from the Factory
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = await factory.GetScheduler();
// and start it off
await scheduler.Start();
// some sleep to show what's happening
await Task.Delay(TimeSpan.FromSeconds(10));
// and last shut down the scheduler when you are ready to close your program
await scheduler.Shutdown();
}
}
}
从Quartz 3.0起,当scheduler.Shutdown()后面没有任何代码可执行时,应用程序将会终止,因为没有任何活动的线程。若你想让调度程序在Task.Delay和ShutDown之后继续运行,应该手动阻止程序退出。
现在运行这个程序不会显示任何东西。10秒过后,程序将终止。现在让我们往控制台中添加一些日志打印吧。
LibLog可以配置为在底层使用不同的日志框架;也就是Log4Net、NLog和Serilog这类的。
当LibLog没有检测到任何其他日志框架存在时,它是静默的。我们可以配置一个自定义日志记录器提供程序,如果你还没有准备好日志框架,它只会将日志记录到控制台并显示输出。
LogProvider.SetCurrentLogProvider(new ConsoleLogProvider());
private class ConsoleLogProvider : ILogProvider
{
public Logger GetLogger(string name)
{
return (level, func, exception, parameters) =>
{
if (level >= LogLevel.Info && func != null)
{
Console.WriteLine("[" + DateTime.Now.ToLongTimeString() + "] [" + level + "] " + func(), parameters);
}
return true;
};
}
public IDisposable OpenNestedContext(string message)
{
throw new NotImplementedException();
}
public IDisposable OpenMappedContext(string key, object value, bool destructure = false)
{
throw new NotImplementedException();
}
}
现在启动这个程序时,我们应该能看到更多的信息了。
我们需要一个简单的测试作业来测试功能,接下来创建一个HelloJob输出到控制台。
public class HelloJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
await Console.Out.WriteLineAsync("Greetings from HelloJob!");
}
}
要使它产生效果,你需要在Start()方法之后、Task.Delay()之前编写它。
// define the job and tie it to our HelloJob class
IJobDetail job = JobBuilder.Create<HelloJob>()
.WithIdentity("job1", "group1")
.Build();
// Trigger the job to run now, and then repeat every 10 seconds
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("trigger1", "group1")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever())
.Build();
// Tell quartz to schedule the job using our trigger
await scheduler.ScheduleJob(job, trigger);
// You could also schedule multiple triggers for the same job with
// await scheduler.ScheduleJob(job, new List() { trigger1, trigger2 }, replace: true);
现在,完整的程序如下:
using System;
using System.Threading.Tasks;
using Quartz;
using Quartz.Impl;
using Quartz.Logging;
namespace QuartzSampleApp
{
public class Program
{
private static async Task Main(string[] args)
{
LogProvider.SetCurrentLogProvider(new ConsoleLogProvider());
// Grab the Scheduler instance from the Factory
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = await factory.GetScheduler();
// and start it off
await scheduler.Start();
// define the job and tie it to our HelloJob class
IJobDetail job = JobBuilder.Create<HelloJob>()
.WithIdentity("job1", "group1")
.Build();
// Trigger the job to run now, and then repeat every 10 seconds
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("trigger1", "group1")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(10)
.RepeatForever())
.Build();
// Tell quartz to schedule the job using our trigger
await scheduler.ScheduleJob(job, trigger);
// some sleep to show what's happening
await Task.Delay(TimeSpan.FromSeconds(60));
// and last shut down the scheduler when you are ready to close your program
await scheduler.Shutdown();
Console.WriteLine("Press any key to close the application");
Console.ReadKey();
}
// simple log provider to get something to the console
private class ConsoleLogProvider : ILogProvider
{
public Logger GetLogger(string name)
{
return (level, func, exception, parameters) =>
{
if (level >= LogLevel.Info && func != null)
{
Console.WriteLine("[" + DateTime.Now.ToLongTimeString() + "] [" + level + "] " + func(), parameters);
}
return true;
};
}
public IDisposable OpenNestedContext(string message)
{
throw new NotImplementedException();
}
public IDisposable OpenMappedContext(string key, object value, bool destructure = false)
{
throw new NotImplementedException();
}
}
}
public class HelloJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
await Console.Out.WriteLineAsync("Greetings from HelloJob!");
}
}
}
一个简单的使用示例就完成了,接下来会对其中一些细节进一步展开。
在使用调度器之前,你需要对它进行实例化(这谁又能猜到呢?)。为此,你需要一个ISchedulerFactory的实现。
一旦实例化了调度器,你就可以启动它、将它设为待机模式或者关闭它。要注意的是,一旦一个调度器被关闭了,它就不能在没有重新实例化的情况下重启。在调度器启动之前,Triggers(触发器)不会被触发(作业不会执行),在它处于暂停状态时候也一样。
使用Quartz.NET
下面是一个快速开始的代码示例,它实例化和启动一个调度器,并调度一个作业执行:
// construct a scheduler factory using defaults
StdSchedulerFactory factory = new StdSchedulerFactory();
// get a scheduler
IScheduler scheduler = await factory.GetScheduler();
await scheduler.Start();
// define the job and tie it to our HelloJob class
IJobDetail job = JobBuilder.Create<HelloJob>()
.WithIdentity("myJob", "group1")
.Build();
// Trigger the job to run now, and then every 40 seconds
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger", "group1")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(40)
.RepeatForever())
.Build();
await scheduler.ScheduleJob(job, trigger);
// You could also schedule multiple triggers for the same job with
// await scheduler.ScheduleJob(job, new List() { trigger1, trigger2 }, replace: true);
用fluent API配置调度器
你还可以使用SchedulerBuilder fluent API以代码方式配置调度器。
var sched = await SchedulerBuilder.Create()
// default max concurrency is 10
.UseDefaultThreadPool(x => x.MaxConcurrency = 5)
// this is the default
// .WithMisfireThreshold(TimeSpan.FromSeconds(60))
.UsePersistentStore(x =>
{
// force job data map values to be considered as strings
// prevents nasty surprises if object is accidentally serialized and then
// serialization format breaks, defaults to false
x.UseProperties = true;
x.UseClustering();
x.UseSqlServer("my connection string");
// this requires Quartz.Serialization.Json NuGet package
x.UseJsonSerializer();
})
// job initialization plugin handles our xml reading, without it defaults are used
// requires Quartz.Plugins NuGet package
.UseXmlSchedulingConfiguration(x =>
{
x.Files = new[] { "~/quartz_jobs.xml" };
// this is the default
x.FailOnFileNotFound = true;
// this is not the default
x.FailOnSchedulingError = true;
})
.BuildScheduler();
await scheduler.Start();
正如你所见,使用Quartz.NET并不复杂。接下来,将快速概述作业(job)和触发器,使你能够更充分理解这个例子。
下面是Quartz API的一些关键接口和类:
在本教程中,为了便于阅读,以下术语可以互换使用:
IScheduler和Scheduler、IJob和Job、IJobDetail和JobDetail、ITrigger和Trigger。
Scheduler的生命周期受其创建(通过SchedulerFactory和对其Shutdown方法的调用)限制。一旦创建了IScheduler接口,就可以用它添加、删除和列出Jobs和Triggers,以及执行其他与调度相关(scheduling-related)的操作(比如暂停触发器)。不过,在使用Start()方法启动之前,Scheduler不会实际操作任何触发器(执行作业)。
Quartz提供了“builder(构建器)”,它定义了域特定语言(Domain Specific Language,简称DSL,有时也称fluent Interface)。在前面章节中,应该看到过它的例子了,这边再展示一小段(就是fluent API吧):
// define the job and tie it to our HelloJob class
IJobDetail job = JobBuilder.Create<HelloJob>()
.WithIdentity("myJob", "group1") // name "myJob", group "group1"
.Build();
// Trigger the job to run now, and then every 40 seconds
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger", "group1")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(40)
.RepeatForever())
.Build();
// Tell quartz to schedule the job using our trigger
await sched.scheduleJob(job, trigger);
这块代码使用JobBuilder构建了作业的定义,使用Fluent Interface创建了IJobDetail。同样,构建触发器的代码也使用了专用于给定触发器类型的TriggerBuilder的fluent Interface和拓展方法。调度的拓展方法有:
DateBuilder类型包含各种方法,用于轻松构造特定时间点的DateTimeOffset实例(例如,下一个偶数小时的时间,比如现在是9:43:27,那么下个目标时间点就是10:00:00)。
Job(作业)是一个实现了IJob接口的类,它只有一个简单的方法:
// IJob Interface
namespace Quartz
{
public interface IJob
{
Task Execute(JobExecutionContext context);
}
}
当Job的触发器触发时,Execute(…)方法会被调度器的一个工作线程调用。
传递给该方法的JobExecutionContext对象向作业实例提供关于其“运行时”环境的信息——执行它的调度器的句柄、触发执行的触发器的句柄、作业的JobDetail对象以及一些其他项。
JobDetail对象在Job被添加到调度器时由Quartz.NET客户端(即你的程序)创建。它包含了Job的各种属性设置,以及一个JobDataMap,它可用于存储作业类的给定实例的状态信息。它本质上是作业实例的定义。
触发器对象用于触发作业的执行。当你希望调度一个作业时,就实例化一个触发器并“调整”它的属性以达到你想要的调度。触发器还可能有一个与之关联的JobDataMap——这对于向Job传递专用于触发器触发的参数很有用。Quartz发布了几种不同的触发器类型,其中最常用的类型是SimpleTrigger(接口ISimpleTrigger)和CronTrigger(接口ICronTrigger)。
若你想要“一次执行”(在给定时间内只执行一次作业),或者你需要给定时间内触发作业,并让它重复N次,每次执行之间的延迟为T,SimpleTrigger会很方便。
如果你希望触发基于日历的调度——比如“每周五中午”或“每个月的第十天的十点钟”。那么CronTrigger是很有效的。
为什么会有作业和触发器呢?许多作业调度器没有单独的作业和触发器的概念。有些人将“作业”定义为简单的带有一些作业标识符的执行时间(或调度)。另一些人将其定义为很像Quartz的作业与触发器对象结合体的底下。在开发Quartz的过程中,开发者们认为有必要在调度和在调度上执行的作业之间建立一个隔离。这会带来很多好处。
例如,作业能独立于触发器在作业调度器中创建并存储,并且多个触发器可以与同一个作业关联。这种松耦合(loose-coupling)的另一个好处是能够配置在相关触发器过期后仍保留在调度器中的作业,以便稍后可以重新调度它,而不必重新定义它。这也允许修改或替换触发器,而无需重新定义其相关的作业。
在向Quartz调度器注册作业和触发器时,将被提供一个标识键(identifying keys)。作业和触发器的键(JobKey和TriggerKey)允许将它们放入“groups(组)”中,这对将作业和触发器组织成“reporting jobs(报告作业)”和“maintain jobs(维护作业)”等类别很有用。作业和触发器的键的名称在组中必须是唯一的。
现在你应该对什么是作业和触发器有个大致的概念了。