正如您在第 2 课中看到的,作业很容易实现。 关于作业的性质、IJob 接口的 Execute(..) 方法以及 JobDetails,您还需要了解一些其他的事情。

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

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

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

Using Quartz.NET

  1. // define the job and tie it to our HelloJob class
  2. IJobDetail job = JobBuilder.Create<HelloJob>()
  3. .WithIdentity("myJob", "group1")
  4. .Build();
  5. // Trigger the job to run now, and then every 40 seconds
  6. ITrigger trigger = TriggerBuilder.Create()
  7. .WithIdentity("myTrigger", "group1")
  8. .StartNow()
  9. .WithSimpleSchedule(x => x
  10. .WithIntervalInSeconds(40)
  11. .RepeatForever())
  12. .Build();
  13. sched.ScheduleJob(job, trigger);

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

  1. public class HelloJob : IJob
  2. {
  3. public async Task Execute(IJobExecutionContext context)
  4. {
  5. await Console.Out.WriteLineAsync("HelloJob is executing.");
  6. }
  7. }

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

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

JobDataMap

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

以下是在将作业添加到调度程序之前将数据放入 JobDataMap 的一些快速片段:

在 JobDataMap 中设置值

  1. // define the job and tie it to our DumbJob class
  2. IJobDetail job = JobBuilder.Create<DumbJob>()
  3. .WithIdentity("myJob", "group1") // name "myJob", group "group1"
  4. .UsingJobData("jobSays", "Hello World!")
  5. .UsingJobData("myFloatValue", 3.141f)
  6. .Build();

下面是一个在作业执行期间从 JobDataMap 获取数据的快速示例:

从 JobDataMap 获取值

  1. public class DumbJob : IJob
  2. {
  3. public async Task Execute(IJobExecutionContext context)
  4. {
  5. JobKey key = context.JobDetail.Key;
  6. JobDataMap dataMap = context.JobDetail.JobDataMap;
  7. string jobSays = dataMap.GetString("jobSays");
  8. float myFloatValue = dataMap.GetFloat("myFloatValue");
  9. await Console.Error.WriteLineAsync("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
  10. }
  11. }

如果您使用持久的 JobStore(在本教程的 JobStore 部分讨论),您应该谨慎决定在 JobDataMap 中放置什么,因为其中的对象将被序列化,因此它们容易出现类版本问题。显然,标准的 .NET 类型应该是非常安全的,但除此之外,任何时候有人更改您已为其序列化实例的类的定义,都必须注意不要破坏兼容性。

或者,您可以将 AdoJobStore 和 JobDataMap 设置为只能将基元和字符串存储在映射中的模式,从而消除以后出现序列化问题的任何可能性。

如果你在你的作业类中添加与 JobDataMap 中的键名对应的 set 访问器属性,那么 Quartz 的默认 JobFactory 实现将在作业实例化时自动调用这些设置器,从而避免需要显式地从在您的执行方法中映射。请注意,使用自定义 JobFactory 时,默认情况下不维护此功能。

触发器也可以有与之关联的 JobDataMaps。这在您有一个存储在调度程序中供多个触发器定期/重复使用的作业的情况下很有用,但是对于每个独立的触发,您希望为作业提供不同的数据输入。

在 Job 执行期间在 JobExecutionContext 上找到的 JobDataMap 起到了方便的作用。它是 JobDetail 上的 JobDataMap 和 Trigger 上的 JobDataMap 的合并,后者中的值覆盖前者中的任何同名值。

这是一个在作业执行期间从 JobExecutionContext 的合并 JobDataMap 获取数据的快速示例:

  1. public class DumbJob : IJob
  2. {
  3. public async Task Execute(IJobExecutionContext context)
  4. {
  5. JobKey key = context.JobDetail.Key;
  6. JobDataMap dataMap = context.MergedJobDataMap; // Note the difference from the previous example
  7. string jobSays = dataMap.GetString("jobSays");
  8. float myFloatValue = dataMap.GetFloat("myFloatValue");
  9. IList<DateTimeOffset> state = (IList<DateTimeOffset>)dataMap["myStateData"];
  10. state.Add(DateTimeOffset.UtcNow);
  11. await Console.Error.WriteLineAsync("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
  12. }
  13. }

或者,如果您希望依赖 JobFactory 将数据映射值“注入”到您的类中,它可能看起来像这样:

  1. public class DumbJob : IJob
  2. {
  3. public string JobSays { private get; set; }
  4. public float MyFloatValue { private get; set; }
  5. public async Task Execute(IJobExecutionContext context)
  6. {
  7. JobKey key = context.JobDetail.Key;
  8. JobDataMap dataMap = context.MergedJobDataMap; // Note the difference from the previous example
  9. IList<DateTimeOffset> state = (IList<DateTimeOffset>)dataMap["myStateData"];
  10. state.Add(DateTimeOffset.UtcNow);
  11. await Console.Error.WriteLineAsync("Instance " + key + " of DumbJob says: " + JobSays + ", and val is: " + MyFloatValue);
  12. }
  13. }

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

作业“实例”

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

您可以创建单个作业类,并通过创建多个 JobDetails 实例在调度程序中存储它的许多“实例定义”

  • 每个都有自己的一组属性和 JobDataMap - 并将它们全部添加到调度程序。

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

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

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

作业状态和并发

现在,关于作业的状态数据(又名 JobDataMap)和并发性的一些附加说明。有几个属性可以添加到您的 Job 类中,这些属性会影响 Quartz 在这些方面的行为。

[DisallowConcurrentExecution] 是一个可以添加到 Job 类的属性,它告诉 Quartz 不要同时执行给定作业定义(指给定作业类)的多个实例。请注意那里的措辞,因为它是经过精心挑选的。在上一节的示例中,如果“SalesReportJob”具有此属性,则在给定时间只能执行一个“SalesReportForJoe”实例,但它可以与“SalesReportForMike”实例同时执行。约束基于实例定义 (JobDetail),而不是作业类的实例。然而,决定(在 Quartz 的设计期间)让类本身带有属性,因为它确实经常对类的编码方式产生影响。

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

如果您使用 PersistJobDataAfterExecution 属性,您应该强烈考虑同时使用 [DisallowConcurrentExecution] 属性,以避免在同时执行同一作业 (JobDetail) 的两个实例时可能会留下存储的数据的混淆(竞争条件)。

工作的其他属性

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

  • Durability - 如果一个作业是非持久的,一旦不再有任何与之关联的活动触发器,它就会自动从调度程序中删除。换句话说,非持久性工作的寿命受其触发因素的限制。
  • RequestsRecovery- 如果一个作业“请求恢复”,并且它在调度程序的“硬关闭”期间执行(即它在崩溃中运行的进程,或者机器被关闭),那么它会被重新执行当调度程序再次启动时。在这种情况下,JobExecutionContext.Recovering 属性将返回 true。

JobExecutionException

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