第3课:更多关于工作和工作详情
正如你在第2课中看到的,Jobs很容易实现,在接口中只有一个“execute”方法。 您只需要了解有关作业性质,Job界面的execute(..)方法以及JobDetails的更多内容。
虽然您实现的作业类具有知道如何做特定类型作业的实际工作的代码,但Quartz需要被通知您可能希望该作业的实例具有的各种属性。 这通过JobDetail类完成,在上一节中简要介绍。
JobDetail实例使用JobBuilder类构建。 您通常会使用静态导入其所有方法,以便在您的代码中具有DSL感觉。
import static org.quartz.JobBuilder.*;
现在让我们稍等一点,讨论一下乔布斯的“自然”和“石英”中的工作实例的生命周期。 首先让我们回顾一下我们在第1课中看到的一段代码:
// 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实例,并且通过在构建JobDetail时简单地提供作业的类,它知道要执行的作业的类型。 调度程序执行作业的每一个(和每个)时间,它在调用其execute(..)方法之前创建一个新的类的实例。 执行完成后,对作业类实例的引用将被删除,然后将该实例进行垃圾回收。 这种行为的一个后果是,作业必须具有无参数构造函数(使用默认JobFactory实现时)。 另一个结论是,在作业类上定义状态数据字段是没有意义的 - 因为它们的值不会在作业执行之间保留。
您可能现在想要问“如何为Job实例提供属性/配置”?“以及”如何跟踪执行之间的工作状态?“这些问题的答案是一样的:关键是JobDataMap ,它是JobDetail对象的一部分。
JobDataMap
JobDataMap可用于保存任何您希望在执行时对作业实例可用的数据对象(可序列化)。 JobDataMap是Java Map接口的一个实现,并且具有用于存储和检索原始类型的数据的一些额外的便利方法。
在将作业添加到调度程序之前,以下是定义/构建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放入只允许将图元和字符串存储在地图中的模式,从而消除后续序列化问题的任何可能性。
如果将Jobter方法添加到与JobDataMap中的键名称相对应的作业类(例如上述示例中的数据的setJobSays(String val)方法),则Quartz的默认JobFactory实现将在作业被实例化,从而防止在执行方法中明确地将值从地图中取出。
触发器也可以与其相关联的JobDataMaps。 这在您有一个Job存储在调度程序中以供多次触发器常规/重复使用的情况下可用,但是对于每个独立触发,您希望为Job提供不同的数据输入。
在Job执行期间在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中检索值。 这是你的选择。
工作“实例”
许多用户花费时间对什么构成“工作实例”感到困惑。 我们将尝试清除这里和下面关于作业状态和并发的部分。
您可以创建单个作业类,并通过创建JobDetails的多个实例(每个具有自己的一组属性和JobDataMap)并将它们全部添加到调度程序中,在调度程序中存储许多“实例定义”。
例如,您可以创建一个实现名为“SalesReportJob”的Job接口的类。 该作业可能被编码,以期望发送给它的参数(通过JobDataMap)来指定销售人员应该基于的销售人员的名称。 然后,他们可以创建作业的多个定义(JobDetails),例如“SalesReportForJoe”和“SalesReportForMike”,它们在相应的JobDataMaps中指定的“joe”和“mike”作为相应作业的输入。
当触发器触发时,与其相关联的JobDetail(实例定义)被加载,并且通过在Scheduler上配置的JobFactory来实例引用它引用的作业类。 默认的JobFactory只是在作业类上调用newInstance(),然后尝试在与JobDataMap中的键名匹配的类上调用setter方法。 您可能需要创建自己的JobFactory实现,以完成诸如让应用程序的IoC或DI容器生成/初始化作业实例之类的操作。
在“Quartz talk”中,我们将每个存储的JobDetail称为“作业定义”或“JobDetail实例”,我们将每个执行的作业称为“作业实例”或“作业定义的实例”。 通常,如果我们使用“job”这个词,我们指的是一个命名定义,或者JobDetail。 当我们指的是实现作业界面的类时,通常使用术语“作业类”。
作业状态和并发性
现在,关于作业的状态数据(又名JobDataMap)和并发的一些附加说明。 有几个注释可以添加到Job类中,这些注释会影响Quartz关于这些方面的行为。
@DisallowConcurrentExecution是一个注释,可以添加到Job类中,该类可以告诉Quartz不要同时执行给定作业定义(引用给定作业类)的多个实例。
注意那里的措辞,因为它被非常仔细地选择。 在上一节的示例中,如果“SalesReportJob”具有此注释,则只能在给定时间执行一个“SalesReportForJoe”实例,但它可以与“SalesReportForMike”实例同时执行。 约束基于实例定义(JobDetail),而不是作业类的实例。 然而,决定(在Quartz的设计期间)将注释载入类本身,因为它通常会对类的编码方式产生影响。
@PersistJobDataAfterExecution是一个注释,可以添加到Job类中,该类可以告诉Quartz在execute()方法成功完成后(而不会抛出异常)更新JobDetail的JobDataMap的存储副本,以便下次执行相同的作业JobDetail)接收更新的值而不是原始存储的值。 像@DisallowConcurrentExecution注释一样,这适用于作业定义实例,而不是作业类实例,尽管它决定让作业类携带属性,因为它通常会对类的编码有所不同(例如“状态” '需要被execute方法中的代码明确地“理解”)。
如果您使用@PersistJobDataAfterExecution注释,则应强烈考虑使用@DisallowConcurrentExecution注释,以避免同时执行同一作业(JobDetail)的两个实例时可能存在什么数据的可能混淆(竞争条件)。
工作的其他属性
以下是通过JobDetail对象为作业实例定义的其他属性的快速摘要:
- 耐用性 - 如果作业是非持久性的,则在不再有任何与之相关联的活动触发器时,它将自动从调度程序中删除。 换句话说,非耐用工作的生命周期是由触发器的存在所限制的。
- RequestsRecovery - 如果作业“请求恢复”,并且它在调度程序的“严重关机”(即,在崩溃中运行的进程,或者机器被关闭)的时间内执行,则重新执行当调度程序再次启动时。 在这种情况下,JobExecutionContext.isRecovering()方法将返回true。
JobExecutionException
最后,我们需要通知你几个Job.execute(..)
方法的细节。 允许从execute方法抛出的唯一类型的异常(包括RuntimeExceptions)是JobExecutionException。 因此,您通常应该使用“try-catch”块来包装execute方法的全部内容。 您还应该花一些时间查看JobExecutionException的文档,因为您的工作可以使用它为调度程序提供有关如何处理异常的各种指令。