Quartz 关于作业的更多信息

如第 3 课中所见,实现作业相当简单。关于作业的本质、IJob 接口的 Execute(..) 方法以及 JobDetails,您还需要了解一些额外的内容。

虽然您实现的作业类包含了执行特定类型作业的实际工作代码,但 Quartz.NET 需要了解您可能希望该作业实例具有的各种属性。这是通过 JobDetail 类来完成的,在前一节中已经简要提到过。

JobDetail 实例是使用 JobBuilder 类构建的。JobBuilder 允许您使用流畅的接口描述作业的详细信息。

现在我们花点时间来讨论一下作业的 “本质” 以及作业实例在 Quartz.NET 中的生命周期。首先回顾一下我们在第 1 课中看到的那段代码片段:

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

现在考虑定义为这样的作业类HelloJob

public class HelloJob : IJob
{
 public async Task Execute(IJobExecutionContext context)
 {
  await Console.Out.WriteLineAsync("HelloJob is executing.");
 }
}

注意我们给调度器提供了一个 IJobDetail 实例,并且它通过简单地提供作业的类来指明要执行的作业。每次调度器执行作业时,都会在调用其 Execute(..) 方法之前创建该类的新实例。这种行为的一个后果是作业必须有一个无参数的构造函数。另一个后果是在作业类上定义数据字段是没有意义的——因为它们的值在作业执行之间不会被保留。

依赖注入
如果在使用 Quartz.NET 的同时使用依赖注入框架,您的构造函数可以像 ASP.NET MVC 中的控制器一样拉入服务依赖项。

您现在可能想问:“我如何为作业实例提供属性/配置?”和“我如何在执行之间跟踪作业的状态?”这些问题的答案是一样的:关键在于 JobDataMap,它是 JobDetail 对象的一部分。

JobDataMap

JobDataMap可以用来保存任何数量的(可序列化)对象,您希望在作业实例执行时使其可用。JobDataMapIDictionary接口的实现,并具有一些方便存储和检索原始类型数据的方法。

下面是一些在将作业添加到调度器之前将数据放入JobDataMap的快速代码片段:

在 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获取数据的快速示例:

从 JobDataMap 中获取值

public class DumbJob : IJob
{
 public async Task Execute(IJobExecutionContext context)
 {
  JobKey key = context.JobDetail.Key;

        // note: use context.MergedJobDataMap in production code
  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 类型应该是非常安全的,但除此之外,每当有人更改你有序列化实例的类的定义时,都需要小心以避免破坏兼容性。

可选地,您可以将 AdoJobStoreJobDataMap 置于一个模式,其中只允许存储和字符串在映射中,从而消除了以后可能出现的序列化问题。

如果您在作业类中添加了与 JobDataMap 中的键名相对应的具有公共 set 访问器的属性,那么 Quartz 的默认 JobFactory 实现将在实例化作业时自动调用这些 setter,从而无需在执行方法中显式地从映射中获取值。请注意,当使用自定义 JobFactory 时,默认情况下不维护此功能。

触发器也可以有关联的 JobDataMap。在您有一个存储在调度器中的作业,用于常规/重复使用由多个触发器,并且每次独立触发时,您希望为作业提供不同的数据输入的情况下,这可能会很有用。

在作业执行期间在 JobExecutionContext上找到的 JobDataMap 提供了一种便利。它是从 JobDetail 上找到的 JobDataMap 和从 Trigger 上找到的 JobDataMap 的合并,触发器中的值覆盖作业中的同名值。

这里是一个在作业执行期间从 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 MyFloatValue { 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: " + MyFloatValue);
 }
}

您会注意到类的整体代码更长,但在Execute()方法中的代码更简洁。也有人可能会争辩说,尽管代码更长,但实际上编码更少,如果程序员的 IDE 被用来自动生成属性,而不是必须手工编码从 JobDataMap 中检索值的个别调用。选择权在于您。

作业“实例”

许多用户花费时间对究竟什么是“作业实例”感到困惑。我们将在本节和下面关于作业状态和并发的部分中澄清这一点。

您可以创建一个单一的作业类,并通过创建多个 JobDetails 的实例将其多次“实例定义”存储在调度器中并添加到调度器中。

  • 每个都有自己的属性集和 JobDataMap。

例如,您可以创建一个实现 IJob 接口的类,称为“SalesReportJob”。作业可能被编码为期望通过 JobDataMap 发送参数来指定销售报告应基于的销售人员姓名。然后,他们可以创建该作业的多个定义(JobDetails),例如“SalesReportForJoyce”和“SalesReportForMike”,在相应的 JobDataMaps 中分别指定“joyce”和“mike”作为各自作业的输入。

当触发器触发时,与其关联的 JobDetail(实例定义)被加载,并且它引用的作业类通过配置在 Scheduler 上的 JobFactory 实例化。默认的 JobFactory 简单地使用 Activator.CreateInstance 调用作业类的默认构造函数,然后尝试调用类上与 JobDataMap 中键名匹配的 setter 属性。您可能想要创建自己的 JobFactory 实现来完成诸如让应用程序的 IoC 或 DI 容器生产/初始化作业实例的任务。

在“Quartz 术语”中,我们将每个存储的 JobDetail 称为“作业定义”或“JobDetail 实例”,并将每个正在执行的作业称为“作业实例”或“作业定义的实例”。通常,如果我们只使用“作业”这个词,我们指的是命名定义,或 JobDetail。当我们指的是实现作业接口的类时,我们通常使用“作业类型”的术语。

作业状态和并发

现在,关于作业的状态数据(即 JobDataMap)和并发的一些额外说明。有一些属性可以添加到作业类中,影响 Quartz 对此类行为。

[DisallowConcurrentExecution] 是一个可以添加到作业类的属性,告诉 Quartz 不要并行执行给定作业定义(引用给定作业类)的多个实例。请注意这里的措辞,因为它被精心选择。在上一节的例子中,如果“SalesReportJob”具有这个属性,那么只有一个“SalesReportForJoyce”的实例可以在给定时间执行,但它可以与“SalesReportForMike”的一个实例并行执行。约束是基于作业定义实例(JobDetail),而不是作业类实例。然而,决定将该属性放在类本身上,是因为它通常会影响到类的编码方式。

[PersistJobDataAfterExecution] 是一个可以添加到作业类的属性,告诉 Quartz 在Execute()方法成功完成(不抛出异常)后更新存储的 JobDetail 的 JobDataMap 副本,以便下次执行同一作业(JobDetail)时接收到更新后的值而不是最初存储的值。与 [DisallowConcurrentExecution] 属性一样,这适用于作业定义实例,而不是作业类实例,但是决定让作业类携带该属性,是因为它通常确实会影响到类的编码方式(例如,需要显式地“理解”执行方法内部的“有状态性”)。

如果您使用 PersistJobDataAfterExecution 属性,您应强烈考虑同时也使用 [DisallowConcurrentExecution] 属性,以避免可能的混淆(竞态条件),当同一作业(JobDetail)的两个实例并行执行时,存储的是什么数据。

作业的其他属性

以下是可以通过 JobDetail 对象为作业实例定义的其他属性的快速总结:

属性 描述
持久性 如果作业是非持久性的,则一旦不再有任何活动的触发器与之相关联,它就会自动从调度器中删除。换句话说,非持久性作业的生命周期受限于其触发器的存在。
请求恢复 如果作业“请求恢复”,并且在调度器“硬关闭”期间正在执行(即,运行它的进程崩溃或机器被关闭),则在调度器再次启动时重新执行。在这种情况下, JobExecutionContext.Recovering 属性将返回 true。

JobExecutionException

最后,我们需要向您介绍关于 IJob.Execute(..) 方法的一些细节。从执行方法中抛出的唯一类型的异常应该是 JobExecutionException。因此,您通常应该使用 'try-catch' 块包裹执行方法的全部内容。您还应该花时间查看 JobExecutionException 的文档,因为您的作业可以使用它来向调度器提供关于如何处理该异常的各种指令。

在本文档中