Quartz.Net进阶之一:初识Job作业和触发器

      前几天写了一篇有关Quartz.Net入门的文章,大家感觉不过瘾,想让我在写一些比较深入的文章。其实这个东西,我也是刚入门,我也想继续深入了解一下,所以就努力看了一些资料,然后自己再整理和翻译一些,作为学习的历程,就记录下来,希望对大家有帮助。

一、使用 Quartz(Using Quartz)

      在您使用一个调度程序之前,需要实例化一个调度程序的实例。 为此,您需要使用一个实现了ISchedulerFactory接口的子类型。

      一旦调度程序被实例化,它就可以启动,然后处于待机模式,当然也可以关闭。 请注意,一旦调度程序关闭,就不能在不重新实例化的情况下重新启动它。 在调度程序启动之前,触发器不会触发(Job作业也不会执行),也不会在处于暂停状态时触发。

      这是一段简洁的代码片段,用于实例化和启动调度程序,并安排执行Job作业:

      使用 Quartz.NET

 1         // construct a scheduler factory
 2         NameValueCollection props = new NameValueCollection
 3         {
 4             { "quartz.serializer.type", "binary" }
 5         };
 6         StdSchedulerFactory factory = new StdSchedulerFactory(props);
 7     
 8         // get a scheduler
 9         IScheduler sched = await factory.GetScheduler();
10         await sched.Start();
11     
12         // define the job and tie it to our HelloJob class
13         IJobDetail job = JobBuilder.Create<HelloJob>()
14             .WithIdentity("myJob", "group1")
15             .Build();
16 
17         // Trigger the job to run now, and then every 40 seconds
18         ITrigger trigger = TriggerBuilder.Create()
19             .WithIdentity("myTrigger", "group1")
20             .StartNow()
21             .WithSimpleSchedule(x => x
22                 .WithIntervalInSeconds(40)
23                 .RepeatForever())
24         .Build();
25       
26         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”)。在上一节中,您看到了一个示例,我们再次在此处介绍其中的一部分:

 1     //定义作业并将其绑定到HelloJob类
 2     IJobDetail job = JobBuilder.Create <HelloJob>()
 3     .WithIdentity("myJob""group1") //名称"myJob",组"group1"
 4     .Build();
 5 
 6     //触发作业立即运行,然后每40秒运行一次
 7     ITrigger trigger = TriggerBuilder.Create()
 8     .WithIdentity("myTrigger","group1")
 9     .StartNow()
10     .WithSimpleSchedule(x => x
11     .WithIntervalInSeconds(4012     .RepeatForever())
13     .Build();
14 
15     //告诉quartz使用我们的触发器安排作业
16     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

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


        现在考虑将Job(作业)类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实例,并且它通过简单地提供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

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


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

          或者,您可以将AdoJobStore和JobDataMap配置为只能在其类型中只能存储基元类型和字符串的模式,从此以后就可以消除任何序列化的可能性,完全避免发生序列化。

          如果向Job作业类添加具有set访问器的属性,这些属性对应于JobDataMap中的键名称,然后Quartz的JobFactory的默认实现将在实例化Job作业类时自动调用这些setter,因此,无需在执行方法中明确地从JobDataMap中获取值。

          触发器也可以具有与之关联的JobDataMaps。如果您有一个存储在调度程序中的Job作业以供多个触发器定期/重复使用,但是,每次独立触发,并且您希望为Job作业提供不同的数据输入,这就可能很有用了。

          在Job作业执行期间,在JobExecutionContext上找到的MergedJobDataMap是为了方便使用而进行处理过的。为什么这样说呢,因为它是在JobDetail上找到的JobDataMap和在Trigger上找到的JobDataMap的合并体,并且后者中的值覆盖前者中的任何同名值。

          以下是在Job作业执行期间从JobExecutionContext的合并 MergedJobDataMap 获取数据的快速示例:

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


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

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


          你会注意到类的整体代码更长,但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.quartz-scheduler.net/documentation/quartz-3.x/tutorial/index.html

原文地址:https://www.cnblogs.com/PatrickLiu/p/10005912.html