对简单的Quartz了解的不简单一些
Quartz.Net的关键接口
- Scheduler - 与调度程序交互的主要API。[ IScheduler]
- Job - 由希望由调度程序执行的组件实现的接口。[IJob]
- JobDetail - 用于定义作业的实例。[IJobDetail]
- Trigger(即触发器) - 定义执行给定作业的计划的组件。[ITrigger]
- JobBuilder - 用于定义/构建JobDetail实例,用于定义作业的实例。
- TriggerBuilder - 用于定义/构建触发器实例。
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<HelloJob>()
.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
- JobDetail实例是通过JobBuilder类创建的。
// define the job and tie it to our HelloJob class IJobDetail job = JobBuilder.Create<HelloJob>() .WithIdentity("myJob", "group1") .Build();
- 注册到 Scheduler 的不是Job对象,而是 JobDetail 实例 。Job对象只是 JobDetail 实例的一部分。
await scheduler.ScheduleJob(job, trigger);
可以只创建一个job类,然后创建多个与该Job关联的JobDetail实例,每一个实例都有自己的属性集和JobDataMap,最后,将所有的实例都加到scheduler中 。
-
通过JobDetail对象,可以给job实例配置的其它属性有:
Durability:[StoreDurably()]如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的存在与否决定的;
RequestsRecovery:[RequestRecovery()]如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。
-
Job
-
每一个Job都必须实现IJob。例如上面的 HelloJob。这个类仅仅表明该job需要完成什么类型的任务,除此之外,Quartz还需要知道该Job实例所包含的属性,这将由JobDetail类来完成。
public class HelloJob : IJob { public Task Execute(IJobExecutionContext context) { Console.WriteLine("Hello World"); return Task.CompletedTask; } }
-
Job的生命周期
// define the job and tie it to our HelloJob class IJobDetail job = JobBuilder.Create<HelloJob>() .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传参数的。
-
SimpleTrigger可以满足的调度需求是:在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。
ITrigger trigger = TriggerBuilder.Create() .WithIdentity("myTrigger", "group1") .StartNow() .WithPriority(5) .WithSimpleSchedule(x => x .WithIntervalInSeconds(3) .RepeatForever()) .Build();
-
CronTrigger基于日历的概念进行作业启动计划。
ITrigger trigger1 = TriggerBuilder.Create() .WithIdentity("myTrigger1", "group1") .ForJob(job) .WithCronSchedule("0 0/3 * * ?") .Build();
CronExpression表达式 :https://www.cnblogs.com/yaowen/p/3779284.html
-
优先级(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<TKey, TValue> 接口的一个实现,额外增加了一些便于存取基本类型的数据的方法。
JobDetail 和 Trigger 示例都可以设置 JobDataMap(通过UsingJobData()方法)
IJobDetail job = JobBuilder.Create<HelloJob>()
.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获取
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
...
需要注意的是:
- MergedJobDataMap是将JobDetail.JobDataMap和Trigger.JobDataMap的值合并的,如果key重复,将读取Trigger中相同可以的值。
- 可以通过 context.JobDetail.JobDataMap 和 context.Trigger.JobDataMap分别读取。
- 在IJob.Execute()方法中修改任何JobDataMap值,是不会影响到下次Job执行JobDataMap的值的。只在本次Job中有效。
那有什么办法让本次执行的状态修改,影响到以一次执行呢?即修改JobDataMap的值,下一次执行取出的是上一次修改过的?办法是有的,给Job类打标签
Job属性标签
PersistJobDataAfterExecution:将该注解加在job类上,告诉Quartz在成功执行了job类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。
[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;
}
}
StdSchedulerFactory factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
await scheduler.Start();
IJobDetail job1 = JobBuilder.Create<HelloJob>()
.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 )
IJobDetail job1 = JobBuilder.Create<HelloJob>()
.WithIdentity("myJob.1", "group")
.UsingJobData("flag", "myJob.1")
.Build();
job1绑定触发器trigger1(ITrigger) 每秒执行一次
ITrigger trigger1 = TriggerBuilder.Create()
.WithIdentity("myTrigger.1", "group")
.WithCronSchedule("0/1 * * * * ?")
.Build();
await scheduler.ScheduleJob(job1, trigger1);
HelloJob 的执行耗时为2秒
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 属性标签
[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 不变。
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<HelloJob>()
.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<HelloJob>()
.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 version="1.0" encoding="UTF-8"?>
<job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0">
<processing-directives>
<overwrite-existing-data>true</overwrite-existing-data>
</processing-directives>
<schedule>
<job>
<name>myJob.1</name>
<group>group</group>
<description>Hello World!</description>
<job-type>Sayook.Schedule.Client.HelloJob, Sayook.Schedule.Client</job-type>
<job-data-map>
<entry>
<key>flag</key>
<value>myJob.1</value>
</entry>
</job-data-map>
</job>
<trigger>
<cron>
<name>myTrigger.1</name>
<group>group</group>
<description>Hello World! </description>
<job-name>myJob.1</job-name>
<job-group>group</job-group>
<job-data-map>
<entry>
<key>key</key>
<value>1</value>
</entry>
</job-data-map>
<cron-expression>0/3 * * * * ?</cron-expression>
</cron>
</trigger>
</schedule>
</job-scheduling-data>
我们可以将 job_scheduling_data_2_0.xsd 文件添加到 VisualStudio2019 的XML架构(直接在IDE顶部搜索框搜索xml架构),我们在编写xml配置文件的时候就会有提示和验证了。
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
应用示例
基于.NetCore的依赖注入,对Quartz.Net的使用
示例里面包含:
- 可视化面板控制的调度应用[Sayook.Schedule.Manager]
- 使用控制台应用程序创建的 泛型主机 调用应用[Sayook.Schedule.Client]
通过xml文件配置Job,后续维护、新增Job,对代码的改动都非常小, 是最轻量级的使用。