在使用调度程序之前,需要实例化它(谁会猜到?)。为此,请使用SchedulerFactory。Quartz的一些用户可能在JNDI存储中保留工厂的实例,其他用户可能会发现直接实例化和使用工厂实例同样容易(或更容易)(如下面的示例)。
一旦调度程序被实例化,它就可以被启动、置于待机模式和关闭。请注意,一旦计划程序关闭,则在未重新实例化的情况下无法重新启动它。触发器在调度程序启动之前不会触发(作业不会执行),也不会在调度程序处于暂停状态时触发。
下面是一小段代码,用于实例化和启动调度程序,并调度要执行的作业:
- SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
-
- Scheduler sched = schedFact.getScheduler();
-
- sched.start();
-
- // define the job and tie it to our HelloJob class
- JobDetail job = newJob(HelloJob.class)
- .withIdentity("myJob", "group1")
- .build();
-
- // Trigger the job to run now, and then every 40 seconds
- Trigger trigger = newTrigger()
- .withIdentity("myTrigger", "group1")
- .startNow()
- .withSchedule(simpleSchedule()
- .withIntervalInSeconds(40)
- .repeatForever())
- .build();
-
- // Tell quartz to schedule the job using our trigger
- sched.scheduleJob(job, trigger);
Quartz API的关键接口包括:
通过SchedulerFactory和对其shutdown()方法的调用,调度器的生命周期受其创建的限制。一旦创建了Scheduler界面,就可以使用添加、删除和列出作业和触发器,并执行其他与调度相关的操作(例如暂停触发器)。然而,在使用start()方法启动Scheduler之前,它不会实际作用于任何触发器(执行作业),如第1课所示。
Quartz提供了定义领域特定语言(或DSL,有时也称为“流畅接口”)的“构建器”类。在上一课中,您看到了它的一个示例,我们在这里再次展示了其中的一部分:
- // define the job and tie it to our HelloJob class
- JobDetail job = newJob(HelloJob.class)
- .withIdentity("myJob", "group1") // name "myJob", group "group1"
- .build();
-
- // Trigger the job to run now, and then every 40 seconds
- Trigger trigger = newTrigger()
- .withIdentity("myTrigger", "group1")
- .startNow()
- .withSchedule(simpleSchedule()
- .withIntervalInSeconds(40)
- .repeatForever())
- .build();
-
- // Tell quartz to schedule the job using our trigger
- sched.scheduleJob(job, trigger);
构建作业定义的代码块使用从JobBuilder类静态导入的方法。同样,构建触发器的代码块使用从TriggerBuilder类和SimpleScheduleBulder类导入的方法。
DSL的静态导入可以通过如下导入语句实现:
- import static org.quartz.JobBuilder.*;
- import static org.quartz.SimpleScheduleBuilder.*;
- import static org.quartz.CronScheduleBuilder.*;
- import static org.quartz.CalendarIntervalScheduleBuilder.*;
- import static org.quartz.TriggerBuilder.*;
- import static org.quartz.DateBuilder.*;
各种“ScheduleBuilder”类具有与定义不同类型的计划相关的方法。
DateBuilder类包含各种方法,用于轻松构建特定时间点的java.util.Date实例(例如表示下一个偶数小时的日期,或者换句话说,如果当前为9:43:27,则为10:00:00)。
Job是实现Job接口的类,该接口只有一个简单的方法:
Job接口:
- package org.quartz;
-
- public interface Job {
-
- public void execute(JobExecutionContext context)
- throws JobExecutionException;
- }
当作业的触发器触发时(稍后详细介绍),execute(..)方法由调度程序的一个工作线程调用。传递给该方法的JobExecutionContext对象为作业实例提供有关其“运行时”环境的信息——执行它的调度器的句柄、触发执行的触发器的句柄、作业的JobDetail对象和其他一些项。
JobDetail对象由Quartz客户端(您的程序)在将作业添加到调度程序时创建。它包含作业的各种属性设置,以及JobDataMap,可用于存储作业类的给定实例的状态信息。它本质上是作业实例的定义,将在下一课中进一步详细讨论。
触发器对象用于触发作业的执行(或“触发”)。当您希望调度作业时,可以实例化触发器并“调整”其属性,以提供您希望的调度。触发器也可以具有与其关联的JobDataMap-这对于将特定于触发器触发的参数传递给作业很有用。Quartz附带几种不同的触发器类型,但最常用的类型是SimpleTrigger和CronTrigger。
如果您需要“一次性”执行(仅在给定时间的某个时刻执行作业),或者如果您需要在给定时间激发作业,并使其重复N次,执行之间的延迟为T,SimpleTrigger非常方便。如果您希望基于类似日历的计划进行触发,例如“每周五中午”或“每月10日10:15”,CronTrigger非常有用
为什么选择工作和触发器?许多作业调度器没有作业和触发器的单独概念。一些人将“作业”定义为简单的执行时间(或时间表)以及一些小的作业标识符。另一些则很像Quartz的工作和触发器对象的结合。在开发Quartz时,我们认为在时间表和按该时间表执行的工作之间创建分隔是有意义的。(在我们看来)这有许多好处。
例如,可以独立于触发器在作业调度程序中创建和存储作业,并且许多触发器可以与同一作业相关联。这种松散耦合的另一个好处是能够配置在相关联的触发器过期后仍留在调度程序中的作业,以便以后可以重新调度它,而不必重新定义它。它还允许您修改或替换触发器,而不需要重新定义其相关联的作业。
作业很容易实现,在接口中只有一个“execute”方法。关于作业的性质、Job接口的execute(..)方法和JobDetails,您只需要了解更多的东西。
虽然您实现的作业类具有知道如何执行特定类型作业的实际工作的代码,但Quartz需要了解您可能希望该作业的实例具有的各种属性。这是通过JobDetail类完成的,在前一节中简要提到了该类。
JobDetail实例是使用JobBuilder类构建的。您通常希望使用其所有方法的静态导入,以便在代码中具有DSL感觉。
import static org.quartz.JobBuilder.*;
现在让我们花点时间来讨论一下Jobs的“性质”和Quartz中作业实例的生命周期。
- // define the job and tie it to our HelloJob class
- JobDetail job = newJob(HelloJob.class)
- .withIdentity("myJob", "group1") // name "myJob", group "group1"
- .build();
-
- // Trigger the job to run now, and then every 40 seconds
- Trigger trigger = newTrigger()
- .withIdentity("myTrigger", "group1")
- .startNow()
- .withSchedule(simpleSchedule()
- .withIntervalInSeconds(40)
- .repeatForever())
- .build();
-
- // Tell quartz to schedule the job using our trigger
- sched.scheduleJob(job, trigger);
现在考虑这样定义的作业类“HelloJob”:
- public class HelloJob implements Job {
-
- public HelloJob() {
- }
-
- public void execute(JobExecutionContext context)
- throws JobExecutionException
- {
- System.err.println("Hello! HelloJob is executing.");
- }
- }
请注意,我们为调度程序提供了一个JobDetail实例,它知道要执行的作业类型,只需在构建JobDeail时提供作业的类。调度器每次(和每次)执行作业时,它都会在调用其execute(..)方法之前创建类的新实例。当执行完成时,将删除对作业类实例的引用,然后对实例进行垃圾收集。这种行为的后果之一是作业必须具有无参数构造函数(当使用默认的JobFactory实现时)。另一个分支是,在作业类上定义状态数据字段是没有意义的,因为它们的值不会在作业执行之间保留。
您现在可能想问“我如何为作业实例提供属性/配置?”和“我如何在执行之间跟踪作业的状态?”这些问题的答案是相同的:关键是JobDataMap,它是JobDetail对象的一部分。
JobDataMap可以用于保存任何数量的(可序列化)数据对象,这些数据对象是您希望在作业实例执行时提供给它的。JobDataMap是JavaMap接口的一个实现,并增加了一些用于存储和检索原始类型数据的便利方法。
下面是在将作业添加到调度程序之前,在定义/构建JobDetail时将数据放入JobDataMap的一些快速片段:
- // define the job and tie it to our DumbJob class
- JobDetail job = newJob(DumbJob.class)
- .withIdentity("myJob", "group1") // name "myJob", group "group1"
- .usingJobData("jobSays", "Hello World!")
- .usingJobData("myFloatValue", 3.141f)
- .build();
下面是一个在作业执行期间从JobDataMap获取数据的快速示例:
- public class DumbJob implements Job {
-
- public DumbJob() {
- }
-
- public void execute(JobExecutionContext context)
- throws JobExecutionException
- {
- JobKey key = context.getJobDetail().getKey();
-
- JobDataMap dataMap = context.getJobDetail().getJobDataMap();
-
- String jobSays = dataMap.getString("jobSays");
- float myFloatValue = dataMap.getFloat("myFloatValue");
-
- System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
- }
- }
如果使用持久化JobStore(在本教程的JobStore部分中讨论),则在决定放置在JobDataMap中的内容时应该谨慎,因为其中的对象将被序列化,因此它们很容易出现类版本控制问题。显然,标准Java类型应该是非常安全的,但除此之外,每当有人更改您已序列化实例的类的定义时,都必须注意不要破坏兼容性。(可选)您可以将JDBC JobStore和JobDataMap置于只允许在映射中存储基元和字符串的模式中,从而消除以后序列化问题的任何可能性。
如果将setter方法添加到与JobDataMap中键的名称相对应的作业类中(例如上面示例中的数据的setJobSays(String val)方法),则Quartz的默认JobFactory实现将在实例化作业时自动调用这些setter方法,从而防止需要在执行方法中显式地从映射中获取值。
触发器也可以具有与其关联的JobDataMaps。当您有一个存储在调度程序中供多个触发器定期/重复使用的作业时,这可能很有用,但对于每个独立的触发,您希望为该作业提供不同的数据输入。
作业执行期间在JobExecutionContext上找到的JobDataMap非常方便。它是在JobDetail上找到的JobDataMap和在Trigger上找到的一个JobDataMap的合并,后者中的值覆盖前者中的任何相同命名值。
下面是一个在作业执行期间从JobExecutionContext的合并JobDataMap中获取数据的快速示例:
- public class DumbJob implements Job {
-
- public DumbJob() {
- }
-
- public void execute(JobExecutionContext context)
- throws JobExecutionException
- {
- JobKey key = context.getJobDetail().getKey();
-
- JobDataMap dataMap = context.getMergedJobDataMap(); // Note the difference from the previous example
-
- String jobSays = dataMap.getString("jobSays");
- float myFloatValue = dataMap.getFloat("myFloatValue");
- ArrayList state = (ArrayList)dataMap.get("myStateData");
- state.add(new Date());
-
- System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
- }
- }
或者,如果您希望依赖JobFactory将数据映射值“注入”到类中,则它可能看起来像这样:
- public class DumbJob implements Job {
-
-
- String jobSays;
- float myFloatValue;
- ArrayList state;
-
- public DumbJob() {
- }
-
- public void execute(JobExecutionContext context)
- throws JobExecutionException
- {
- JobKey key = context.getJobDetail().getKey();
-
- JobDataMap dataMap = context.getMergedJobDataMap(); // Note the difference from the previous example
-
- state.add(new Date());
-
- System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
- }
-
- public void setJobSays(String jobSays) {
- this.jobSays = jobSays;
- }
-
- public void setMyFloatValue(float myFloatValue) {
- myFloatValue = myFloatValue;
- }
-
- public void setState(ArrayList state) {
- state = state;
- }
-
- }
您会注意到类的整体代码更长,但execute()方法中的代码更干净。人们还可以认为,尽管代码更长,但如果使用程序员的IDE自动生成setter方法,而不是必须手工编码单个调用以从JobDataMap检索值,则实际上所需的编码更少。选择权在你。
与作业一样,触发器也很容易使用,但它确实包含各种可定制的选项,在充分利用Quartz之前,您需要了解和理解这些选项。此外,如前所述,您可以从中选择不同类型的触发器来满足不同的调度需求。
两种最常见的触发器类型:
除了所有触发器类型都具有用于跟踪其标识的TriggerKey属性之外,还有许多其他属性是所有触发器类型共有的。在构建触发器定义时,可以使用TriggerBuilder设置这些常见属性(下面将给出该示例)。
下面列出了所有触发器类型的通用属性:
有时,当您有许多触发器(或Quartz线程池中有几个工作线程)时,Quartz可能没有足够的资源来立即触发计划同时触发的所有触发器。在这种情况下,您可能希望控制哪个触发器在可用的Quartz工作线程上获得第一个裂纹。为此,可以设置触发器的优先级属性。如果同时触发N个触发器,但当前只有Z个工作线程可用,则将首先执行具有最高优先级的第一个Z触发器。如果不在触发器上设置优先级,则它将使用默认优先级5。优先级允许使用任何整数值,正值或负值。
注意:仅当触发器具有相同的触发时间时,才会比较优先级。计划在10:59发射的触发器将始终在计划在11:00发射的触发器之前发射。
注意:当检测到触发器的作业需要恢复时,将以与原始触发器相同的优先级计划其恢复。
触发器的另一个重要属性是其“缺火指令”。如果持久触发器由于调度程序关闭而“错过”其触发时间,或者由于Quartz的线程池中没有用于执行作业的可用线程,则会发生缺火。不同的触发器类型具有不同的缺火指令。默认情况下,它们使用“智能策略”指令,该指令具有基于触发器类型和配置的动态行为。当调度程序启动时,它搜索任何未点火的持久触发器,然后根据它们各自配置的未点火指令更新每个触发器。当您开始在自己的项目中使用Quartz时,应该熟悉在给定触发器类型上定义并在其JavaDoc中解释的Misfire指令。有关Misfire说明的更多具体信息将在特定于每个触发器类型的教程课程中提供。
Quartz日历对象(不是java.util.Calendar对象)可以在定义触发器并将其存储在调度程序中时与触发器相关联。日历对于从触发器的触发时间表中排除时间块很有用。例如,您可以创建一个触发器,该触发器在每个工作日的上午9:30触发作业,但随后添加一个排除所有企业假日的日历。
日历可以是实现Calendar接口的任何可序列化对象,如下所示:
日历接口:
- package org.quartz;
-
- public interface Calendar {
-
- public boolean isTimeIncluded(long timeStamp);
-
- public long getNextIncludedTime(long timeStamp);
-
- }
请注意,这些方法的参数是长类型的。正如您可能猜测的那样,它们是毫秒格式的时间戳。这意味着日历可以“屏蔽”窄至毫秒的时间段。最有可能的是,你会有兴趣“封锁”整个日子。为了方便起见,Quartz包括org.Quartz.impl.HolidayCalendar类,它就是这样做的。
日历必须通过addCalendar(..)方法实例化并向调度程序注册。如果使用HolidayCalendar,则在实例化它之后,应该使用它的addExcludedDate(Date-Date)方法,以便用希望从计划中排除的日期填充它。同一日历实例可以与多个触发器一起使用,例如:
- HolidayCalendar cal = new HolidayCalendar();
- cal.addExcludedDate( someDate );
- cal.addExcludedDate( someOtherDate );
-
- sched.addCalendar("myHolidays", cal, false);
-
-
- Trigger t = newTrigger()
- .withIdentity("myTrigger")
- .forJob("myJob")
- .withSchedule(dailyAtHourAndMinute(9, 30)) // execute job daily at 9:30
- .modifiedByCalendar("myHolidays") // but not on holidays
- .build();
-
- // .. schedule job with trigger
-
- Trigger t2 = newTrigger()
- .withIdentity("myTrigger2")
- .forJob("myJob2")
- .withSchedule(dailyAtHourAndMinute(11, 30)) // execute job daily at 11:30
- .modifiedByCalendar("myHolidays") // but not on holidays
- .build();
-
- // .. schedule job with trigger2