Quartz 任务调度框架

一、背景和简介

1、产生背景

从JDK1.3开始,Java通过java.util.Timerjava.util.TimerTask可以实现定时器。为什么要使用Quartz而不是使用Java中的这些标准功能呢?

主要原因如下:

  • Timers没有持久化机制
  • Timers不灵活 (只可以设置开始时间和重复间隔,不是基于时间、日期、天等(秒、分、时)的)
  • Timers 不能利用线程池,一个timer一个线程
  • Timers没有真正的管理计划
2、简介

Quartz是一个完全由Java编写的开源任务调度的框架,通过触发器设置作业定时运行规则,控制作业的运行时间。其中quartz集群通过故障切换和负载平衡的功能,能给调度器带来高可用性和伸缩性。主要用来执行定时任务,如:定时发送信息、定时生成报表等等。

优势:

  • Quartz是非常灵活的,并包含多个使用范例,它们可以单独或一起使用,以实现您所期望的行为,并使您能够以最“自然”的方式来编写您的项目的代码。
  • Quartz是非常轻量级的,只需要非常少的配置 —— 它实际上可以被跳出框架来使用,如果你的需求是一些相对基本的简单的需求的话。
  • Quartz具有容错机制,并且可以在重启服务的时候持久化(”记忆”)你的定时任务,你的任务也不会丢失

二、构成和使用

1、Quartz的三大组件

Quartz框架主要核心组件包括调度器、触发器、作业。调度器作为作业的总指挥,触发器作为作业的操作者,作业为应用的功能模块。

  • 简介
  • Job为作业的接口,为任务调度的对象 。
  • JobDetail用来描述Job的实现类及其它相关的静态信息 ,用于包装业务代码为一个可执行的job。
  • Trigger做为作业的定时管理工具,一个Trigger只能对应一个作业实例,而一个作业实例可对应多个触发器 。
  • scheduler做为定时任务容器,是quartz最上层的东西,它提携了所有触发器和作业,使它们协调工作,每个Scheduler都存有JobDetail和Trigger的注册,一个Scheduler中可以注册多个JobDetail和多个Trigger 。

Quartz框架架构 quartz框架优缺点_任务调度

2、组件使用
  • Job

Job是一个接口,只有一个方法void execute(JobExecutionContext context),被调度的作业(类)需实现该接口中execute()方法,JobExecutionContext类提供了调度上下文的各种信息。每次执行该Job均重新创建一个Job实例 。

  • JobDetail

Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。JobDetail 用来保存我们作业的详细信息。一个JobDetail可以有多个Trigger,但是一个Trigger只能对应一个JobDetail 。

  • Trigger

Trigger是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和 CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则 可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等 。

  • Scheduler

Scheduler负责管理Quartz的运行环境,Quartz它是基于多线程架构的,它启动的时候会初始化一套线程,这套线程会用来执行一些预置的作业。Trigger和JobDetail可以注册到Scheduler中;Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。 Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

三、原理和实现

1、开发步骤:
  1. 创建Job,被执行的内容。必须有一个实现了Job接口的类作为参数,实现该接口就是为了后面调用其实现的execute()方法。和线程的run方法类似(恰巧,线程实现Runnable接口,也叫任务task);
  2. 创建trigger。时间触发了事情的执行;
  3. 创建sheduler。一定要有谁来安排这么一个事情的执行;
  4. 组装上面的三个核心组件,运行代码。
2、代码示例

导入依赖

<!-- 定时任务 -->
<dependency>
     <groupId>org.quartz-scheduler</groupId>
     <artifactId>quartz</artifactId>
     <version>2.3.0</version>
</dependency>

创建job

package com.cg.job;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 定时执行任务的任务类-测试
 * @author wanghao
 *
 */
public class TestJob01 implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 业务逻辑
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        System.out.println("任务打印"+sdf.format(new Date()));
    }
}

创建任务调度器(以simpleTrigger方式触发)

package com.cg.job;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

/**
 * 任务调度类
 * 三个元素:任务类,触发器,调度器
 */
public class TestJob01Scheduler {

    //创建调度器
    public static Scheduler getScheduler() throws SchedulerException {
        //通过工厂类获取实例
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        return schedulerFactory.getScheduler();
    }

    // 执行任务
    public static void run() throws SchedulerException{
        /*
         *  创建任务
         *  包装业务代码为一个可执行的job
         *  反射机制
         * */
        JobDetail jobDetail = JobBuilder.newJob(TestJob01.class).withIdentity("testJob01", "group11").build();
        /*
         * 创建触发器 每5秒钟执行一次(每五秒钟就触发一次)
         */
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "group21")
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
                .build();
        Scheduler scheduler = getScheduler();
        //将任务及其触发器放入调度器
        scheduler.scheduleJob(jobDetail, trigger);
        //调度器开始调度任务
        // 启动
        if (!scheduler.isShutdown()) {
            scheduler.start();
        }
    }

    public static void main(String[] args) throws SchedulerException {
        TestJob01Scheduler testJob01Scheduler = new TestJob01Scheduler();
        testJob01Scheduler.run();
    }
}

创建任务调度器(以CronTrigger方式触发)

package com.cg.job;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzManager {

    private static SchedulerFactory gSchedulerFactory = new StdSchedulerFactory();
    private static String JOB_GROUP_NAME = "EXTJWEB_JOBGROUP_NAME";
    private static String TRIGGER_GROUP_NAME = "EXTJWEB_TRIGGERGROUP_NAME";

    /**
     * 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名
     * @param jobName 任务名
     * @param cls 任务
     * @param time 时间设置
     */

    public static void addJob(String jobName, Class cls, String time) {
        try {
            // org.quartz-scheduler
            Scheduler sched = gSchedulerFactory.getScheduler();
            // 任务名,任务组,任务执行类
            //创建任务
            JobDetail jobDetail = JobBuilder.newJob(cls).withIdentity(jobName, TRIGGER_GROUP_NAME).build();
            //可以传递参数
            jobDetail.getJobDataMap().put("param", "railsboy");
            // 触发器
            //每秒钟触发一次任务
            //秒 分 时 日 月 年 星期 *=每 /=间隔  */2==每两秒 ,不连贯的值  -连贯的范围
            // ?用在星期或者日期上,二者存在冲突,解决二者冲突  L用在星期或者日期上,表示最后一天
            // w离该日最近的一个工作日,不可跨月   lw当月最后一个工作日
            // #用在星期上,4#2 表示第二个周三
            CronTrigger trigger = TriggerBuilder.newTrigger().
                    withIdentity(jobName, TRIGGER_GROUP_NAME).
                    // 每三秒执行一次
                    withSchedule(CronScheduleBuilder.cronSchedule("*/3 * * * * ? *")).
                    build();
            // 触发器时间设定
            sched.scheduleJob(jobDetail, trigger);
            // 启动
            if (!sched.isShutdown()) {
                sched.start();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        QuartzManager.addJob("Test01",TestJob01.class,"");
    }
}

Cron 表达式

Quartz框架架构 quartz框架优缺点_触发器_02

星号():可用在所有字段中,表示对应时间域的每一个时刻,例如, 在分钟字段时,表示“每分钟”;

问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;

减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;

逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;

斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;

L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五;

W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围;

LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;

井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;

C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。

Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。