Quartz 基本概念讲解

354 阅读10分钟

定时任务框架 - Quartz

内容来自: java教程之精品详解Quartz,企业中热门实用的技能【黑马程序员】

1 核心概念

  • 任务Job

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

  • 触发器 Trigger

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

  • 调度器Scheduler

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

2 体系结构

image.png

3 Quartz的几个常用API

  • Scheduler 用于与调度程序交互的主程序接口。
  • Scheduler 调度程序-任务执行计划表
  • Job 我们预先定义的希望在未来时间能被调度程序执行的任务类,我们可以自定义。
  • JobDetail 使用JobDetail来定义定时任务的实例,JobDetail实例是通过JobBuilder类创建的。
  • JobDataMap 可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用 其中的数据;JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数 据的方法。
  • Trigger 触发器,Trigger对象是用来触发执行Job的。当调度一个job时,我们实例一个触发器然后调整它的属性来满足job执行的条件。表明任务在什么时候会执行。定义了一个已经被安排的任务将会在什么时候执行的时间条件,比如每2秒就执行一次。
  • JobBuilder -用于声明一个任务实例,也可以定义关于该任务的详情比如任务名、组名等,这个声明的实例将会作为一个实际执行的任务。
  • TriggerBuilder 触发器创建器,用于创建触发器trigger实例。
  • JobListener、TriggerListener、SchedulerListener监听器,用于对组件的监听

4 Quartz的使用

4.1 准备工作

maven 项目
创建HelloJob任务类

// 定义任务类
public class HelloJob implements Job {

	@Override
	public void execute(JobExecutionContext arg0) throws JobExecutionException {
		// 定义时间
		Date date = new Date();
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateString = dateFormat.format(date);
		// 定义工作任务内容
		System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
	}
}

创建任务调度类HelloSchedulerDemo

public class HelloSchedulerDemo {

	public static void main(String[] args) throws Exception {
		// 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("job1", "group1") // 定义该实例唯一标识
                .build();

        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 定义该实例唯一标识
                .startNow()  // 马上执行
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
        .repeatSecondlyForever(5)) // 每5秒执行一次   
                .build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
	}
}
4.2 Job、JobDetail、JobExecutionContext、JobDataMap
  • Job:
    • 工作任务调度的接口,任务类需要实现该接口, eg: HelloJob implements Job,该类中定义了execute方法
    • Job 实例: 在 Quartz 中的生命周期:每次调度器执行 Job 时,它在调用 execute 方法前会创建一个新的 Job 实例,当调用完成后,关联的 Job 对象实例会被释放,释放的实例会被垃圾回收机制回收
  • JobDetail:
    • JobDetail 为 Job 实例提供了许多设置的属性,以及 JobDataMap 成员变量属性,它用来存储特定 Job 实例的状态信息,调度器需要借助 JobDetail 对象来添加Job实例
    • JobDetail 重要属性: name, group, jobClass, jobDataMap
JobDetail job = JobBuilder.newJob(HelloJob.class)
        .withIdentity("job1", "group1") // 定义该实例唯一标识,并指定一个组。
        .build();
  
System.out.println("name:"+job.getKey().getName()); // key的名称
System.out.println("group:"+job.getKey().getGroup()); // 组的名称,默认为Default
System.out.println("jobClass:"+job.getJobClass().getName()); // 任务的类 
  • JobExecutionContext:

    • 当Scheduler调用一个Job,就会将JobExecutionContext传递给Job的execute()方法;
    • Job能通过JobExecutionContext对象访问到Quartz运行时候的环境以及Job本身的明细数据。
  • JobDataMap:

    • JobDataMap存储在JobExecutionContext中 ,非常方便获取
    • JobDataMap可以用来装载任何可序列化的数据对象,当job实例对象被执行时这些参数对象会传递给它
    • JobDataMap实现了JDK的Map接口,并且添加了非常方便的方法用来存取基本数据类型
4.3 有状态的Job和无状态的Job
  1. 当前是无状态,即每次调用时都会创建一个新的JobDataMap

修改boot程序,添加.usingJobData(“count”, 0),其表示计数器。

JobDetail job = JobBuilder.newJob(HelloJob.class)
    		.withIdentity("job1", "group1") // 定义该实例唯一标识
    		.usingJobData("message", "打印日志")
    		.usingJobData("count", 0)
    		.build();

修改HelloJob.java

private Integer count;
public void setCount(Integer count) {
	this.count = count;
}

在public void execute(JobExecutionContext context) throws JobExecutionException的方法中添加。

++count;
System.out.println("count数量:"+count);
context.getJobDetail().getJobDataMap().put("count", count);

我们发现,打印的count始终为1

  1. 如何变成有状态呢?使用@PersistJobDataAfterExecution 注解
@PersistJobDataAfterExecution
public class HelloJob implements Job {
}
4.4 Trigger 触发器

image.png

  1. jobKey

    • 表示job实例的标识,触发器被触发时,该指定的job实例会被执行
  2. startTime

    • 表示触发器的时间表,第一次开始被触发的时间,数据类型是java.util.Date
  3. endTime

    • 指定触发器终止被触发的时间,数据类型是java.util.Date
4.5 CronTrigger

使用场景:需要像日历那样按日程来触发任务,而不是像SimpleTrigger 那样每隔特定的间隔时间触发,CronTriggers通常比SimpleTrigger更有用,因为它是基于日历的作业调度器。

使用CronTrigger,你可以指定诸如“每个周五中午”,或者“每个工作日的9:30”或者“从每个周一、周三、周五的上午9:00到上午10:00之间每隔五分钟”这样日程安排来触发。甚至,象SimpleTrigger一样,CronTrigger也有一个startTime以指定日程从什么时候开始,也有一个(可选的)endTime以指定何时日程不再继续

4.6 Cron表达式

Cron表达式被用来配置CronTrigger实例。Cron表达式是一个由7个子表达式组成的字符串。每个子表达式都描述了一个单独的日程细节。这些子表达式用空格分隔,分别表示:

  1. Seconds 秒
  2. Minutes 分钟
  3. Hours 小时
  4. Day-of-Month 月中的天
  5. Month 月
  6. Day-of-Week 周中的天
  7. Year (optional field) 年(可选的域)

image.png

5 Quartz任务调度

5.1 配置、资源SchedulerFactory

所有的Scheduler实例由SchedulerFactory创建 Quartz的三个核心概念:调度器、任务、触发器,三者之间的关系是:

image.png

注意!

  • 我们知道一个作业,比较重要的三个要素就是Schduler,jobDetail,Trigger;而Trigger 对于job而言就好比一个驱动器;没有触发器来定时驱动作业,作业就无法运行;
  • 对于Job而言,一个job可以对应多个Trigger,但对于Trigger而言,一个Trigger只能对应一个job;所以一个Trigger 只能被指派给一个 Job;如果你需要一个更复杂的触发计划,你可以创建多个 Trigger并指派它们给同一个 Job

Scheduler的创建方式:StdSchedulerFactory

  1. 使用一组参数(java.util.Properties)来创建和初始化Quartz调度器
  2. 配置参数一般存储在quartz.properties文件中
  3. 调用getScheduler方法就能创建和初始化调度器对象
SchedulerFactory schedulerFactory = new StdSchedulerFactory();	
Scheduler scheduler = schedulerFactory.getScheduler();

用法1: 输出调度时间

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("调度器开始的时间是:"+dateFormat.format(scheduler.scheduleJob(job, trigger)));

用法2: 启动任务调度

scheduler.start();

用法3: 任务调度挂起

// Scheduler 执行2秒后自动挂起
Thread.sleep(2000L);
scheduler.standby();	
// Scheduler 执行5秒后自动开启
Thread.sleep(5000L);
scheduler.start();	

用法四:关闭任务调度

// Scheduler执行2秒后自动挂起
Thread.sleep(2000L);
scheduler.shutdown();
// Scheduler执行5秒后自动开启Thread.sleep(5000L);
scheduler.start();

区分

  • shutdown(true)表示等待所有正在执行的job执行完毕之后,再关闭scheduler
  • shutdown(false)即直接关闭scheduler
5.2 Quartz.properties

我们也可以在项目的资源下添加quartz.properties文件,去覆盖底层的配置文件。

  1. 调度器属性:
    • org.quartz.scheduler.instanceName: 属性用来区分特定的调度器实例
    • org.quartz.scheduler.instanceId: 这个值必须在所有调度器实例中是唯一的,尤其是在一个集群环境中,作为集群的唯一key
  2. 线程池属性:
    • threadCount: 至少为1,但最多的话最好不要超过100
    • threadPriority: 最小为1,最大为10,默认为5

6 监听器

6.1 概念

区分全局监听器与非全局监听器

  • 全局监听器: 全局监听器能够接收到所有的Job/Trigger的事件通知
  • 非全局监听器: 而非全局监听器只能接收到在其上注册的Job或Trigger的事件,不在其上注册的Job或Trigger则不会进行监听
6.2 JobListener

使用场景: 任务调度过程中,与任务Job相关的事件包括:job开始要执行的提示; job执行完成的提示

public interface JobListener {  	
    String getName();  	
    void jobToBeExecuted(JobExecutionContext context);  	
    void jobExecutionVetoed(JobExecutionContext context);
    void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException);
}

示例:

HelloJobListener.java

// 定义任务类
public class HelloJobListener implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 定义时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateString = dateFormat.format(date);

        // 定义工作任务内容
        System.out.println("进行数据库备份操作。当前任务执行的时间:"+dateString);
    }
}

创建自定义的JobListener

MyJobListener.java

public class MyJobListener implements JobListener{
    @Override
    public String getName() {
        String name = getClass().getSimpleName();
        System.out.println("监听器的名称是:"+name);
        return name;
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:"+jobName+"     Scheduler在JobDetail将要被执行时调用这个方法");
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:"+jobName+"     Scheduler在JobDetail即将被执行,但又被TriggerListerner否决时会调用该方法");
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:"+jobName+"     Scheduler在JobDetail被执行之后调用这个方法");
    }
}

执行调度器

HelloSchedulerDemoJobListener.java

public class HelloSchedulerDemoJobListener {
    public static void main(String[] args) throws Exception {

        // 1:从工厂中获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail job = JobBuilder.newJob(HelloJobListener.class)
                .withIdentity("job1", "group1") // 定义该实例唯一标识
                .build();

        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 定义该实例唯一标识
                .startNow()  // 马上执行
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .repeatSecondlyForever(5)) // 每5秒执行一次
                .build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

        // 创建并注册一个全局的 Listener
        // scheduler.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs());

        // 创建并注册一个局部的 Listener, 表示指定的job
        scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(JobKey.jobKey("job1","group1")));



        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
    }
}
6.3 TriggerListener

使用场景: 任务调度过程中,与触发器Trigger相关的事件包括:触发器触发、触发器未正常触发、触发器完成等。

public interface TriggerListener {    
    public String getName();    
    public void triggerFired(Trigger trigger, JobExecutionContext context);
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context);    
    public void triggerMisfired(Trigger trigger);    
    public void triggerComplete(Trigger trigger, JobExecutionContext context,            int triggerInstructionCode);
}

HelloJobListener.java
同上

MyTriggerListener.java

public class MyTriggerListener implements TriggerListener{	
    private String name;	
    public MyTriggerListener(String name) {   		 
        this.name = name;	
    }    
    @Override    
    public String getName() {        
        return name;    
    }    
    
    @Override    
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        String triggerName = trigger.getKey().getName(); 
        System.out.println(triggerName + " 被触发");    
    }    
        
    @Override    
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {        
    String triggerName = trigger.getKey().getName();        
    System.out.println(triggerName + " 没有被触发");        
    return true; // true:表示不会执行Job的方法    
    }    
    
    @Override    
    public void triggerMisfired(Trigger trigger) {        
        String triggerName = trigger.getKey().getName();
        System.out.println(triggerName + " 错过触发");    
    }    
        
    @Override    
    public void triggerComplete(Trigger trigger, JobExecutionContext context,            CompletedExecutionInstruction triggerInstructionCode) {        
        String triggerName = trigger.getKey().getName(); 
        System.out.println(triggerName + " 完成之后触发");    
    }
 }

任务调度类HelloSchedulerDemoTriggerListener.java

public class HelloSchedulerDemoTriggerListener {    

    public static void main(String[] args) throws Exception {        
    
        // 1:从工厂中获取任务调度的实例        
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();        
        // 2:定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口        
        JobDetail job = JobBuilder.newJob(HelloJobListener.class)
        .withIdentity("job1", "group1") // 定义该实例唯一标识                
        .build();
        
        // 3:定义触发器 ,马上执行, 然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 定义该实例唯一标识
                .startNow()  // 马上执行
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                    .repeatSecondlyForever(5)) // 每5秒执行一次 
                .build();

        // 4:使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

        // 创建并注册一个全局的Trigger Listener
        scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), EverythingMatcher.allTriggers());

        // 创建并注册一个局部的Trigger Listener
        // scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), KeyMatcher.keyEquals(TriggerKey.triggerKey("trigger1", "group1")));

        // 5:开启
        scheduler.start();
        // 关闭
        // scheduler.shutdown();
    }
 }


6.4 SchedulerListener

略 ...