# Sayook.Schedule.Framework **Repository Path**: sayook/Sayook.Schedule.Framework ## Basic Information - **Project Name**: Sayook.Schedule.Framework - **Description**: 基于.NetCore的依赖注入 对Quartz.Net 的应用示例 - **Primary Language**: C# - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2020-05-21 - **Last Updated**: 2023-11-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README #### Quartz.Net的关键接口 * Scheduler - 与调度程序交互的主要API。[ IScheduler] * Job - 由希望由调度程序执行的组件实现的接口。[IJob] * JobDetail - 用于定义作业的实例。[IJobDetail] * Trigger(即触发器) - 定义执行给定作业的计划的组件。[ITrigger] * JobBuilder - 用于定义/构建JobDetail实例,用于定义作业的实例。 * TriggerBuilder - 用于定义/构建触发器实例。 ```c# public class HelloJob : IJob { public Task Execute(IJobExecutionContext context) { Console.WriteLine("Hello World"); return Task.CompletedTask; } } class Program { static async Task Main(string[] args) { StdSchedulerFactory factory = new StdSchedulerFactory(); var scheduler = await factory.GetScheduler(); await scheduler.Start(); // define the job and tie it to our HelloJob class IJobDetail job = JobBuilder.Create() .WithIdentity("myJob", "group1") .Build(); // Trigger the job to run now, and then every 3 seconds ITrigger trigger = TriggerBuilder.Create() .WithIdentity("myTrigger", "group1") .StartNow() .WithSimpleSchedule(x => x .WithIntervalInSeconds(3) .RepeatForever()) .Build(); await scheduler.ScheduleJob(job, trigger); // 30秒后停止调度计划 await Task.Delay(1000 * 30); await scheduler.Shutdown(); } } ``` #### Scheduler Scheduler的生命期,从SchedulerFactory创建它时开始,到Scheduler调用Shutdown()方法时结束。 Scheduler被创建后,可以增加、删除和列举Job和Trigger,以及执行其它与调度相关的操作(如暂停Trigger)。 Scheduler只有在调用Start()方法后,才会真正地触发trigger 。 #### Job与JobDetail *下面讲到的 **Job** 都是指的是实现 IJob 的类,例如:HelloJob* * **JobDetail** 1. JobDetail实例是通过JobBuilder类创建的。 ```c# // define the job and tie it to our HelloJob class IJobDetail job = JobBuilder.Create() .WithIdentity("myJob", "group1") .Build(); ``` 2. 注册到 Scheduler 的不是Job对象,而是 JobDetail 实例 。Job对象只是 JobDetail 实例的一部分。 ```c# await scheduler.ScheduleJob(job, trigger); ``` 可以只创建一个job类,然后创建多个与该Job关联的JobDetail实例,每一个实例都有自己的属性集和JobDataMap,最后,将所有的实例都加到scheduler中 。 3. 通过JobDetail对象,可以给job实例配置的其它属性有: * **Durability**:[StoreDurably()]如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的存在与否决定的; * **RequestsRecovery**:[RequestRecovery()]如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。 * **Job** 1. 每一个Job都必须实现IJob。例如上面的 **HelloJob**。这个类仅仅表明该job需要完成什么类型的任务,除此之外,Quartz还需要知道该Job实例所包含的属性,这将由JobDetail类来完成。 ```c# public class HelloJob : IJob { public Task Execute(IJobExecutionContext context) { Console.WriteLine("Hello World"); return Task.CompletedTask; } } ``` 2. Job的生命周期 ```C# // define the job and tie it to our HelloJob class IJobDetail job = JobBuilder.Create() .WithIdentity("myJob", "group1") .Build(); // Trigger the job to run now, and then every 40 seconds ITrigger trigger = TriggerBuilder.Create() .WithIdentity("myTrigger", "group1") .StartNow() .WithSimpleSchedule(x => x .WithIntervalInSeconds(3) .RepeatForever()) .Build(); await scheduler.ScheduleJob(job, trigger); ``` 可以看到,我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名(HelloJob)传给了JobDetail,所以scheduler就知道了要执行何种类型的job; 每次当scheduler执行job时,在调用其Execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收。 > Job 的实例要到该执行它们的时候才会实例化出来。每次 Job 被执行,一个新的 Job 实例会被创建。也就是说 Job 不必担心线程安全性,因为同一时刻仅有一个线程去执行给定 Job 类的实例,甚至是并发执行同一 Job 也是如此。 这种执行策略需要我们注意: * job必须有一个无参的构造函数; * 在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留。 那么如何给job实例增加属性或配置呢?如何在job的多次执行中,跟踪job的状态呢?答案就是:JobDataMap,JobDetail对象的一部分。 (后面会详细介绍用法) #### Trigger Trigger对象是用来触发执行Job的。当调度一个job时,我们实例一个触发器然后调整它的属性来满足job执行的条件。触发器也有一个和它相关的JobDataMap,它是用来给被触发器触发的job传参数的。 1. **SimpleTrigger**可以满足的调度需求是:在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。 ```c# ITrigger trigger = TriggerBuilder.Create() .WithIdentity("myTrigger", "group1") .StartNow() .WithPriority(5) .WithSimpleSchedule(x => x .WithIntervalInSeconds(3) .RepeatForever()) .Build(); ``` 2. **CronTrigger**基于日历的概念进行作业启动计划。 ```c# ITrigger trigger1 = TriggerBuilder.Create() .WithIdentity("myTrigger1", "group1") .ForJob(job) .WithCronSchedule("0 0/3 * * ?") .Build(); ``` CronExpression表达式 :https://www.cnblogs.com/yaowen/p/3779284.html 3. 优先级(priority) 方法:**.WithPriority(5)** 如果你的trigger很多(或者Quartz线程池的工作线程太少),Quartz可能没有足够的资源同时触发所有的trigger;这种情况下,你可能希望控制哪些trigger优先使用Quartz的工作线程,要达到该目的,可以在trigger上设置priority属性。 比如,你有N个trigger需要同时触发,但只有Z个工作线程,优先级最高的Z个trigger会被首先触发。如果没有为trigger设置优先级,trigger使用默认优先级,值为5;priority属性的值可以是任意整数,正数、负数都可以。(只有同时触发的trigger之间才会比较优先级。10:59触发的trigger总是在11:00触发的trigger之前执行。) #### Job与Trigger Trigger对于job而言就好比一个驱动器;没有触发器来定时驱动作业,作业就无法运行; 对于Job而言,一个job可以对应多个Trigger,但对于Trigger而言,一个Trigger只能对应一个job;所以一个Trigger 只能被指派给一个 Job;如果你需要一个更复杂的触发计划,你可以创建多个 Trigger 并指派它们给同一个 Job。(Trigger实例对应一个JobDetail实例,Job类可以添加到多个JobDetail实例中) Scheduler 是基于配置在 Job上的 Trigger 来决定正确的执行计划的。 #### JobDataMap 重点介绍一下JobDataMap,这是一个非常好用且被我们忽视的属性。 JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据;JobDataMap是 IDictionary 接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。 JobDetail 和 Trigger 示例都可以设置 JobDataMap(通过UsingJobData()方法) ```c# IJobDetail job = JobBuilder.Create() .WithIdentity("myJob", "group1") .UsingJobData("jobDetail", "J") .Build(); ITrigger trigger = TriggerBuilder.Create() .WithIdentity("myTrigger", "group") .UsingJobData("trigger", "T") .WithCronSchedule("0/3 * * * * ?") .Build(); await scheduler.ScheduleJob(job, trigger); ``` 传递的值可以通过IJob.Execute(IJobExecutionContext context) 中context.MergedJobDataMap获取 ```c# public class HelloJob : IJob { public Task Execute(IJobExecutionContext context) { var jobDataMap = context.MergedJobDataMap; Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-jobDetail:{jobDataMap["jobDetail"]}-trigger:{jobDataMap["trigger"]}"); return Task.CompletedTask; } } ``` 执行结果: ``` 11:36:36-jobDetail:J-trigger:T 11:36:39-jobDetail:J-trigger:T 11:36:42-jobDetail:J-trigger:T ... ``` 需要注意的是: 1. MergedJobDataMap是将JobDetail.JobDataMap和Trigger.JobDataMap的值合并的,如果key重复,将读取Trigger中相同可以的值。 2. 可以通过 context.JobDetail.JobDataMap 和 context.Trigger.JobDataMap分别读取。 3. 在IJob.Execute()方法中修改任何JobDataMap值,是不会影响到下次Job执行JobDataMap的值的。只在本次Job中有效。 那有什么办法让本次执行的状态修改,影响到以一次执行呢?即修改JobDataMap的值,下一次执行取出的是上一次修改过的?办法是有的,给Job类打标签 #### Job属性标签 **PersistJobDataAfterExecution**:将该注解加在job类上,告诉Quartz在成功执行了job类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。 ```c# [PersistJobDataAfterExecution] public class HelloJob : IJob { public Task Execute(IJobExecutionContext context) { var count = context.JobDetail.JobDataMap.GetInt("Count"); Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-Count:{count}"); count++; context.JobDetail.JobDataMap.Put("Count", count); return Task.CompletedTask; } } ``` ```c# StdSchedulerFactory factory = new StdSchedulerFactory(); var scheduler = await factory.GetScheduler(); await scheduler.Start(); IJobDetail job1 = JobBuilder.Create() .WithIdentity("myJob.1", "group") .UsingJobData("Count", "1") .Build(); ITrigger trigger1 = TriggerBuilder.Create() .WithIdentity("myTrigger.1", "group") .WithCronSchedule("0/3 * * * * ?") .Build(); await scheduler.ScheduleJob(job1, trigger1); ``` 执行结果: ``` 14:17:09-Count:1 14:17:12-Count:2 14:17:15-Count:3 ... ``` > 如果使用了[PersistJobDataAfterExecution]标签,将强烈建议同时使用[DisallowConcurrentExecution]标签,因为当同一个job(JobDetail)的两个实例被并发执行时,由于竞争,JobDataMap中存储的数据很可能是不确定的。 **DisallowConcurrentExecution**:将该注解加到job类上, 告诉Quartz不要并发地执行同一个job定义的多个实例 。 举例说明就是:将 HelloJob 添加到 job1(IJobDetail ) ```c# IJobDetail job1 = JobBuilder.Create() .WithIdentity("myJob.1", "group") .UsingJobData("flag", "myJob.1") .Build(); ``` job1绑定触发器trigger1(ITrigger) 每秒执行一次 ```c# ITrigger trigger1 = TriggerBuilder.Create() .WithIdentity("myTrigger.1", "group") .WithCronSchedule("0/1 * * * * ?") .Build(); await scheduler.ScheduleJob(job1, trigger1); ``` HelloJob 的执行耗时为2秒 ```c# public class HelloJob : IJob { public Task Execute(IJobExecutionContext context) { var flag = context.MergedJobDataMap.GetString("flag"); //模拟耗时2秒 Thread.Sleep(2000); Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-flag:{flag}"); return Task.CompletedTask; } } ``` 首先不打 DisallowConcurrentExecution 标签,看看输出结果: ``` 任务启动:14:39:05 14:39:07-flag:myJob.1 14:39:08-flag:myJob.1 14:39:09-flag:myJob.1 14:39:10-flag:myJob.1 ... ``` 通过结果输入,可以看到,第一次任务是从14:39:05开始,14:39:07结束;第二次的任务接时间是14:39:08,退出开始时间是14:39:06,以此类推...也就说前一个任务未完成,并不影响之后任务的开始 接着我们个 HelloJob 打上 DisallowConcurrentExecution 属性标签 ```c# [DisallowConcurrentExecution] public class HelloJob : IJob { public Task Execute(IJobExecutionContext context) { var flag = context.MergedJobDataMap.GetString("flag"); //模拟耗时2秒 Thread.Sleep(2000); Console.WriteLine($"{DateTime.Now.ToLongTimeString()}-flag:{flag}"); return Task.CompletedTask; } } ``` 看看输出结果: ``` 任务启动:14:47:34 14:47:36-flag:myJob.1 14:47:38-flag:myJob.1 14:47:40-flag:myJob.1 14:47:42-flag:myJob.1 ... ``` 通过结果输出可以看到,文本输出是每两秒一次,也就说,前一个任务未完成,之后任务不会开始。即不会创建一个新的 HelloJob 实例。这也就不会并发处理任务了。 > 需要注意的是,DisallowConcurrentExecution 属性标签,限制的是 JobDetail ,而不是 Job(HelloJob)。同一个JobDetail 实例创建的 Job 不会并发。但,不同的 JobDetail 实例创建的 Job 是可以并发的。 我们再创建一组关于 HelloJob 的任务:job2(IJobDetail),trigger2(ITrigger),HelloJob 不变。 ```c# class Program { static async Task Main(string[] args) { Console.WriteLine($"任务启动:{DateTime.Now.ToLongTimeString()}"); StdSchedulerFactory factory = new StdSchedulerFactory(); var scheduler = await factory.GetScheduler(); await scheduler.Start(); IJobDetail job1 = JobBuilder.Create() .WithIdentity("myJob.1", "group") .UsingJobData("flag", "myJob.1") .Build(); ITrigger trigger1 = TriggerBuilder.Create() .WithIdentity("myTrigger.1", "group") .WithCronSchedule("0/1 * * * * ?") .Build(); IJobDetail job2 = JobBuilder.Create() .WithIdentity("myJob.2", "group") .UsingJobData("flag", "myJob.2") .Build(); ITrigger trigger2 = TriggerBuilder.Create() .WithIdentity("myTrigger.2", "group") .WithCronSchedule("0/1 * * * * ?") .Build(); await scheduler.ScheduleJob(job1, trigger1); await scheduler.ScheduleJob(job2, trigger2); Console.ReadKey(); } } ``` 看看输出结果: ``` 任务启动:15:02:24 15:02:26-flag:myJob.1 15:02:26-flag:myJob.2 15:02:28-flag:myJob.1 15:02:28-flag:myJob.2 15:02:30-flag:myJob.1 15:02:30-flag:myJob.2 ... ``` 通过结果输出可以看到,同一个JobDetail,是没有每秒执行的,即前一个任务没有完成,后面的任务不会执行。但不同的JobDetail,却在同一时间执行了。 #### Job的配置 像上面示例中,我们配置Job,基本都是硬编码,我们可以把配置移到配置文件中,方便修改和添加 默认配置文件名:quartz_jobs.xml ```xml true myJob.1 group Hello World! Sayook.Schedule.Client.HelloJob, Sayook.Schedule.Client flag myJob.1 myTrigger.1 group Hello World! myJob.1 group key 1 0/3 * * * * ? ``` 我们可以将 job_scheduling_data_2_0.xsd 文件添加到 VisualStudio2019 的XML架构(直接在IDE顶部搜索框搜索xml架构),我们在编写xml配置文件的时候就会有提示和验证了。 ```c# class Program { static async Task Main(string[] args) { var properties = new NameValueCollection { ["quartz.plugin.xml.type"] = "Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz.Plugins", ["quartz.plugin.xml.fileNames"] = "quartz_jobs.xml", // this is the default ["quartz.plugin.xml.FailOnFileNotFound"] = "true", // this is not the default ["quartz.plugin.xml.failOnSchedulingError"] = "true" }; StdSchedulerFactory factory = new StdSchedulerFactory(properties); var scheduler = await factory.GetScheduler(); await scheduler.Start(); Console.ReadKey(); } } ``` ["quartz.plugin.xml.FailOnFileNotFound"] = "true", ["quartz.plugin.xml.failOnSchedulingError"] = "true" 上面两个配置文件强烈建议添加,以为这样,配置文件错误了,会有详细的异常信息抛出,以便修改,负责是不会报错,很难定位问题。 > 使用配置文件,要引用包:**Quartz.Plugins** 相关配置可查看文章:Quartz.NET 配置文件详解 https://www.cnblogs.com/abeam/p/8044460.html