前几天写了一篇有关 Quartz.NET 入门的文章, 大家感觉不过瘾, 想让我在写一些比较深入的文章. 其实这个东西, 我也是刚入门, 我也想技术深入了解一下, 所以就努力看了一些资料, 然后自己再整理和翻译一些, 作为学习的历程, 就记录下来, 希望对大家有帮助.
一, 使用 Quartz(Using Quartz)
在您使用一个调度程序之前, 需要实例化一个调度程序的实例. 为此, 您需要使用一个实现了 ISchedulerFactory 接口的子类型.
一旦调度程序被实例化, 它就可以启动, 然后处于待机模式, 当然也可以关闭. 请注意, 一旦调度程序关闭, 就不能在不重新实例化的情况下重新启动它. 在调度程序启动之前, 触发器不会触发(Job 作业也不会执行), 也不会在处于暂停状态时触发.
这是一段简洁的代码片段, 用于实例化和启动调度程序, 并安排执行 Job 作业:
使用 Quartz.NET
- // construct a scheduler factory
- NameValueCollection props = new NameValueCollection
- {
- { "quartz.serializer.type", "binary" }
- };
- StdSchedulerFactory factory = new StdSchedulerFactory(props);
- // get a scheduler
- IScheduler sched = await factory.GetScheduler();
- await sched.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 sched.ScheduleJob(job, trigger);
如您所见, 使用 Quartz.NET 非常简单.
二, Job 作业和触发器(Jobs And Triggers)
1,Quartz API
Quartz API 的关键接口和类是:
IScheduler - 与调度程序交互的主要 API.
IJob - 希望由调度程序执行的作业实现的接口, 或者说 Job 将要完成的功能.
IJobDetail - 用于定义 Jobs 的实例.
ITrigger - 一个功能组件, 用于定义执行给定作业的计划.
JobBuilder - 用于定义 / 构建 JobDetail 实例, 用于定义 Jobs 的实例.
TriggerBuilder - 用于定义 / 构建触发器实例.
在本教程中, 为了便于阅读, 下面的术语可以互换使用: IScheduler 和 Scheduler,IJob 和 Job,IJobDetail 和 JobDetail,ITrigger 和 Trigger.
Scheduler 调度程序实例的生命周期因其被创建而开始, 可以通过调用 SchedulerFactory 类型 Shutdown() 方法来结束. 创建后, 可以使用 IScheduler 接口添加, 删除和列出作业和触发器, 以及执行其他与调度相关的操作(例如暂停触发器). 但是, 在使用 Start() 方法启动调度程序之前, 调度程序实际上不会对任何触发器执行(执行作业)
Quartz 提供了 "构建器(Builder)" 类, 用于定义该领域的特定语言(或 DSL, 有时也称为 "fluent interface"). 在上一节中, 您看到了一个示例, 我们再次在此处介绍其中的一部分:
- // 定义作业并将其绑定到 HelloJob 类
- IJobDetail job = JobBuilder.Create <HelloJob>()
- .WithIdentity("myJob","group1") // 名称 "myJob", 组 "group1"
- .Build();
- // 触发作业立即运行, 然后每 40 秒运行一次
- ITrigger trigger = TriggerBuilder.Create()
- .WithIdentity("myTrigger","group1")
- .StartNow()
- .WithSimpleSchedule(x => x
- .WithIntervalInSeconds(40)
- .RepeatForever())
- .Build();
- // 告诉 quartz 使用我们的触发器安排作业
- await sched.scheduleJob(job,trigger);
创建 Job 作业定义的代码块使用 JobBuilder 对象的流畅的接口来创建 IJobDetail 的实例对象. 同样, 构建触发器的代码块使用 TriggerBuilder 的流畅接口和扩展方法来创建触发器实例对象, 其中的扩展方法是针对特定的触发器类型. 可能的调度扩展方法是:
- .WithCalendarIntervalSchedule(使用日历间隔时间表的调度程序)
- .WithCronSchedule(使用 Cron 表达式的调度程序)
- .WithDailyTimeIntervalSchedule(使用每日时间间隔时间表的调度程序)
- .WithSimpleSchedule(使用简单的时间表的调度程序)
DateBuilder 类包含各种方法, 可以轻松地为特定时间点构建 DateTimeOffset 实例(例如表示下一个偶数小时的日期 - 或者换句话说, 如果当前是 9:43:27, 则为 10:00:00)
2, 工作和触发器(Jobs and Triggers)
Job 是一个实现 IJob 接口的类, 它只有一个简单的方法:
IJob 接口
- namespace Quartz
- {
- public interface IJob
- {
- Task Execute(JobExecutionContext context);
- }
- }
当 Job 作业的触发器触发时 (稍后会更多),Execute(..) 方法由调度程序的一个工作线程调用. 传递给此方法的 JobExecutionContext 对象为 Job 作业实例提供有关其 "运行时" 环境的信息 - 执行它的调度程序的句柄, 触发执行触发器的句柄, Job 作业的 JobDetail 对象以及其他一些项目.
JobDetail 对象是在将 Job 添加到调度程序时由 Quartz.NET 客户端 (您的程序) 创建的. 它包含 Job 的各种属性设置, 以及 JobDataMap, 它可用于存储 Job 作业类的给定实例的状态信息. 它本质上是作业实例的定义, 将在下一节中进一步详细讨论.
触发器对象用于触发作业的执行 (或 "触发"). 当您希望调度作业时, 可以实例化触发器并 "调整" 其属性以提供您希望的规划. 触发器也可能有一个与之关联的 JobDataMap - 这对于将参数传递给特定触发器触发的 Job 作业非常有用. Quartz 附带了一些不同的触发器类型, 但最常用的类型是 SimpleTrigger(接口 ISimpleTrigger) 和 CronTrigger(接口 ICronTrigger).
如果您需要 "一次性" 执行(在给定时刻只执行一次作业), 或者如果您需要在给定时间触发作业, 并且重复 N 次, 延迟, 则 SimpleTrigger 非常方便. 如果您希望基于类似日历的时间表触发 - 例如 "每个星期五, 中午" 或 "每个月的第 10 天 10:15",CronTrigger 非常有用.
为什么我们把作业和触发器的概念分离呢? 很多 Job 的调度程序并没有单独区分作业和触发器的概念. 有些将 "作业" 简单的定义为执行时间 (或计划) 以及一些小作业标识符. 其他的定义很像 Quartz 的作业和触发器对象的结合体. 在开发 Quartz 时, 我们认为在时间计划表和要按照该时间计划表执行的作业之间, 两者分离是有意义的. 这 (我们认为) 有很多好处.
例如, 可以独立于触发器创建作业调度程序并将其存储在作业调度程序中, 并且许多触发器可以与同一作业相关联. 此松散耦合的另一个好处是能够配置在关联的触发器到期后保留在调度程序中的作业, 以便以后可以重新调度, 而无需重新定义它. 它还允许您修改或替换触发器, 而无需重新定义其关联的作业.
3, 身份标识
作业和触发器在 Quartz 调度程序中注册时被赋予标识符. 作业和触发器的名称 (JobKey 和 TriggerKey) 允许将它们放入 "组" 中, 这对于将作业和触发器组织成 "报告作业" 和 "维护作业" 等组别时非常有用. 作业或触发器的名称部分在组内必须是唯一的, 换句话说, 作业或触发器的完整名称 (或标识符) 是名称和组名的组合.
您现在可以大致了解作业和触发器的内容, 您可以在第 3 节: 有关 Job 作业和 JobDetail 作业详细信息的更多内容以及有关 Trigger 触发器的更多信息中了解有关它们的更多信息.
三, 有关 Job 和 JobDetials 的更多信息
正如您在第 2 节中看到的那样, Job(作业)是很容易实现的. 关于 Job(作业)的性质, 关于 IJob 接口的 Execute(..)方法以及 JobDetails, 还需要了解更多内容.
虽然您实现的 Job(作业)类具有知道如何处理特定类型作业的实际工作的代码, 但 Quartz.NET 也需要了解您可能希望该 Job(作业)实例具有的各种属性. 这是通过 JobDetail 类完成的, 在上一节中已经简要提到过.
JobDetail 实例是使用 JobBuilder 类构建的. JobBuilder 允许您使用流畅的接口描述您的 Job(作业)的细节.
现在让我们花点时间讨论一下在 Quartz.NET 中 Job(作业)的 "本质" 和 Job(作业)实例的生命周期. 首先让我们回顾一下我们在第 1 节中看到的一些代码片段:
- Using Quartz.NET
- // 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();
- sched.ScheduleJob(job, trigger);
现在考虑将 Job(作业)类 HelloJob 定义为:
- public class HelloJob : IJob
- {
- public async Task Execute(IJobExecutionContext context)
- {
- await Console.Out.WriteLineAsync("HelloJob is executing.");
- }
- }
请注意, 我们为调度程序提供了一个 IJobDetail 实例, 并且它通过简单地提供 Job(作业)类来引用要执行的 Job(作业). 每次调度程序在执行 Job(作业)的 Execute(..)方法之前创建该类的新实例. 这种行为的一个后果是, Job(作业)必须有一个无参数的构造函数. 另一个分支是在 Job 作业类上定义数据字段没有意义 - 因为它们的值不会在作业执行之间保留.
您现在可能想问 "我如何为 Job 实例提供属性 / 配置?" 和 "如何在执行之间跟踪 Job 作业的状态?" 这些问题的答案是相同的: 这就会涉及到一个关键对象, 它就是 JobDataMap , 它是 JobDetail 对象的一部分.
1,JobDataMap
JobDataMap 可用于保存您希望在 Job 作业实例执行时可用的任意数量 (可序列化) 对象. JobDataMap 是 IDictionary 接口的一个实现, 并且具有一些用于存储和检索基本类型数据的便利方法.
以下是在将作业添加到调度程序之前将数据放入 JobDataMap 的一些快速代码段:
- Setting Values in a JobDataMap
- // define the job and tie it to our DumbJob class
- IJobDetail job = JobBuilder.Create<DumbJob>()
- .WithIdentity("myJob", "group1") // name "myJob", group "group1"
- .UsingJobData("jobSays", "Hello World!")
- .UsingJobData("myFloatValue", 3.141f)
- .Build();
这是在作业执行期间从 JobDataMap 获取数据的快速示例:
- Getting Values from a JobDataMap
- public class DumbJob : IJob
- {
- public async Task Execute(IJobExecutionContext context)
- {
- JobKey key = context.JobDetail.Key;
- JobDataMap dataMap = context.JobDetail.JobDataMap;
- string jobSays = dataMap.GetString("jobSays");
- float myFloatValue = dataMap.GetFloat("myFloatValue");
- await Console.Error.WriteLineAsync("Instance" + key + "of DumbJob says:" + jobSays + ", and val is:" + myFloatValue);
- }
- }
如果您使用持久性 JobStore(在本教程的 JobStore 部分中讨论), 您应该谨慎地决定放置在 JobDataMap 中的内容, 因为其中的对象将被序列化, 因此它们容易出现类型的版本问题. 显然, 标准的. NET 类型应该是非常安全的, 但除此之外, 每当有人更改您已序列化实例的类型的定义时, 必须注意不要破坏兼容性.
或者, 您可以将 AdoJobStore 和 JobDataMap 配置为只能在其类型中只能存储基元类型和字符串的模式, 从此以后就可以消除任何序列化的可能性, 完全避免发生序列化.
如果向 Job 作业类添加具有 set 访问器的属性, 这些属性对应于 JobDataMap 中的键名称, 然后 Quartz 的 JobFactory 的默认实现将在实例化 Job 作业类时自动调用这些 setter, 因此, 无需在执行方法中明确地从 JobDataMap 中获取值.
触发器也可以具有与之关联的 JobDataMaps. 如果您有一个存储在调度程序中的 Job 作业以供多个触发器定期 / 重复使用, 但是, 每次独立触发, 并且您希望为 Job 作业提供不同的数据输入, 这就可能很有用了.
在 Job 作业执行期间, 在 JobExecutionContext 上找到的 JobDataMap 是为了方便使用而进行处理过的. 为什么这样说呢, 因为它是在 JobDetail 上找到的 JobDataMap 和在 Trigger 上找到的 JobDataMap 的合并体, 并且后者中的值覆盖前者中的任何同名值.
以下是在 Job 作业执行期间从 JobExecutionContext 的合并 JobDataMap 获取数据的快速示例:
- public class DumbJob : IJob
- {
- public async Task Execute(IJobExecutionContext context)
- {
- JobKey key = context.JobDetail.Key;
- JobDataMap dataMap = context.MergedJobDataMap; // Note the difference from the previous example
- string jobSays = dataMap.GetString("jobSays");
- float myFloatValue = dataMap.GetFloat("myFloatValue");
- IList<DateTimeOffset> state = (IList<DateTimeOffset>)dataMap["myStateData"];
- state.Add(DateTimeOffset.UtcNow);
- await Console.Error.WriteLineAsync("Instance" + key + "of DumbJob says:" + jobSays + ", and val is:" + myFloatValue);
- }
- }
或者, 如果您希望依赖 JobFactory 将数据映射值 "注入" 到您的类中, 它可能看起来像这样:
- public class DumbJob : IJob
- {
- public string JobSays { private get; set; }
- public float FloatValue { private get; set; }
- public async Task Execute(IJobExecutionContext context)
- {
- JobKey key = context.JobDetail.Key;
- JobDataMap dataMap = context.MergedJobDataMap; // Note the difference from the previous example
- IList<DateTimeOffset> state = (IList<DateTimeOffset>)dataMap["myStateData"];
- state.Add(DateTimeOffset.UtcNow);
- await Console.Error.WriteLineAsync("Instance" + key + "of DumbJob says:" + JobSays + ", and val is:" + FloatValue);
- }
- }
你会注意到类的整体代码更长, 但 Execute()方法中的代码更清晰. 我们可以这样争辩说, 虽然代码更长, 如果程序员的 IDE 可以用于自动生成属性, 而不是必须手动编写单个调用以从 JobDataMap 检索值, 实际上需要的编码会更少. 两种方式, 你可以根据你的爱好选择你的编码方式.
2,Job 作业的 "实例"
很多人花费了大量时间, 依然对于 "Job 作业实例" 究竟是由什么构成的感到困惑. 我们将在这里以及下面有关作业状态和并发性的部分章节中尝试说清楚它.
您可以创建一个唯一的 Job 作业类, 并通过创建 JobDetails 的多个实例在调度程序中存储它的许多个 "实例定义".
- 每个都有自己的属性和 JobDataMap - 并将它们全部添加到调度程序.
例如, 您可以创建一个实现 IJob 接口的, 名称是 "SalesReportJob" 的类型. 可以对该 Job 作业进行编码, 通过发送给该 Job 作业的参数 (通过 JobDataMap) 来指定销售报表是基于某个销售人员的姓名的. 然后, 他们可以创建 Job 作业的多个定义(JobDetails), 例如 "SalesReportForJoe" 和 "SalesReportForMike", 它们在各自的 JobDataMaps 中将 "joe" 和 "mike" 作为相应作业的输入, 就可以指出报表出处与谁.
触发器触发时, 将加载与该触发器相关联的 JobDetail(实例定义), 并通过 Scheduler 上配置的 JobFactory 实例化 JobDetial 引用的 Job 作业类. 默认的 JobFactory 使用 Activator.CreateInstance 简单地调用 Job 作业类的默认构造函数, 然后尝试在类上调用与 JobDataMap 中的键名匹配的 setter 属性. 您可能希望创建自己的 JobFactory 实现来完成诸如让应用程序的 IoC 或 DI 容器生成 / 初始化作业实例之类的事情.
在 "Quartz speak" 中, 我们将每个存储的 JobDetail 称为 "作业定义" 或 "JobDetail 实例", 并且我们将每个执行作业称为 "作业实例" 或 "作业定义的实例". 通常, 如果我们只使用 "job" 这个词, 我们指的是命名定义或 JobDetail. 当我们提到实现作业接口 IJob 的类时, 我们通常使用术语 "作业类型".
3,Job 作业的状态和并发
现在, 关于作业的状态数据 (也称为 JobDataMap) 和并发性的一些附加说明. 有几个属性可以添加到您的 Job 类中, 这些属性会影响 Quartz 在这些方面的行为.
1),DisallowConcurrentExecution 是一个可以添加到 Job 类的属性, 它告诉 Quartz 不要同时执行给定作业定义的多个实例 (指向给定的作业类). 注意那里的措辞, 因为它是非常谨慎地选择的. 在上一节的示例中, 如果 "SalesReportJob" 具有此属性, 则只能在给定时间执行 "SalesReportForJoe" 的一个实例, 但它可以与 "SalesReportForMike" 实例同时执行. 约束基于实例定义(JobDetail), 而不是基于 Job 作业类的实例. 然而, 决定(在 Quartz 的设计期间) 具有类本身所承载的属性, 因为它通常会对类的编码方式产生影响.
2),PersistJobDataAfterExecution 是一个可以添加到 Job 类的属性, 它告诉 Quartz 在 Execute()方法成功完成后 (不抛出异常) 更新 JobDetail 的 JobDataMap 的存储副本, 以便下一次执行相同的作业 (JobDetail) ) 接收更新的值而不是原始存储的值. 与 DisallowConcurrentExecution 属性类似, 这适用于作业定义实例, 而不是作业类实例, 尽管决定让作业类携带属性, 因为它通常会对类的编码方式产生影响(例如,'有状态'需要由 execute 方法中的代码明确'理解').
如果使用 PersistJobDataAfterExecution 属性, 则应强烈考虑使用 DisallowConcurrentExecution 属性, 以避免在同时执行同一作业 (JobDetail) 的两个实例时可能存在的数据混乱(竞争条件).
4,Job 作业的其他属性
以下是可以通过 JobDetail 对象为作业实例定义的其他属性的快速摘要:
1),Durability 如果 Job 作业是非持久性的, 如果没有任何活动触发器与之相关联, 它将自动从调度程序中删除. 换句话说, 非持久性 Job 作业的寿命由其触发器的存在所限制.
2),RequestsRecovery 如果一个作业 "请求恢复", 并且它正在调度程序的 "硬关闭" 期间执行(即它运行在崩溃的进程中, 或者机器被关闭), 那么当调度程序再次启动时它被重新执行. 在这种情况下, JobExecutionContext.Recovering 属性将返回 true.
5,JobExecutionException
最后, 我们需要告诉您 IJob.Execute() 方法的一些细节内容. 您应该从 execute 方法抛出的唯一异常类型是 JobExecutionException. 因此, 您通常应该使用'try-catch'块来包装 execute 方法的全部内容. 您还应该花一些时间查看 JobExecutionException 的文档资料, 因为您的 Job 作业可以使用它来为调度程序提供有关如何处理异常的各种指令.
来源: https://www.cnblogs.com/PatrickLiu/p/10005912.html