Quartz Tutorial 2 - More About Jobs and Job Details

Quartz Tutorial 2 - More About Jobs and Job Details

Quartz Tutorial 2 - More About Jobs and Job Details

就像是在上一节看到的,Job相当容易去实现,毕竟接口中只有一个execute方法。这里只需要再了解一些别的事情你就能理解作业的本质,关于Job接口中的execute方法,关于JobDetail类。

当一个实现了Job接口的类定义完成,它要实际进行的工作也就很清晰了,但Quartz还需要知道你希望一个作业实例所具有的其它属性。那些属性是通过JobDetail类来告知Quartz的,前面已经简单介绍过。

JobDetail实例是用JobBuilder类来构建的,可以使用静态导入该类中所有的静态方法,使代码具有DSL风格。

1
import static org.quartz.JobBuilder.*;

首先花点时间讨论一下Job的本质和一个Job实例在Quartz内的生命周期。首先回顾一下第一节中的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 = JobBuilder.newJob(HelloJob.class)
    .withIdentity("myJob", "group1")
    .build();

// Trigger the job to run now, and then every 40 seconds
Trigger trigger = TriggerBuilder
    .newTrigger()
    .withIdentity("myTrigger", "group1")
    .startNow()
    .withSchedule(SimpleScheduleBuilder
        .simpleSchedule()
        .withIntervalInSeconds(40)
        .repeatForever())
    .build();

// Tell quartz to schedule the job using our trigger
sched.scheduleJob(job, trigger);

作业的实体类HelloJob是这样定义的

1
2
3
4
5
6
public class HelloJob implements Job {
    public HelloJob() {}
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.err.println("Hello!  HelloJob is executing.");
    }
}

注意到,我们给调度器传递了一个JobDetail实例,它知道我们要执行的作业的实际类型,因为我们构造它的时候传递进去了。每次调度器执行这个作业之前,它都会先创建一个新的HelloJob实例,然后调用execute方法。作业一旦执行完毕,调度器对这个对象实例的引用便会丢弃,这个实例就会被垃圾回收。能这样做的一个条件是,Job的实现类必须有一个公共的无参构造器(当然,此时使用的是默认的JobFactory);另一个就是这个实现类不需要有什么表示状态的数据成员,这没什么意义,一旦作业执行完毕,这些数据成员也就不再保留了。

那么我们如何为作业实例提供属性或者配置?或者怎样在执行过程中继续跟踪它的状态呢?这里提供了JobDataMap,它也是JobDetail对象的一部分。

JobDataMap

JobDataMap可以持有你想让作业执行期间能访问到的(可序列化的)数据对象,它是Map接口的一种实现,此外还添加了一些方法使之能方便存取基本类型的数据。

如下一段代码就是在定义/构建JobDetail阶段,将一些数据放置到JobDataMap对象中。

1
2
3
4
5
6
// 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中获取数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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-JobStoreJobDataMap置于只能放基本类型和字符串的模式,这样就能消除未来可能的序列化问题。

如果在你的作业类中添加了set访问器,而且它的名字与JobDataMap中某个键的名字是符合的(例如setJobSays(String val)jobSays),那么Quartz的默认JobFactory类会在实例化这个作业的时候自动调用这些方法,这样就不用在代码中显式获取了。

触发器也可以拥有与之关联的JobDataMap。当你有个储存在调度器中的作业要被多个触发器常规/重复使用的时候,它就非常有用了。对于每次互相独立的触发,你可以给作业提供不同的输入数据。

在作业执行期间可以在JobExecutionContext找到一个JobDataMap,它合并了在JobDetailTrigger中的JobDataMap,而且,对于相同名字的值,后者将会覆盖掉前者。

下面是一个从JobExecutionContext中找到并使用合并后的JobDataMap的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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的注入功能,如下例所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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()方法也确实清晰了。而且像是set构造器可以用IDE自动生成。

Job “Instance”

有很多用户对于一个“作业实例”的确切形式感到疑惑,我们在这一节和后面两小节将之解释清楚。

你可以创建一个单独的作业类,然后通过在调度器内创建多个JobDetail实例来保存它的“实例定义”,每个JobDetail都有他自己的属性集合及JobDataMap,然后将它们都放到调度器中。

例如,你可以创建一个实现了Job接口的类,叫做SalesReportJob。这个作业类需要接受一个参数,来得知消费者的名字然后生成针对他的消费报告。因此可能需要创建多个作业的定义,例如“SalesReportForJoe”或“SalesReportForMike”。

当一个触发器被触发,其关联的JobDetail实例将会被加载,然后它用到的作业类将会通过调度器配置的JobFactory来实例化。默认的JobFactory只是简单的调用newInstance(),然后尝试调用名字和JobDataMap中名字匹配的set访问器来赋值。或许你会需要构造你自己的JobFactory的实现来做一些事,例如你自己的控制反转或依赖注入容器来生成/初始化作业实例。

我们将每个JobDetail称为一个“作业定义”或者“作业明细的实例”;我们还将每个正在执行的作业称为一个“作业实例”或“作业定义的一个实例”。通常,如果我们只是简单用到了“作业”一词,这指的是一个命名好的定义,或者作业明细。如果我们要说Job接口的实现类,我们会使用“作业类”这个词语。

Job State and Concurrency

现在我们将解释一些关于作业的状态数据(JobDataMap)和并发性。它们是能加到你的作业类定义上的注解,它们将会影响Quartz的行为。

@DisallowConcurrentExecution注解是加到作业类上的,它告知Quartz不要并发执行给定作业的多个实例。注意这里的措辞,这是反复推敲过的。在上一节的例子中,如果“SalesReportJob”有这个注解,在给定时刻,只能有一个“SalesReportForJoe”运行,但是它可以和一个“SalesReportForMike”同时运行。这个限制是基于JobDetail,而不是基于作业类的实例。

@PersistJobDataAfterExecution也是给作业类加的注解。它告知Quartz,只有execute方法完全成功执行完毕之后才更新存储JobDetailJobDataMap,因为同一个作业的下一次执行将会使用更新后的值,而不是原来的值。就像上面介绍的@DisallowConcurrentExecution注解,它也是限制作业定义实例而不是作业类实例的。

如果你使用了@PersistJobDataAfterExecution注解,那你也应当认真考虑是否也使用@DisallowConcurrentExecution注解,这是为了避免同一作业的两个实例同时执行时出现可能的竞态条件。

Other Attributes Of Jobs

这里是其它能通过JobDetail对象对作业进行定义的属性:

  • Durability - 如果一个作业是非耐用的,那么一旦调度器中没有活动触发器与之关联,他将会被自动删除。换句话说,非持久作业的生命周期受到与它关联的触发器限制。
  • RequestsRecovery - 如果一个作业“请求恢复”,而且在它执行期间被调度器“强制关闭”(例如进程运行中崩溃,或者电脑关机),那么当调度器再次启动时,这个作业也将再次执行。这种情况下,JobExecutionContext.isRecovering()将返回true

JobExecutionException

最后,我们需要介绍Job.execute方法的一些其它细节。在该方法中,唯一能丢出的异常(包括运行时异常)类型是JobExecutionException。因此你应该使用try-catch语句块包裹execute方法的全部内容。你也应当花时间看看这个异常的文档,因为你的作业能够适应该异常来告知调度器具体使用哪种方式来处理这个出现异常的作业。