Quartz 学习笔记

Quartz 学习笔记

一 简介

quartz是开源且具有丰富特性的"任务调度库",能够集成于任何的java应用,quart主要有三个核心模块:Scheduler、Job、Trigger

  1. Scheduler->调度器

    Scheduler为任务的调度器,它会将任务job及触发器Trigger整合起来,负责基于Trigger设定的时间来执行Job。

  2. Job->任务

    Job就是你想要实现的任务类,每一个Job必须实现org.quartz.job接口,且只需实现接口定义的execute()方法。

  3. Trigger->触发器

    Trigger为你执行任务的触发器,比如你想每天定时3点发送一份统计邮件,Trigger将会设置3点进行执行该任务。Trigger主要包含两种SimpleTrigger和CronTrigger两种。关于二者的区别的使用场景,后续文章会进行讨论。

二 Hello World

依赖:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

创建一个任务:

public class HelloWorldJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("初识quartz任务调度");
    }
}

创建触发器,调度器,包装任务对象,开始与停止执行任务。

public class test {
    public static void main(String[] args) throws Exception {
        // (调度器Scheduler)从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        // (任务job)由JobBuilder进行构建
        JobDetail jobDetail = JobBuilder.newJob(HelloWorldJob.class) // 待执行的任务
                .withIdentity("HelloWorld_Job", "HelloWorld_Group") // 名称与组名组成Scheduler中任务的唯一标识
                .build();// 构建
        // (触发器Trigger)由TriggerBuilder进行构建
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("HelloWorld_Trigger", "HelloWorld_Group") // 名称与组名组成Scheduler中触发器的唯一标识
                .startNow() //直接启动
                .withSchedule(  //用于定义触发器计划
                        SimpleScheduleBuilder.simpleSchedule()
                                .withIntervalInSeconds(2)   //以秒为单位指定重复间隔 - 然后乘以 1000 以产生毫秒
                                .repeatForever() //一直执行
                )
                .build();
        //把jobDetail 和 Trigger 交给 scheduler
        scheduler.scheduleJob(jobDetail, trigger);
        //启动任务
        scheduler.start();
        // 一分钟后关闭调度器
        Thread.sleep(10000);
        //停止任务
        scheduler.shutdown();
        System.out.println("结束任务");
    }
}

三 核心接口

参考资料戳我!

1:Scheduler

调度器Scheduler就相当于一个容器,装载着任务触发器

该类是一个接口,代表一个 Quartz 的独立运行容器, Trigger 和 JobDetail 可以注册到 Scheduler 中, 两者在 Scheduler 中拥有各自的组及名称, 组及名称是 Scheduler 查找定位容器中某一对象的依据, Trigger 的组及名称必须唯一, JobDetail 的组和名称也必须唯一(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。Scheduler 定义了多个接口方法, 允许外部通过组及名称访问和控制容器中 Trigger 和 JobDetail。

Scheduler 可以将 Trigger 绑定到某一 JobDetail 中, 这样当 Trigger 触发时, 对应的 Job 就被执行。一个 Job 可以对应多个 Trigger, 但一个 Trigger 只能对应一个 Job。可以通过 SchedulerFactory 创建一个 Scheduler 实例。Scheduler 拥有一个 SchedulerContext,保存着 Scheduler 上下文信息,Job 和 Trigger 都可以访问 SchedulerContext 内的信息。SchedulerContext 内部通过一个 Map,以键值对的方式维护这些上下文数据,SchedulerContext 为保存和获取数据提供了个 put() 和 getXxx() 的方法。可以通过 Scheduler.getContext() 获取对应的 SchedulerContext 实例。

一个调度器的生命周期为通过SchedulerFactory创建,直到执行其shutdown()方法。当Scheduler创建之后,可以进行增加、删除及显示任务Job与触发器Trigger,并且执行其他的调度相关的操作,如暂停一个触发器Trigger。需要注意的是,直到调用start()方法时,Scheduler才正式开始执行job和trigger。

StdSchedulerFactory用于创建Scheduler,其依赖于一系列的属性来决定如何产生Scheduler。可以通过四种途径向StdSchedulerFactory提供属性配置信息。

通过java.util.Properties初始化StdSchedulerFactory

通过外部属性文件初始化StdSchedulerFactory

通过含有属性文件内容的java.io.InputStream初始化StdSchedulerFactory

通过quartz.properties配置文件初始化StdSchedulerFactory


2:Job

Job,是你执行一个任务的Java类。该任务可以是java编码的任何功能,如使用JavaMail发送邮件、创建远程接口并调用EJB上的方法等。

Java类仅需要实现org.quartz.job接口,将所需要实现的功能放在其execute方法中。execute方法的定义如下:

public void execute(JobExecutionContext context) throws JobExecutionException;

其中JobExecutionContext对象让Job能访问Quartz运行时环境的所有信息和Job本身的明细数据。运行时环境信息包括注册到Scheduler上与该Job相关联的JobDetail和Trigger。

public class HelloWorldJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        // 获取Scheduler
        Scheduler scheduler = jobExecutionContext.getScheduler();
        // 每一个Job都有其自己所属的JobDetail
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        // 获取Trigger
        Trigger trigger = jobExecutionContext.getTrigger();

        System.out.println("JobDetail的名称和组名:"+jobDetail.getKey());
        try {
            System.out.println("Scheduler名称:"+scheduler.getSchedulerName());
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
        System.out.println("任务执行时间:"+jobExecutionContext.getFireTime());
        System.out.println("任务下次执行时间:"+jobExecutionContext.getNextFireTime());
    }
}

3:JobDetail

JobDetail是用来表达给定Job的详细属性信息的,作者的原话是:“Conveys the detail properties of a given Job instance.”,
JobDetail是一个接口,只有一个实现类JobDetailImpl,这是真正保存Job详细属性的类。在JobDetailImpl中主要有以下参数

参数 类型 默认值 说明
name String 任务名称
group String Scheduler.DEFAULT_GROUP = “DEFAULT” 任务组名称
description String 任务描述
jobClass Class<? extends Job> 真正需要执行的Job任务类
jobDataMap JobDataMap 实现了Map接口,用于保存需要传递给Job的参数信息
durability boolean false 当没有trigger与Job关联时,是否需要保存Job
shouldRecover boolean false 如果遇到“恢复”或“故障转移”情况,Scheduler是否应重新执行Job
key JobKey JobDetail的唯一标识,他由任务名称(name)和任务组名称(group)组成

job

JobDetail是作为 Job实例 进行定义的,Job的实例要到该执行它们的时候才会实例化出来。每次Job被执行,一个新的Job实例会被创建。其中暗含的意思就是你的Job不必担心线程安全性,因为同一时刻仅有一个线程去执行给定Job类的实例,甚至是并发执行同一Job也是如此。

可以使用 JobDataMap 来定义Job的状态,JobDataMap中可以存入key-value对,这些数据可以在Job实现类中进行传递和访问。这是向你的Job传送配置信息的便捷方法。

Job能通过 JobExecutionContext 对象访问 JobDataMap

使用JobDataMap:

//创建一个
JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("name","王思尧");
        jobDataMap.put("age",30);

// 通过.usingJobData()添加参数
JobDetail jobDetail = JobBuilder.newJob(HelloWorldJob.class) // 待执行的任务
        .usingJobData(jobDataMap)// 向Job传送信息
        .withIdentity("HelloWorld_Job", "HelloWorld_Group") // 名称与组名组成Scheduler中任务的唯一标识
        .build();// 构建

Job中使用:

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        System.out.println("初识quartz任务调度:name:"+mergedJobDataMap.get("name")+";age:"+mergedJobDataMap.get("age"));
    }

Tips:部署在Scheduler上的每一个Job只创建一个JobDetail实例。且需要注意的是注册到Scheduler上的不是Job对象,而是JobDetail实例。


@PersistJobDataAfterExecution

有状态的Job可以理解为多次Job调用期间可以持有一些状态信息,这些状态信息存储在JobDataMap中,而默认的无状态job每次调用时都会创建一个新的JobDataMap。

@PersistJobDataAfterExecution 的作用在于持久化保存在JobDataMap中的传递参数,使得多次执行Job,可以获取传递参数的状态信息。

我们在jobDataMap 中添加一个count

JobDetail jobDetail = JobBuilder.newJob(HelloWorldJob.class) // 待执行的任务
        .usingJobData(jobDataMap)// 向Job传送信息
        .usingJobData("count", 0) // 将count初始化为0
        .withIdentity("HelloWorld_Job", "HelloWorld_Group") // 名称与组名组成Scheduler中任务的唯一标识
        .build();// 构建

Job中使用@PersistJobDataAfterExecution注解,并改变count的值之后重新赋值给jobDataMap

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        //JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
        int count = jobDataMap.getInt("count");
        ++count;
        jobDataMap.put("count", count);
        System.out.println("初识quartz任务调度:name:"+jobDataMap.get("name")+";count:"+ count);
    }

Tips:看到注释的这行带么了么,用那种方式获取的jobDataMap并不会具有状态,源码注释:

获得此执行上下文的便利 JobDataMap 性。
在此对象上找到的JobDataMap是为了方便 - 它是在JobDetail上找到的,和在Trigger上找到的JobDataMap合并,后者中的值覆盖前者中的 任何同名值。因此,作业的执行代码从此对象上的 JobDataMap 检索数据被认为是一种“最佳实践”。
注意:不要期望此 JobDataMap 中的值“set”以某种方式被设置或持久化回作业自己的 JobDataMap 上 - 即使它有@PersistJobDataAfterExecution 注释。

尝试更改此映射的内容通常会导致 IllegalStateException.

另一点需要说明的是,这个注解对Trigger中的jobDataMap是无效的

还有一种比较骚的玩儿法:我在Job中创建一个属性,与jobDataMap的key同名字,并加上set方法,这样会总动把数据解析到这个属性上,在方法中可以直接使用属性,下面打印的两个内容是相同的。

@PersistJobDataAfterExecution
public class HelloWorldJob implements Job {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        System.out.println("初识quartz任务调度:name:"+jobDataMap.get("name")+";name:"+ name);
    }
}

这种方式是从jobExecutionContext.getMergedJobDataMap()这个方法中获取的属性,所以会出现上面Tips中出现的问题,所以用的时候注意一下


@DisallowConcurrentExecution

上面我们说到,Job的执行是并发的,也就是第二个执行Job的时候他并不会理会上一次是否执行完了。但是有些时候我们并不希望如此,我们希望在上一个Job执行完了之后再执行以下依次,为此我们只需要在Job头上加上这个注解就好了。

@DisallowConcurrentExecution该注解可以同一个时刻,同一个任务只能执行一次,不能并行执行两个或多个同一任务。

@DisallowConcurrentExecution
public class HelloWorldJob implements Job {
    private String name;
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("初识quartz任务调度:name:"+jobDataMap.get("name")+";name:"+ name);
    }
}

Tips:要注意的是,多个不同的任务是可以同时执行的。


4: Trigger

Trigger最常用的有两种SimpleTrigger和CronTrigger,首先介绍Trigger的一些基础的信息,然后会详细描述这两种Trigger。

通用属性

quartz中所有的触发器Trigger都有一些共有属性,如TriggerKey,startTime等,这些属性可以使用TriggerBuilder进行设置。常用的属性举例如下:

  1. triggerKey:触发器的标识,由名称与分组唯一指定,便于调度器调用与查找。
  2. jobKey: 当触发器被触发时,标识哪一个任务Job应该被执行。
  3. startTime: 表示触发器第一次开始触发的时间。
  4. endTime: 表示触发器终止触发的时间。

优先级

当存在多个触发器时,quartz可能没有足够的资源立即触发所有配置为同一时间触发的triggers,因此可以设置每个Trigger的优先级。默认的优先级为5,可取任意的整型值,包括正数或负数。注意:优先级仅用于所有相同时间触发的triggers。


未启动指令"Misfire Instructions"

Trigger未触发一般产生于调度器被关闭,或线程池不足时。不同的Trigger类型有不同的未启动指令。默认的,他们会使用"smart policy"指定。这些指令的使用场景在于,当scheduler开启时,它将搜索所有未启动的持久化的触发器,然后基于触发器各自配置"未启动指令"来更新触发器。未启动指令用于当trigger未正常触发时,是否恢复执行的场景。


Calendars

与Trigger关联的Calendar对象,用于在指定的时间内不触发trigger,例如你有一个任务每天执行一次,但你不希望在节假日执行时,Calendar此时派上用场。注意此Calendar为quartz自身的定义接口,而非Java自带的Calendar。

Calendar需要在Scheduler定义过程中,通过scheduler.addCalendar()进行初始化和注册。


SimpleTrigger

当需要在规定时间执行一次或在规定的时间段以一定的时间间隔重复触发执行Job时,SimpleTrigger就可以满足。

SimpleTrigger的属性有:开始时间、结束时间、重复次数和重复的时间间隔。重复次数属性的值可以为0、正整数、或常量 SimpleTrigger.REPEAT_INDEFINITELY,重复的时间间隔属性值必须为0或长整型的正整数,以毫秒作为时间单位,当重复的时间间隔为0时,意味着与Trigger同时触发执行。如果有指定结束时间属性值,则结束时间属性优先于重复次数属性,这样的好处在于:当我们需要创建一个每间隔10秒钟触发一次直到指定的结束时间的 Trigger,而无需去计算从开始到结束的所重复的次数,我们只需简单的指定结束时间和使用REPEAT_INDEFINITELY作为重复次数的属性 值即可。

创建在特定时间触发,非重复的Trigger

SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
    .withIdentity("trigger1", "group1")//使用具有给定名称和组的触发键来标识触发器
    .startAt(new Date()) // 设置触发器应启动的时间
    .forJob("job1", "group1") // 设置应由生成的触发器触发的作业的标识
    .build();

创建在特定时间触发,重复间隔为10次,重复执行10次的Trigger

SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
    .withIdentity("trigger3", "group1")
    .startNow() //直接启动  
    .withSchedule(simpleSchedule()
        .withIntervalInSeconds(10) //以秒为单位指定重复间隔 - 然后乘以 1000 以产生毫秒
        .withRepeatCount(10)) //指定触发器将重复的次数 - 触发总数将为此数字 + 1。
    .forJob(jobDetail) //设置应由生成的触发器触发的作业的标识。                   
    .build();

创建5分钟后执行,且触发一次的Trigger

trigger = (SimpleTrigger) newTrigger()
    .withIdentity("trigger5", "group1")
    .startAt(futureDate(5, IntervalUnit.MINUTE)) // 使用日期生成器创建将来的日期
    .forJob(myJobKey) // 使用作业键识别作业
    .build();

创建立即执行,且重复间隔为5分钟,到22点结束的Trigger

trigger = newTrigger()
    .withIdentity("trigger7", "group1")
    .withSchedule(simpleSchedule()
        .withIntervalInMinutes(5) //以分钟为单位指定重复间隔 - 然后乘以 1000 以产生毫秒
        .repeatForever()) //一直执行
    .endAt(dateOf(22, 0, 0)) //设置触发器不再触发的时间 dateOf是Quartz 提供了一个时间构建器DateBuilder 中的内容
    .build();

构件SimpleTrigger的时候,可以指定Trigger的未启动指令:

trigger = newTrigger()
    .withIdentity("trigger7", "group1")
    .withSchedule(simpleSchedule()
        .withIntervalInMinutes(5)//以分钟为单位指定重复间隔 - 然后乘以 1000 以产生毫秒
        .repeatForever()//一直执行
        .withMisfireHandlingInstructionNextWithExistingCount()) //如果触发器未触发,请使用该指令。
    .build();

关于处理策略可参考这里


CronTrigger

CronTrigger 支持比SimpleTrigger更具体、更复杂的调度。基于cron表达式,CronTrigger支持类似日历的重复间隔,而非单一的时间间隔。

大致内容如下:详细介绍可以参考这里

序号 说明 是否必填 允许填写的值 允许的通配符
1 0-59 , - * /
2 0-59 , - * /
3 小时 0-23 , - * /
4 1-31 , - * ? / L W
5 1-12或JAN-DEC , - * /
6 1-7或SUN-SAT , - * ? / L #
7 空或1999-2017 , - * /

5: JobBuilder

参考这里

JobBuilder采用build模式,用于实例化一个JobDetail。
JobBuilder的属性与JobDetail一模一样(除了没有name和group),当我们使用JobBuild的其他方法时都是将Job详细信息先保存到JobBuilder中,最后调用build方法时,new一个JobDetailImpl,然后将保存的参数都设置到JobDetailImpl中,然后返回。

6: TriggerBuilder

参考这里

TriggerBuilder是用来实例化Trigger的。他与JobBuilder在整体结构或者说设计风格上可以说是一模一样的,同样采用Builder模式。

posted @ 2023-09-05 21:54  抓哇攻城狮  阅读(78)  评论(0)    收藏  举报