Quartz定时任务框架(一):概述和Job、JobDetail详解

Quartz定时任务框架

核心概念

  • Job
  • JobDetail
  • Trigger
  • Scheduler

Job:

既然是定时任务,任务任务对吧,job就是你要定时干的事。比如就在控制台打印Hello World

JobDetail:

Job只是定义你要干什么,可以理解为模板不能直接用。需要创建一个Job实例,这个实例就是JobDetail。

Trigger:

触发器,规定怎么执行,5s一次还是10s一次。

Scheduler:

调度器、执行器。它可以根据触发器Trigger去执行Job

Hello World 入门

我们用结果驱动学习,先来一个Hello World玩玩。
依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

从上面的概念可以理解到:我们要输出Hello World,这不就是一个任务(Job)吗?来:

Job是一个接口,我们写一个类继承即可:

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("Hello World!");
    }
}

我滴任务完成啦~~~!!

任务就是这么简单。


任务有了,接下来就是触发器Trigger、调度器Scheduler。

写一个main方法就能搞定了:

public class TestScheduler {
    public static void main(String[] args) throws SchedulerException {

        // 创建调度器
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 创建任务实例(传入job类)
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("hello", "job")
                .build();
        
        // 创建触发器
        SimpleTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger", "trigger")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
                .build();
        
        // 调度器接管任务和触发器
        scheduler.scheduleJob(jobDetail, trigger);
        // 开始执行
        scheduler.start();
    }
}

我们现在只需要知道,现在每5s钟,控制台就会打印Hello World

我们现在不要去纠结那些方法是干嘛的(让结果驱动学习)。

可以看见触发器Trigger和任务Job都是有一个xxxBuilder的构建对象!!!!

调度器则是有一个工厂类获取的。

一切就是这么简单。

动手操作一遍,运行main方法:

image-20220427145533412

接下来,我们慢慢深入

深入学习

原理概述

Job为一个接口,只有一个方法:

public interface Job {
    void execute(JobExecutionContext var1) throws JobExecutionException;
}

属实很朴素。

job的一个 trigger 被触发后(稍后会讲到),execute() 方法会被 scheduler 的一个工作线程调用;传递给 execute() 方法的 JobExecutionContext 对象中保存着该 job 运行时的一些信息 ,执行 job 的 scheduler 的引用,触发 job 的 trigger 的引用,JobDetail 对象引用,以及一些其它信息。(当job的触发器被触发时,scheduler调度器的一个线程会执行job的execute方法,同时会传递一些参数)

JobDetail 对象是在将 job 加入 scheduler 时,由客户端程序(你的程序)创建的。它包含 job 的各种属性设置,以及用于存储 job 实例状态信息的 JobDataMap

Trigger 用于触发 Job 的执行。当你准备调度一个 job 时,你创建一个 Trigger 的实例,然后设置调度相关的属性。Trigger 也有一个相关联的 JobDataMap,用于给 Job 传递一些触发相关的参数。Quartz 自带了各种不同类型的 Trigger,最常用的主要是 SimpleTriggerCronTrigger

SimpleTrigger 主要用于一次性执行的 Job(只在某个特定的时间点执行一次),或者 Job 在特定的时间点执行,重复执行 N 次,每次执行间隔T个时间单位。CronTrigger 在基于日历的调度上非常有用,如“每个星期五的正午”,或者“每月的第十天的上午 10:15”等。

为什么既有 Job,又有 Trigger 呢?很多任务调度器并不区分 Job 和 Trigger。有些调度器只是简单地通过一个执行时间和一些 job 标识符来定义一个 Job;其它的一些调度器将 Quartz 的 Job 和 Trigger 对象合二为一。在开发 Quartz 的时候,我们认为将调度和要调度的任务分离是合理的。在我们看来,这可以带来很多好处。

例如,Job 被创建后,可以保存在 Scheduler 中,与 Trigger 是独立的,同一个 Job可以有多个 Trigger;这种松耦合的另一个好处是,当与 Scheduler 中的 Job 关联的 trigger 都过期时,可以配置 Job 稍后被重新调度,而不用重新定义 Job;还有,可以修改或者替换 Trigger,而不用重新定义与之关联的 Job。


将 Job 和 Trigger 注册到 Scheduler 时,可以为它们设置 key,配置其身份属性。

image-20220427151613343

Job 和 Trigger 的 key(JobKey 和 TriggerKey)可以用于将 Job 和 Trigger 放到不同的分组(group)里,然后基于分组进行操作。同一个分组下的 Job 或 Trigger 的名称必须唯一,即一个 Job 或 Trigger 的 key 由名称(name)和分组(group)组成。

Job和JobDetail

当你定义了一个实现Job接口的类,这个类仅仅表明该job需要完成什么任务。

除此之外,Quartz还需要知道该Job实例所包含的属性而这将由JobDetail类来完成。

不妨再看看HelloWorld中的代码:

// 创建任务实例(传入job类)
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
    .withIdentity("hello", "job")
    .build();

// 创建触发器
SimpleTrigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("trigger", "trigger")
    .startNow()
    .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
    .build();

// 调度器接管任务和触发器
scheduler.scheduleJob(jobDetail, trigger);

因为我们在创建JobDetail对象的时候,把要执行的Job类型传递了进来。

而JobDetail对象又和触发器一起交给了调度器Scheduler。

所以,调度器Scheduler知道将要执行的任务类型。

只不过,调度器每次执行这个任务的时候,每次都会重新创建任务对象。

这个时候我们思考一个问题:如何给Job对象添加属性来追踪job的状态呢?

答案是:,因为每次执行都会创建一个新的job任务对象,所以无法直接在任务中添加什么属性。

但是,你是否还记得前文原理概述时说过:

public interface Job {
    void execute(JobExecutionContext var1) throws JobExecutionException;
}

JobExecutionContext这个参数:Scheduler执行任务execute方法时,会传递很多的参数,包括JobDetail的对象。

都在这个参数中。

不知道此时你有没有恍然大悟。

既然无法直接在任务上动手脚,就用一个外部对象来,每次执行任务的时候,都传给你这个任务。这个外部对象就是JobDetail,而需要增加的属性就放在JobDetail对象的JobDataMap

我们在execute方法中打个断点,看看JobExecutionContext都有些啥:

image-20220427154246130

JobDataMap

JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据。JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数据的方法

我们就来试试,稍微改动下HelloWorld代码:

// 创建任务实例
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
    .withIdentity("hello", "job")
    .usingJobData("xp","好帅好帅")
    .build();

只需要加上.usingJobData("xp","好帅好帅")

然后我们去任务的execute方法中取这个参数:

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
    String xp = jobDataMap.getString("xp");
    System.out.println("Hello World!" + xp);
}

效果:

image-20220427155017683

在决定JobDataMap中存放什么数据的时候需要小心,因为JobDataMap中存储的对象都会被序列化,因此很可能会导致类的版本不一致的问题;Java的标准类型都很安全,如果你已经有了一个类的序列化后的实例,某个时候,别人修改了该类的定义,此时你需要确保对类的修改没有破坏兼容性

当然,你也可以做一些配置。使得map中仅允许存储基本类型和String类型的数据,这样可以避免后续的序列化问题。


JobExecutionContext中的JobDataMap为我们提供了很多的便利。它是JobDetail中的JobDataMap和Trigger中的JobDataMap的并集,但是如果存在相同的数据,则后者会覆盖前者的值。(打上断点,细心的你可能会发现JobExecutionContext中有好几个jobDataMap)

在execute方法中,可以合并这几个jobDataMap为一个。

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
	JobDataMap mergedJobDataMap = jobExecutionContext.getMergedJobDataMap();
	String xp = mergedJobDataMap.getString("xp");
	System.out.println("Hello World!" + xp);
}

同样也能拿到值,而且这样很方便。不用一会从这个jobDataMap拿数据,一会从另一个拿。


如果你在job类中,为JobDataMap中存储的数据的key增加set方法,那么Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样你就不需要在execute()方法中显式地从map中取数据了。

不明白?上代码,就拿我保存的xp这个值来说

public class MyJob implements Job {

    private String xp;

    /** set方法 */
    public void setXp(String value) {
        this.xp = value;
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("Hello World!" + xp);
    }

}

运行效果:

image-20220427161257714

可以看到,只需要在job类中定义这个属性,添加set方法。就直接帮我们赋值了。

直接使用,简直不要太方便。

Job实例

你可以只创建一个job类,然后创建多个与该job关联的JobDetail实例,每一个实例都有自己的属性集和JobDataMap,最后,将所有的实例都加到scheduler中。

比如,你创建了一个实现Job接口的类“SalesReportJob”。该job需要一个参数(通过JobdataMap传入),表示负责该销售报告的销售员的名字。因此,你可以创建该job的多个实例(JobDetail),比如“SalesReportForJoe”、“SalesReportForMike”,将“joe”和“mike”作为JobDataMap的数据传给对应的job实例。

当一个trigger被触发时,与之关联的JobDetail实例会被加载,JobDetail引用的job类通过配置在Scheduler上的JobFactory进行初始化。默认的JobFactory实现,仅仅是调用job类的newInstance()方法,然后尝试调用JobDataMap中的key的setter方法。你也可以创建自己的JobFactory实现,比如让你的IOC或DI容器可以创建/初始化job实例。


一个任务(Job类)可以有多个任务实例(JobDetail),即:

// 创建任务实例
JobDetail jobDetailA = JobBuilder.newJob(MyJob.class)
    .withIdentity("helloA", "job")
    .usingJobData("xpA","好帅好帅")
    .build();
JobDetail jobDetailB = JobBuilder.newJob(MyJob.class)
    .withIdentity("helloB", "job")
    .usingJobData("xpB","你小子真流弊")
    .build();

但是一个任务实例(JobDetail)只能和一个Trigger对应!

Job的数据状态和并发

关于job的状态数据(即JobDataMap)和并发性,还有一些地方需要注意。

@DisallowConcurrentExecution:将该注解加到job类上,告诉Quartz不要并发地执行同一个job定义(这里指特定的job类)的多个实例

@DisallowConcurrentExecution
public class MyJob implements Job {
    
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("Hello World!");
    }

}

该注解限制是针对JobDetail的,而不是job类

@PersistJobDataAfterExecution:将该注解加在job类上,告诉Quartz在成功执行了job类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。和 @DisallowConcurrentExecution注解一样,尽管注解是加在job类上的,但其限制作用是针对job实例的,而不是job类的。

如果你使用了@PersistJobDataAfterExecution注解,我们强烈建议你同时使用@DisallowConcurrentExecution注解,因为当同一个job类的两个实例被并发执行时,由于竞争,JobDataMap中存储的数据很可能是不确定的。

Job的其它特性

通过JobDetail对象,可以给job实例配置的其它属性有:

  • Durability:如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的存在与否决定的;
  • RequestsRecovery:如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。
// 创建任务实例
JobDetail jobDetailA = JobBuilder.newJob(MyJob.class)
    .requestRecovery(false) // 是否可恢复
    .storeDurably(false) // 是否持久
    .withIdentity("helloA", "job")
    .usingJobData("xpA","好帅好帅")
    .build();

由于篇幅原因,打算分开些。不然一篇得很长了。这里只是说完了Job和JobDetail,后续更精彩。

  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为了我的架构师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值