使用 Quartz.Net 和 Job Listeners & Scheduler Plugins 按顺序运行作业






4.64/5 (4投票s)
在本文中,我们将了解如何使用 Quartz.Net 调度器按顺序(一个接一个地)运行作业。我们还将了解 Job Listeners 和 Quartz 调度器插件。
引言
Quartz.Net 是一个非常流行的、名为 Quartz 的开源作业调度库的 .Net 移植版本(可以集成到各种 Java 应用程序中),开发人员已经使用多年。Quartz 允许用户通过简单地添加/更新少数配置文件中的信息来调度多个作业,例如 quartz_jobs.xml、Quartz.Server.exe.xml 和 quartz.xml。Quartz 利用多线程的基本原理来执行作业,因此它并行运行作业。我们可以通过更改其设置来告诉 Quartz 阻止并发执行作业,但要告诉它在作业一之后执行作业二,在作业二之后执行作业三,这并不像更改设置/配置那样直接。让我们通过一个问题陈述并找到一个合适的解决方案来更好地理解这一点。
注意:要正确理解本文,具备 Quartz 的基本知识非常重要。请通过此处的 Quartz.Net 文档/示例进行查阅。
问题
假设我们有两个作业 - 作业一和作业二。作业一收集信息,并根据某些逻辑对其进行修改和过滤后将其插入数据库表,而作业二则创建报表并使用作业一插入的数据向用户发送报表。因此,在没有先运行作业一的情况下运行作业二是没有意义的,否则我们的用户将收到可能为空或不是最新的(旧数据)报表。所以,必须在作业一完成后才能运行作业二。
寻找解决方案
为了实现这一点,有些人会建议我们应该基于常识调度作业二在作业一运行 15 或 20 分钟后运行,并希望作业一届时已完成。首先,我们无法保证作业一会在 15-20 分钟内完成。这有多种原因,例如,数据库中的数据量太大,或者数据库服务器过载并且响应非常缓慢等等。其次,我们怎么能确定作业一已成功执行且没有遇到任何异常?
为了克服上述挑战,我们将使用 Quartz.Net 的 ISchedulerPlugin 和 IJobListner 接口来编写我们自己的插件和作业监听器。我们将在下面的代码中详细了解这些。
先决条件:
在继续编写代码之前,让我们确保我们已经具备或了解以下内容:
- 将 Quartz.Net 安装为 Windows 服务并进行测试。我使用的是最新版本的 Quartz.Net,即 2.3.3。可以通过此处提到的说明轻松完成。
- 为 Quartz.Net Windows 服务配置日志记录。请遵循此处提到的说明。这将帮助我们通过查看日志文件来跟踪我们的作业正在做什么。
使用代码
现在我们已经将 Quartz.Net 安装为 Windows 服务并配置了日志记录,让我们开始编写代码。
我使用 Visual Studio 创建一个新项目(类库)。使用 Nuget 添加 Quartz.dll 到引用,或使用 Quartz 安装文件夹中的 Quartz.dll(例如,D:\Program Files\Quartz.net,这是我安装 Quartz 的位置。如果您还记得,我们在先决条件步骤中将 Quartz 安装为了 Windows 服务)。确保安装文件夹中的 Quartz.dll 版本与类库项目引用的版本相同。
现在**添加 JobOne 的类**并添加以下代码。
using Quartz; using System; namespace SequentialQuartz_POC { public class JobOne : IJob { /// <summary> /// empty constructor for job initialization /// </summary> public JobOne() { // quartz requires a public empty constructor so that the // scheduler can instantiate the class whenever it needs. } public void Execute(IJobExecutionContext context) { Console.WriteLine("Job one started"); // we can basically write code here for the stuff // which we want our JobOne to do like inserting data into DB, etc. System.Threading.Thread.Sleep(10000); Console.WriteLine("Job one finished"); } } }
同样,**添加 JobTwo 的类**和以下代码。
namespace SequentialQuartz_POC { public class JobTwo: IJob { /// <summary> /// empty constructor for job initialization /// </summary> public JobTwo() { // quartz requires a public empty constructor so that the // scheduler can instantiate the class whenever it needs. } public void Execute(IJobExecutionContext context) { Console.WriteLine("Job two started"); // we can basically write code here for the stuff // which we want our JobOne to do like sending emails to users with reports as attachments, etc. System.Threading.Thread.Sleep(10000); Console.WriteLine("Job two finished"); } } }
请注意,在两个类中我们都实现了 IJob 接口。这使得调度器能够识别这些是需要调度和执行的作业。此外,在两个类中我们都有一个空构造函数,这是 quartz 调度器在需要时实例化类所必需的。
创建作业后,我们将创建一个 Job Listener 类。但在此之前……
什么是 Quartz.Net 监听器?
Quartz.Net 监听器是可以在 Quartz.Net 调度器内发生某些事件时获得通知的对象。有 3 种类型的监听器:
- 调度器监听器
- 作业监听器
- 触发器监听器
在我们的例子中,我们需要向调度器添加一个作业监听器。当我们想要获得作业级别事件的通知时,就会使用作业监听器。要创建它,我们需要实现 **IJobListener** 接口。该接口有 3 个方法(JobExecutionVetoed、JobToBeExecuted 和 JobWasExecuted)和一个属性(Name)。以下是创建我们的作业监听器的代码。
using System; using Quartz; namespace SequentialQuartz_POC { /// <summary> /// this listener class has methods which run before and after job execution /// </summary> public class JobListenerExample : IJobListener { /// <summary> /// to dismiss/ban/veto a job, we should return true from this method /// </summary> /// <param name="context"></param> public void JobExecutionVetoed(IJobExecutionContext context) { // this gets called before a job gets executed // by returning true from here we can basically prevent a job or all jobs from execution // Do nothing } /// <summary> /// this gets called before a job is executed /// </summary> /// <param name="context"></param> public void JobToBeExecuted(IJobExecutionContext context) { Console.WriteLine("Job {0} in group {1} is about to be executed", context.JobDetail.Key.Name, context.JobDetail.Key.Group); } /// <summary> /// this gets called after a job is executed /// </summary> /// <param name="context"></param> /// <param name="jobException"></param> public void JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException) { Console.WriteLine("Job {0} in group {1} was executed", context.JobDetail.Key.Name, context.JobDetail.Key.Group); // only run second job if first job was executed successfully if (jobException == null) { // fetching name of the job to be executed sequentially string nextJobName = Convert.ToString(context.MergedJobDataMap.GetString("NextJobName")); if (!string.IsNullOrEmpty(nextJobName)) { Console.WriteLine("Next job to be executed :" + nextJobName); IJobDetail job = null; // define a job and tie it to our JobTwo class if (nextJobName == "JobTwo") // similarly we can write/handle cases for other jobs as well { job = JobBuilder.Create<JobTwo>() .WithIdentity("JobTwo", "JobTwoGroup") .Build(); } // create a trigger to run the job now ITrigger trigger = TriggerBuilder.Create() .WithIdentity("SimpleTrigger", "SimpleTriggerGroup") .StartNow() .Build(); // finally, schedule the job if (job != null) context.Scheduler.ScheduleJob(job, trigger); } else { Console.WriteLine("No job to be executed sequentially"); } } else { Console.WriteLine("An exception occured while executing job: {0} in group {1} with following details : {2}", context.JobDetail.Key.Name, context.JobDetail.Key.Group, jobException.Message); } } /// <summary> /// returns name of the listener /// </summary> public string Name { get { return "JobListenerExample"; } } } }
正如您在上面的代码中看到的,**JobWasExecuted** 是最重要的一个方法。此方法在调度器执行任何作业后被调用。在这里,我们使用此方法来检查 JobOne 的完成情况(也检查是否有异常),然后调度 JobTwo 在其之后执行。我们必须在 Quartz 安装文件夹中的 quartz_jobs.xml 文件中添加一个键条目,该条目将保存当前作业(JobOne)之后要执行的下一个作业(JobTwo)的信息。因此,一旦 JobOne 完成,JobTwo 将开始运行。
以下是作业的 job-data-map 标签内的键条目。
<schedule> <job> <name>JobOne</name> <group>JobOneGroup</group> <description>Sample job for Quartz Server</description> <job-type>SequentialQuartz_POC.JobOne, SequentialQuartz_POC</job-type> <durable>true</durable> <recover>false</recover> <job-data-map> <entry> <key>NextJobName</key> <value>JobTwo</value> </entry> </job-data-map> </job> <trigger> <simple> <name>sampleSimpleTrigger</name> <group>sampleSimpleGroup</group> <description>Simple trigger to simply fire sample job</description> <job-name>JobOne</job-name> <job-group>JobOneGroup</job-group> <misfire-instruction>SmartPolicy</misfire-instruction> <repeat-count>-1</repeat-count> <repeat-interval>100000</repeat-interval> </simple> </trigger> </schedule>
这里的问题是,**调度器将如何知道我们刚刚创建的作业监听器?**例如,调度器通过读取 Quartz 安装文件夹中的 quartz_jobs.xml 文件来了解我们创建的作业。不幸的是,无法通过 XML 或配置文件让调度器了解此作业监听器。为了解决这个问题,我们将创建一个自定义插件,并在 Quartz 安装文件夹(在我这里是 D:\Program Files\Quartz.net)中的 quartz.xml 文件中进行配置/注册,然后使用此插件将作业监听器添加到调度器中。让我们看看如何做到这一点。
现在,我们将**创建一个自定义插件**。每当需要在调度器开始执行作业之前或之后执行某些操作时,插件都会非常有用。
Quartz 提供了一个接口 (ISchedulerPlugin) 来插入附加功能。
Quartz 附带的插件可提供各种实用功能,可以在 Quartz.Plugins 命名空间中找到文档。它们提供诸如调度器启动时自动调度作业、记录作业和触发器事件历史以及确保虚拟机退出时调度器正常关闭等功能。
**在我们的例子中**,**我们希望在调度器开始执行任何作业之前将作业监听器附加到我们的调度器**。
**创建我们的插件类**,如下所示:
using Quartz; using Quartz.Spi; using Quartz.Impl.Matchers; namespace SequentialQuartz_POC { /// <summary> /// this class is for our plugin. We will use it to add a job listener to our scheduler /// </summary> public class PluginExample : ISchedulerPlugin { public void Initialize(string pluginName, IScheduler scheduler) { scheduler.ListenerManager.AddJobListener(new JobListenerExample(), EverythingMatcher<JobKey>.AllJobs()); } public void Shutdown() { //Do Nothing } public void Start() { //Do Nothing } } }
在上面的代码中,我们使用了插件的 initialize 方法将作业监听器添加到我们的调度器。为了让调度器知道这个新添加的自定义插件,请转到 Quartz 安装文件夹,打开 quartz.xml 文件,然后添加以下代码。
# job initialization plugin handles adding a listener for our jobs quartz.plugin.PluginExample.type = SequentialQuartz_POC.PluginExample, SequentialQuartz_POC
编写上述代码时要小心。它应该结构如下:
quartz.plugin.{name}.type = {type name}, {assembly name}
{name} 部分是您想给插件起的名称,也是传递给 Initialize 方法的名称。属性 {type name}, {assembly name} 的值告诉调度器插件的类型,以便它可以加载到内存中。 {type name} 是我们插件的完整名称,在本例中是 Examples.PluginExample。 {assembly name} 是您的程序集文件的名称,不带 .dll 扩展名。
就是这样。只需将我们的 Visual Studio 解决方案以 Release 模式构建,将 bin 文件夹中的项目 dll 复制并粘贴到 Quartz 安装文件夹中。**我们现在可以启动 Quartz Windows 服务来执行我们的作业。** 转到 services.msc 并查找 Quartz 服务。启动它。过一段时间,转到 Quartz 安装文件夹,您会在那里找到一个 trace 文件夹。进入其中并打开 application.log.txt 文件。您会在日志中找到类似以下的行。
这意味着我们的调度器已成功启动。我们也可以在作业/插件/监听器代码中添加自定义日志记录,而不是/除了 Console.WriteLine()。这样,我们就可以在一个单独的日志文件中跟踪所有作业信息和异常详细信息。
要查看我们在代码中添加的所有 Console.WriteLine 消息,请停止 Quartz Windows 服务,转到 Quartz 安装文件夹,找到 Quartz.Server(应用程序)并双击它。这将打开一个控制台窗口,并在此处显示以下消息。
关注点
因此,我们刚刚了解了如何在另一个作业成功执行后调度作业。我们可以扩展相同的逻辑来按顺序运行多个作业。我附上了我为此创建的项目以及我使用的 Quartz 设置。如果您有任何疑问或无法下载代码,请告知我。
历史
在此处保持您所做的任何更改或改进的实时更新。