# 更多关于作业和作业详细

正如你在第2课中看到的,Jobs接口很容易实现,界面中只有一个“execute”方法。关于作业的性质、作业接口的execute(..)方法以及JobDetails,您还需要了解一些其他内容。

虽然您实现的作业类具有知道如何执行特定作业类型的实际工作的代码,但Quartz需要了解您可能希望该作业实例具有的各种属性。这是通过JobDetail类完成的,这在上一节中简要介绍过。

JobDetail实例是使用JobBuilder类生成的。您通常希望使用其所有方法的静态导入,以便在代码中有DSL的感觉。

import static org.quartz.JobBuilder.*;

现在让我们花点时间来讨论一下Jobs接口的“本质”以及Quartz中的作业实例的生命周期。首先让我们回顾一下我们在第1课中看到的一些代码片段:

  //定义作业详情对象并将其绑定到我们的HelloJob类中
  JobDetail job = newJob(HelloJob.class)
      .withIdentity("myJob", "group1") //名为myJob,组为group1
      .build();

  //触发该作业立即运行,并每40秒一次
  Trigger trigger = newTrigger()
      .withIdentity("myTrigger", "group1")
      .startNow()
      .withSchedule(simpleSchedule()
          .withIntervalInSeconds(40)
          .repeatForever())            
      .build();

  //通知Quartz使用我们的触发器安排该工作
  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可用于保存任何数量的(可串行化的)数据对象,你希望这些对象在作业实例执行时对其可用。JobDataMapJava Map接口的一种实现,它为存储和检索原始类型的数据添加了一些方便的方法。

以下是在将作业添加到调度器之前,在定义/构建JobDetail时将数据放入JobDataMap的一些简短片段:

  //定义作业详情对象并将其绑定到我们的DumbJob类中
  JobDetail job = newJob(DumbJob.class)
      .withIdentity("myJob", "group1") //名为myJob,组为group1
      .usingJobData("jobSays", "Hello World!") //属性jobSays的值为Hello World!
      .usingJobData("myFloatValue", 3.141f)	//属性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类型应该是非常安全的,但除此之外,每当有人更改已序列化实例的类的定义时,都必须注意不要破坏兼容性。可选的,您可以将JDBCJobStoreJobDataMap设置为仅允许在映射中存储原语和字符串的模式,从而消除了以后出现序列化问题的可能性。

如果将setter方法添加到与JobDataMap中键的名称相对应的作业类中(例如上面示例中数据的setJobSays(Stringval)方法),则Quartz的默认JobFactory实现将在作业实例化时自动调用这些setter方法,从而防止需要在执行方法中显式地从映射中获取值。

触发器还可以具有与其关联的JobDataMaps。如果您有一个存储在调度程序中的作业供多个触发器定期/重复使用,但对于每个独立的触发器,您希望为该作业提供不同的数据输入,则这一点非常有用。

JobExecutionContext上找到的JobDataMap在作业执行期间提供了方便。它是JobDetail上的JobDataMapTrigger上的Job DataMap的合并,后者中的值覆盖前者中的任何相同命名值。

以下是在作业执行期间从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();  //注意和上面的例子之间的区别

      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();  //注意和上面的例子之间的区别

      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”,它们在相应的JobDataMap中指定了“joe”和“mike”作为相应作业的输入。

触发触发器时,将加载与其关联的JobDetail(实例定义),并通过调度器上配置的JobFactory实例化其引用的作业类。默认JobFactory只需在作业类上调用newInstance(),然后尝试在类上调用与JobDataMap中键名称匹配的setter方法。您可能希望创建自己的JobFactory实现,以完成诸如让应用程序的IoC或DI容器生成/初始化作业实例之类的任务。

用“Quartz语言”来说,我们将每个存储的JobDetail称为“作业定义”或“JobDetail实例”,将每个正在执行的作业称为“任务实例”或“作业定义的实例”。通常,如果我们只使用单词“job”,我们指的是一个命名的定义,或JobDetail。当我们提到实现作业接口的类时,我们通常使用术语“作业类”。

# 作业状态和并发性

现在附件一些关于作业的状态数据(也称为JobDataMap)和并发性的说明。有一些注释可以添加到Job类中,这些注释会影响Quartz在这些方面的行为。

@DisallowConcurrentExecution是一个可以添加到Job类的注释,它告诉Quartz不要同时执行给定作业定义(引用给定作业类)的多个实例。

注意这里的说法,因为它是非常仔细地选择的。在上一节的示例中,如果“SalesReportJob”具有此注释,则在给定时间只能执行一个“SalesReportForJoe”实例,但它可以与一个“Sales ReportForMike”实例同时执行。约束基于实例定义(JobDetail),而不是作业类的实例。然而,(在Quartz的设计过程中)决定将注释放在类本身上,因为它通常会对类的编码方式产生影响。

@PersistJobDataAfterExecution是一个可以添加到Job类的注释,该注释告诉Quartz在execute()方法成功完成后(没有引发异常)更新JobDetailJobDataMap的存储副本,以便同一作业(JobDetail)的下一次执行接收更新的值,而不是原始存储的值。与@DisallowConcurrentExecution注释一样,这适用于作业定义实例,而不是作业类实例,尽管决定让作业类携带该属性,因为它通常会对类的编码方式产生影响(例如,execute方法中的代码需要显式“理解”“状态”)。

如果使用@PersistJobDataAfterExecution注释,则应强烈考虑同时使用@DisallowConcurrentExecution注释,以避免在同时执行同一作业(JobDetail)的两个实例时可能会混淆(竞争条件)存储的数据。

# 作业的其他属性

以下是可以通过JobDetail对象为作业实例定义的其他属性的摘要:

  • 持久性[Durability] - 如果作业不持久,那么一旦不再有任何与之相关的活动触发器,它就会自动从调度程序中删除。换句话说,非持久性工作的寿命受其触发因素的存在所限制。

  • 请求恢复[RequestsRecovery] - 如果作业“请求恢复”,并且它在调度器“硬关闭”期间执行(即它正在崩溃中运行的进程,或机器关闭),那么当调度器再次启动时,它将被重新执行。在本例中,“JobExecutionContext.isRecovery()”方法将返回true。

# 作业执行异常

最后,我们需要告知你Job.execute(..)方法的一些细节。唯一允许从execute方法抛出的异常类型(包括RuntimeExceptions)是JobExecutionException。因此,通常应使用“try-catch”块包装execute方法的全部内容。你还应该花一些时间查看JobExecutionException的文档,因为你的作业可以使用它向调度程序提供有关如何处理异常的各种指令。


微信公众号

QQ交流群
原创网站开发,偏差难以避免。

如若发现错误,诚心感谢反馈。

愿你倾心相念,愿你学有所成。

愿你朝华相顾,愿你前程似锦。