作业流程是在调度器的统一调度下完成的,它可以调度多个作业,触发器提供作业执行的条件(每天 8:00 am),触发器与作业关联,它们是 1:N 的关系,1个触发器可以关联1个或多个作业。
附带的作业
我们知道要实现自己的作业功能只要继承 IJob 接口并实现 Execute(JobExecutionContext context) 方法,再把它添加到调度器,调度器会调用执行Execute(JobExecutionContext context) 方法。调用期间,调度器会跟踪作业和它们的执行次数。Quartz.NET 默认提供了 FileScanJob 监视某个文件是否被修改,NativeJob 执行指定程序,NoOpJob 空操作用来给系统调用 ITriggerListener 、IJobListener ,以及 SendMailJob 邮件发送作业
它有一个存储作业执行时数据的重要属性:MergedJobDataMap,它是 JobDataMap 类型,也可以使用 JobExecutionContext.JobDetail.JobDataMap 来获得 JobDataMap 对象的引用。作为传递参数的容器,JobDataMap 间接继承了 DirtyFlagMap,DirtyFlagMap 内嵌了 Hashtable 容器,它有一组数据读写方法。可以使用 Visual Studio 的类图来查看。
当实现一个真实的作业类时,一些属性是需要告知Quartz的,也是您希望该作业需要有的。这就需要通过JobDetail类,这个类在前面我们已经简单介绍过。
下面我们看一下作业的本质,以及生命周期。还是看看前面的例子:
Lesson1
// construct a scheduler factory
ISchedulerFactory schedFact = new StdSchedulerFactory();
// get a scheduler
IScheduler sched = schedFact.GetScheduler();
sched.Start();
// construct job info
JobDetail jobDetail = new JobDetail("myJob", null, typeof(DumbJob));
// fire every hour
Trigger trigger = TriggerUtils.MakeHourlyTrigger();
// start on the next even hour
trigger.StartTime = TriggerUtils.GetEvenHourDate(DateTime.UtcNow);
trigger.Name = "myTrigger";
sched.ScheduleJob(jobDetail, trigger);
public class DumbJob : IJob
{
public DumbJob() { }
publicvoid Execute(JobExecutionContext context)
{
Console.WriteLine("DumbJob is executing.");
}
}
如上例所示,向scheduler提供JobDetail实例,就能指向作业执行。scheduler每次执行(Execute方法)作业前,都新生成一个作业实例。由于是没有参数的构造器,每次作业执行,数据都没清一次。
JobDataMap
在作业实例执行的时候,JobDataMap可以用来保存任何数量对象(可序列化)的。JobDataMap继承自IDictionary接口,添加一些便捷的方法,以便存储和获得原型数据。
以下是添加到scheduler之前,向JobDataMap存放数据的代码片段:
jobDetail.JobDataMap[
"jobSays"
] =
"Hello World!"
;
jobDetail.JobDataMap[
"myFloatValue"
] = 3.141f;
jobDetail.JobDataMap[
"myStateData"
] =
new
ArrayList();
以下是在作业执行时,取数据的代码片段:
public
class
DumbJob : IJob
{
public
void
Execute(JobExecutionContext context)
{
string
instName = context.JobDetail.Name;
string
instGroup = context.JobDetail.Group;
JobDataMap dataMap = context.JobDetail.JobDataMap;
string
jobSays = dataMap.GetString(
"jobSays"
);
float
myFloatValue = dataMap.GetFloat(
"myFloatValue"
);
ArrayList state = (ArrayList) dataMap[
"myStateData"
];
state.Add(DateTime.UtcNow);
Console.WriteLine(
"Instance {0} of DumbJob says: {1}"
, instName, jobSays);
}
}
如果需要用JobStore来做持久化的话,就需要小心存在JobDataMap的是什么了。因为存放的对象是要被序列化的,那么就很容易出现对象的版本问题。显然,.Net的标准数据类型是安全的,除此之外,序列化的对象实例,任何变化,都会导致版本兼容问题。或者干脆只允许原型数据类型和字符串能保存,这样能避免以后序列化版本冲突问题。
在作业执行时,JobDataMap可以从JobExecutionContext中获得。这时候它是JobDetail和Trigger中的JobDataMap合并,是后来同名对象的覆盖。
下面是在作业执行时,从JobExecutionContext中获得JobDataMap的代码片段:
public class DumbJob : IJob
{
public void Execute(JobExecutionContext context)
{
string instName = context.JobDetail.Name;
string instGroup = context.JobDetail.Group;
// Note the difference from the previous example
JobDataMap dataMap = context.MergedJobDataMap;
string jobSays = dataMap.GetString("jobSays");
float myFloatValue = dataMap.GetFloat("myFloatValue");
ArrayList state = (ArrayList) dataMap.Get("myStateData");
state.Add(DateTime.UtcNow);
Console.WriteLine("Instance {0} of DumbJob says: {1}", instName, jobSays);
}
}
有状态与无状态的作业
以上我们的作业实例都是从 IJob 继承,Quartz.NET 里还有 IStatefulJob 、IInterruptableJob,它的声明方式为:
public interface IStatefulJob : IJob
{
}
public interface IInterruptableJob : IJob
{
void Interrupt();
}
IInterruptableJob 接口提供了一个中断方法,但是 IStatefulJob 没有自己的方法。从 Quartz.NET 官方了解到:
一个 Job 实例可以被定义为“有状态的”或者“无状态的”。在执行无状态的任务过程中任何对 JobDataMap 所作的更改都将丢失。有状态的任务恰好相反,它在任务的每次执行之后重新存储 JobDataMap 。有状态任务的一个缺点就是它不能并发执行。也就是说,如果任务有状态,那么当触发器试图触发它,触发器就会被阻塞直到前面的执行完成。想使任务有状态,它就要实现 IStatefulJob 接口而不是实现IJob接口。
作业实例,可以被指定为“statefull”,或者“non-statefull”。Non-statefull作业只在JobDataMap添加到scheduler时有数据。就是意味着,在下一次作业执行时,任何作业状态数据(JobDataMap)的修改丢将丢失或看不见。正如您猜测的那样,statefull作业正好相反,作业的每次执行,JobDataMap都会重新保存,这给statefull作业的负作用,就是不能并发执行。也就是说,如果一个statefull作业,触发器已经触发正在执行了,下个触发器将会被阻塞,直到前一个执行完成。
标识一个statefull作业,只要继承IStatefullJob接口,而不是IJob接口。
Job 'Instances'
这个需要最后明确的是,您可以创建一个作业类,通过创建多个JobDetail实例,存储在scheduler中,每个实例都有自己的属性集及JobDataMap。
当与作业关联的触发器触发时,作业通过JobFactory实例化。缺省的JobFactory调用Activator.CreateInstance。
Other Attributes Of Jobs
这里列一下JobDetail对象的其他属性:
- Durable - 一个non-durable作业, 当没有与之关联的active trigger时,会自动从scheduler中删除的。
- Volatile - 一个volatile作业, 在re-starts scheduler之间是不保留的。
- RequestsRecovery - 一个"requests recovery"作业,在scheduler“hard shutdown”的情况下,或者机器断电的时候,当scheduler重启时,作业重新执行。JobExecutionContext.isRecovering()返回true。
- JobListeners - 作业可以没有或多个JobListeners。作业执行时,listeners会被提醒。更多讨论后面专门讨论。
The Job.Execute(..) Method
为什么作业和触发分开
很多作业调度程序,作业和触发没有明显的分开界限。有些作业定义为一个作业识别号和相应的执行时间。还有些更像Quartz.NET作业和触发的聚合。当开发Quartz的时候,Quartz team决定,把调度和所要执行的动作分开(trigger)。据我们所知,这将带来很多便捷。例如:
- 作业可以独立trigger存储在scheduler中,多个trigger关联同一个作业。
- 松耦合可以重新调度已经过期作业,而不需要重新定义。也可以修改替换触发器而不需要重新定义与之相关的作业。
识别符Identifiers
与scheduler注册作业和触发器时,需要指定识别名称的。作业、触发器也能够安祖存放,为日后维护时组织分类作业和触发器提供方便。作业和触发器的名称在组内必须是唯一的。也就是说,正确的识别符是名称 + 组,如果让组名(Job or Trigger)留空 'null', 就等于说属于SchedulerConstants.DefaultGroup。