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 = awaitfactory.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 seconds18 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(40) 12 .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上找到的JobDataMap是為了方便使用而進行處理過的。為什麼這樣說呢,因為它是在JobDetail上找到的JobDataMap和在Trigger上找到的JobDataMap的合併體,並且後者中的值覆蓋前者中的任何同名值。
以下是在Job作業執行期間從JobExecutionContext的合併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.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