前幾天寫了一篇有關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(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上找到的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