Quartz.NET - 教程 3: 更多关于 Jobs 和 JobsDetails
译者注:
目录在这
原文在这 Lesson 3: More About Jobs & JobDetails
正如你在 中看到的, 作业相当容易实现. 你还需要了解关于作业性质, IJob 接口的 Execute(..) 方法, 以及 JobDetails 这些内容.
虽然你实现的作业类具有知道如何执行特定类型作业实际工作的代码, 但 Quartz.NET 有你可能需要希望的作业实例的的各种属性. 这是通过 JobDetail 类完成的, 这在上一节中简单提到过.
JobDetail 实例是使用 JobBuilder 类来构建的. JobBuilder 允许你使用流式接口来描述你的作业详情.
现在让我们花点时间来讨论一下关于 Quartz.NET 中作业的 '性质' 以及作业实例的生命周期. 首先我们来回顾一下我们在 中看到的代码片段:
使用 Quartz.NET
// 定义作业并将其绑定到我们的 HelloJob 类
IJobDetail job = JobBuilder.Create()
.WithIdentity("myJob", "group1")
.Build();
// 立即触发作业运行, 然后每40秒运行一次
ITrigger trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger", "group1")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(40)
.RepeatForever())
.Build();
sched.ScheduleJob(job, trigger);
与此同时定义的作业类 HelloJob 如下:
public class HelloJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
await Console.Out.WriteLineAsync("HelloJob is executing.");
}
}
作业数据对象
JobDataMap 可用于保存任意数量的 (可序列化的) 对象, 这些对象希望在作业实例执行时对其可用. JobDataMap 是 IDictionary 接口的一个实现, 她为存储和检索原始类型的数据增加了一些便捷的的方法.
在 JobDataMap 中设置值
以下是在将作业添加到调度程序之前将数据放入 JobDataMap 的一些快速片段:
// 定义作业并将其绑定到我们的 DumbJob 类
IJobDetail job = JobBuilder.Create()
.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;
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访问器的属性添加到作业类中,并与JobDataMap中的键的名称相对应,那么当作业实例化时,Quartz的默认JobFactory实现将自动调用这些设置器,从而避免需要显式地将值在执行方法内映射。
如果你将带有 set 访问器的属性添加到与 JobDataMap 中键的名称相对应的作业类中,那么 Quartz 中默认的 JobFactory 实现将在作业实例化时自动调用这些 setters, 从而避免在执行方法中需要显式地从映射中获取值. 注意当使用自定义JobFactory 时默认情况下不维护此功能.
触发器也可以有与之关联的 JobDataMaps. 如果你有一个作业存储在调度器中, 供多个触发器定期/重复使用, 但对于每个独立的触发器, 你都希望为作业提供不同的数据输入, 则此功能非常有用.
作业执行期间可以非常便利的在 JobExecutionContext 中找到的 JobDataMap. 它是 JobDetail 上的 JobDataMap 和触发器上的 JobDataMap 的并集, 后者中的值将覆盖前者中的任何同名值.
以下是一个在作业执行期间从 JobExecutionContext 中获取合并后的 JobDataMap 数据的简单示例:
public class DumbJob : IJob
{
public async Task Execute(IJobExecutionContext context)
{
JobKey key = context.JobDetail.Key;
JobDataMap dataMap = context.MergedJobDataMap; // 注意和上一个示例的区别
string jobSays = dataMap.GetString("jobSays");
float myFloatValue = dataMap.GetFloat("myFloatValue");
IList state = (IList)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; // 注意和上一个例子的区别
IList state = (IList)dataMap["myStateData"];
state.Add(DateTimeOffset.UtcNow);
await Console.Error.WriteLineAsync("Instance " + key + " of DumbJob says: " + JobSays + ", and val is: " + FloatValue);
}
}
你会注意到类的整体代码较长, 但是 Execute() 方法中的代码会更简洁. 也有人可能会争辩说, 虽然代码较长, 但如果使用 IDE 中自动生成属性, 而不是手工编写从 JobDataMap 中检索值的单个调用, 则实际上所需的代码更少. 选择权在你.
作业 “实例”
许多用户把时间花在了 "作业实例" 的确切构成的困惑中. 在这里和下面的部分中我们将尝试介绍关于作业状态和并发性的内容.
你可以创建一个单一的作业类, 把它的许多 “实例定义” 存储在创建了 JobDetails 的多个实例的调度程序中.
- 每个都有自己的一组属性和 JobDataMap - 并将它们全部添加到调度程序中.
例如, 你可以创建一个实现了 IJob 接口的作业类 "SalesReportJob". 作业可能把发送给它的参数 (通过 JobDataMap) 用来指定销售人员的名称. 然后, 他们可以创建作业的多个定义 (JobDetails), 例如 “SalesReportForJoe” 和“SalesReportForMike”, 它们对应作业的输入参数有 JobDataMaps 中指定的 “joe” 和 “mike”.
当触发器触发时, 它所关联的 JobDetail (实例定义) 将被加载, 并且她引用的作业类将通过调度程序上配置的 JobFactory 来实例化. 默认的 JobFactory 只是使用 Activator.CreateInstance 调用作业类的默认构造函数, 然后尝试调用该类的 JobDataMap 中的匹配键名称的 setter 属性. 你可能希望创建自己的 JobFactory 实现来完成诸如让应用程序的 IoC 或 DI 容器生成/初始化作业实例等事情.
在 “Quartz speak” 中, 我们将每个存储的 JobDetail 称之为 “作业定义” 或 “作业详情实例”, 并将每个正在执行的作业称为 “作业实例” 或 “作业定义实例”. 通常, 如果我们只使用 “job” 这个词,我们就是指一个命名定义, 或者 JobDetail. 当我们指的是实现了作业接口的类时, 我们通常使用术语 “job type”.
作业状态和并发
一些关于作业状态数据 (也叫 JobDataMap) 和并发的附加说明. 有几个属性可以添加的你的作业类中从而影响 Quartz 相关的行为.
DisallowConcurrentExecution 是一个可以添加到作业类的属性她告诉 Quartz 不要同时执行指定的 Job 定义(指定的作业类)的多个实例. 注意这里的描述, 因为它是精心挑选的. 在上一节的示例中, 如果 "SalesReportJob" 有这个属性, 则在指定时间只能执行一个 "SalesReportForJoe" 实例, 但它可以与 "SalesReportForMike" 实例同时执行. 该约束基于实例定义 (JobDetail), 而不是基于作业类的实例. 然而, 她通常会对类的编码方式产生影响, 还是决定(在 Quartz 的设计过程中)将属性携带到作业类本身.
PersistJobDataAfterExecution 是一个可以添加到作业类的属性她告诉 Quartz 在 Execute()方法成功完成 (没有抛出异常) 之后更新 JobDetail 的 JobDataMap 的存储副本, 这样,同一作业 (JobDetail) 的下一次执行将接收到更新的值, 而不是原始存储的值. 与 DisallowConcurrentExecution 属性一样, 这适用于作业定义实例, 而不是作业类实例, 因为她会对类的编码方式产生影响 (例如, Execute() 方法中的代码需要显式地 '理解' '状态性'), 决定让作业类携带该属性.
如果你使用 PersistJobDataAfterExecution 属性, 则应着重考虑同时使用 DisallowConcurrentExecution 属性, 以规避当同一作业 (JobDetail) 的两个实例并发执行时可能的数据混淆 (竞态条件).
其他作业属性
下面是可以通过 JobDetail 对象为作业实例定义其他属性的快速摘要:
- Durability - 如果一个作业是非持久性的, 那么当没有任何活动的触发器与之关联时, 该作业将被调度程序自动删除. 换句话说, 非持久性作业的寿命取决于其触发器的存在.
- RequestsRecovery - 如果一个作业 "请求恢复", 并且其在调度器的 '硬关闭' 期间执行 (即: 所在进程崩溃, 或机器被关机), 那么当调度程序重启时该作业会重新执行. 在这种情况下, JobExecutionContext.Recovering 属性将返回 true.
作业执行异常
最后, 我们需要告知您 IJob.Execute(..) 方法的一些细节. 您应该从 Execute(..) 方法抛出的唯一异常类型是 JobExecutionException. 因此, 您通常应使用 'try-catch' 代码块包含 Execute(..) 方法的整个内容. 您还应该花一些时间看看 JobExecutionException 的文档, 因为您的作业可以使用她向调度程序提供有关如何处理异常的各种指令.