任务调度 Quartzh 框架使用指南

概述

Quartz 是开源任务调度框架中的翘首,是 OpenSymphony 开源组织在 Job scheduling 领域又一个开源项目,完全由 Java 开发,可以用来执行定时任务,类似于 java.util.Timer。但是相较于 Timer, Quartz 增加了很多功能:

  • Quartz 提供了强大任务调度机制,同时保持了使用的简单性。Quartz 允许开发人员灵活地定义触发器的调度时间表,并可以对触发器和任务进行关联映射。
  • Quartz 提供了调度运行环境的持久化机制,可以保存并恢复调度现场,即使系统因故障关闭,任务调度现场数据并不会丢失。
  • Quartz 还提供了组件式的侦听器、各种插件、线程池等功能。

大部分公司都会用到定时任务这个功能。拿火车票购票来说:

  1. 当下单购票后,后台就会插入一条待支付的 task(job),一般是30分钟,超过30min后就会执行这个 job,去判断是否支付,未支付就会取消此次订单;
  2. 当支付完成之后,后台拿到支付回调后就会再插入一条待消费的 task(job),Job 触发日期为火车票上的出发日期,超过这个时间就会执行这个job,判断是否使用等。

Quartz 的核心类有以下三部分:

  • 任务 Job : 需要实现 org.quartz.Job 接口的任务类,实现 execute() 方法,执行后完成任务。

  • 触发器 Trigger :

    实现触发任务去执行的触发器,触发器 Trigger 最基本的功能是指定 Job 的执行时间,执行间隔,运行次数等。

    包括 SimpleTrigger(简单触发器)和 CronTrigger。

  • 调度器 Scheduler : 任务调度器,负责基于 Trigger 触发器,来执行 Job任务。

主要关系如下:

在这里插入图片描述


入门案例

依赖

SpringBoot集成依赖

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

原生依赖

    <!-- 核心包 -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.3.0</version>
    </dependency>
    <!-- 工具包 -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz-jobs</artifactId>
        <version>2.3.0</version>
    </dependency>

代码

自定义任务类

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Date;

@Slf4j
public class QuartzJob implements Job {
    
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
    
    
        //先得到任务,之后就得到map中的名字
        Object name = context.getJobDetail().getJobDataMap().get("name");
        log.info(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss") + " "+ name + "搞卫生");
    }
}

任务调度方法

import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;

@Slf4j
@Component
public class QuartzSheduleTask {
    
    

//    @PostConstruct	// 实例化类后执行该方法
    public void startDheduleTask() throws Exception {
    
    
        // 创建任务
        JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
            .withIdentity("任务-A", "任务分组-A")
            .withDescription("开年大扫除")
            .usingJobData("name", "王阿姨")
            .build();

        // 创建触发器
        Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity("触发器-A", "触发器分组-A")
//            .startAt(new Date())
            .startNow()
            .withSchedule(
                simpleSchedule()
                    .withIntervalInSeconds(5)   // 任务执行间隔
//                    .withRepeatCount(10)      // 任务重复执行次数,-1 为一直执行,缺省值为0
                    .repeatForever()            // 任务一直执行
            )
//            .withSchedule(
//                CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
//            )
            .withDescription("大扫除触发器")
            .build();

        // 实例化调度器工厂
        SchedulerFactory factory = new StdSchedulerFactory();
        // 得到调度器
        Scheduler scheduler = factory.getScheduler();
        // 将触发器和任务绑定到调度器中去
        scheduler.scheduleJob(jobDetail, trigger);
        // 启动调度器
        scheduler.start();
    }

Quartz API

官方API:http://www.quartz-scheduler.org/api/2.3.0/index.html

主要 API 接口概述

Quartz API 的主要接口有:

  • Job :任务逻辑类需要实现的接口,定义被调度的任务内容

  • JobDetail(任务详情):又称任务实例,用于绑定 Job,为 Job 提供了许多设置属性

    调度器需要借助 Jobdetail 对象来添加 Job 实例。

  • JobBuilder :用于构建 JobDetail 实例

  • Trigger(触发器):定义任务的调度计划

  • TriggerBuilder :用于构建触发器实例

  • Scheduler(任务调度控制器):与调度程序交互的主要API

  • SchedulerFactory(调度器工厂):创建调度器实例

  • DateBuilder :可以很方便地构造表示不同时间点的 java.util.Date 实例

  • Calendar:一些日历特定时间点的集合。一个 trigger 可以包含多个 Calendar,以便排除或包含某些时间点


Job(自定义任务逻辑类)

实现 Job 接口

任务类是一个实现 org.quartz.Job 接口的类,且任务类必须含有空构造器

当关联这个任务实例的的一个 trigger(触发器)被触发后,execute() 方法会被 scheduler(调度器)的一个工作线程调用。

代码示例:

public class QuartzJob implements Job {
    
    
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
    
    
        // 任务的具体逻辑
    }
}

JobExecutionContext

传递给 execute() 方法的 JobExecutionContext 对象

  • 保存着该 job 运行时的一些信息 ,包括执行 job 的 scheduler 对象的引用,触发 job 的 trigger 对象的引用,JobDetail 对象的引用等。

  • API 方法:

    Scheduler getScheduler();
    Trigger getTrigger();
    Calendar getCalendar();
    boolean isRecovering();
    TriggerKey getRecoveringTriggerKey();
    int getRefireCount();
    JobDataMap getMergedJobDataMap();
    JobDetail getJobDetail();
    Job getJobInstance();
    Date getFireTime();
    Date getScheduledFireTime();
    Date getPreviousFireTime();
    Date getNextFireTime();
    String getFireInstanceId();
    Object getResult();
    void setResult(Object var1);
    long getJobRunTime();
    

获取 JobDataMap 的键值

  • 方法1:在 Job 实现类中通过 context 先获取 JobDetail 和 Trigger 对象,再从这两个对象中获取 JobDataMap 对象

  • 方法2:在 Job 实现类中通过 context 直接获取 MergedJobDataMap(存储着 jobdetail 和 trigger 合并后的键值)

    需保证 jobDetail 和 Trigger 的 JobDataMap 中无重复 key。

    因为当有相同的 key 时,是先获取 jobdetail 中的,再获取 trigger 中的,同名 key 的值会被覆盖

  • 方法3:Job 实现类中定义 JobDataMap 中 key 同名的属性,并添加 Setter 方法

    类自动注入,底层将使用反射直接将 scheduler 中的 jobdetail 和 trigger 中的 jobDataMap 中的键值,通过 Setter 方法注入对应字段(名称要相同)

    需保证 jobDetail 和 Trigger 的 JobDataMap 中无重复 key。原因同上


JobDetail(任务详情)

包含 job 的各种属性设置,以及用于存储 job 对象数据的 JobDataMap。

其实现类的属性:

private String name;			// 任务名称,同分组必须唯一
private String group="DEFAULT";	// 任务分组
private String description;		// 任务描述
private Class<? extends Job> jobClass;	// 任务类
private JobDataMap jobDataMap;			// 任务数据
private boolean durability=false;		// 在没有 Trigger 关联的条件下是否保留
private boolean shouldRecover=false;	// 当任务执行期间,程序被异常终止,程序再次启动时是否再次执行该任务
private transient JobKey key;	// 任务唯一标识,包含任务名称和任务分组信息

接口 API 方法:

JobKey getKey();
String getDescription();
Class<? extends Job> getJobClass();
JobDataMap getJobDataMap();
boolean isDurable();		// 获取 durability 的值
boolean isPersistJobDataAfterExecution();	// 任务调用后是否保存任务数据(JobData)
boolean isConcurrentExectionDisallowed();	// 获取任务类是否标注了 @DisallowConcurrentExecution 注解
boolean requestsRecovery();		// 获取 shouldRecover 的值
Object clone();		// 克隆一个 JobDetail 对象
JobBuilder getJobBuilder();

JobBuilder

JobBuilder 用于构建 JobDetail 实例

API 方法:

// 创建一个 JobBuilder 对象
public static JobBuilder newJob()
public static JobBuilder newJob(Class<? extends Job> jobClass)

// 设置任务的名称、分组、唯一标识。不指定则会自动生成任务的名称、唯一标识,分组默认DEFUALT
public JobBuilder withIdentity(String name)
public JobBuilder withIdentity(String name, String group)
public JobBuilder withIdentity(JobKey jobKey)

public JobBuilder withDescription(String jobDescription)	// 设置任务描述
public JobBuilder ofType(Class<? extends Job> jobClazz)		// 设置任务类
public JobBuilder requestRecovery()		// 设置 shouldRecover=true
public JobBuilder requestRecovery(boolean jobShouldRecover)
public JobBuilder storeDurably()		// 设置 durability=true
public JobBuilder storeDurably(boolean jobDurability)
public JobBuilder usingJobData(String dataKey, String value)	// jobDataMap.put(dataKey, value)
public JobBuilder usingJobData(JobDataMap newJobDataMap)		// jobDataMap.putAll(newJobDataMap)
public JobBuilder setJobData(JobDataMap newJobDataMap)			// jobDataMap=newJobDataMap

public JobDetail build()

Trigger(触发器)

触发器有很多属性,这些属性都是在使用 TriggerBuilder 构建触发器时设置的。

Trigger 公有属性:

/* Trigger 接口中定义 */
int MISFIRE_INSTRUCTION_SMART_POLICY = 0;				// 默认Misfire策略-智能
int MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1;		// Misfire策略-忽略错过的触发
int DEFAULT_PRIORITY = 5;				// 默认触发器优先级

/* AbstractTrigger 抽象类中定义 */
private String name;					// 触发器名称,同分组必须唯一
private String group = "DEFAULT";		// 触发器分组
private String jobName;					// 任务名称
private String jobGroup = "DEFAULT";	// 任务分组
private String description;				// 触发器描述
private JobDataMap jobDataMap;			// 任务数据,用于给 Job 传递一些触发相关的参数
private boolean volatility = false;		// 表示该触发器是否被持久化到数据库存储
private String calendarName;
private String fireInstanceId;
private int misfireInstruction = 0;
private int priority = 5;				// 触发器优先级
private transient TriggerKey key;		// 触发器唯一标识,在一个 Scheduler 中必须是唯一的。
										// 多个触发器可以指向同一个工作,但一个触发器只能指向一个工作。

/* 各触发器实现类中定义 */
private Date startTime;			// 触发器的首次触时间
private Date endTime;			// 触发器的末次触时间,设置了结束时间则在这之后,不再触发
private Date nextFireTime;		// 触发器的下一次触发时间
private Date previousFireTime;	// 触发器的本次触发时间

Trigger 接口 API 方法:

TriggerKey getKey();
JobKey getJobKey();
String getDescription();
String getCalendarName();
JobDataMap getJobDataMap();
int getPriority();
boolean mayFireAgain();

Date getStartTime();
Date getEndTime();
Date getNextFireTime();
Date getPreviousFireTime();
Date getFireTimeAfter(Date var1);
Date getFinalFireTime();

int getMisfireInstruction();
int compareTo(Trigger var1);

TriggerBuilder<? extends Trigger> getTriggerBuilder();
ScheduleBuilder<? extends Trigger> getScheduleBuilder();

TriggerBuilder

TriggerBuilder 用于构建 Trigger 实例

TriggerBuilder(以及 Quartz 的其它 builder)会为那些没有被显式设置的属性选择合理的默认值。

例如:

  • 如果没有调用 withIdentity(…) 方法,TriggerBuilder 会为 trigger 生成一个随机的名称

  • 如果没有调用 startAt(…) 方法,则默认使用当前时间,即trigger立即生效

public static TriggerBuilder<Trigger> newTrigger()

// 设置触发器的名称、分组、唯一标识,不指定则会自动生成触发器的名称、唯一标识,分组默认DEFUALT
public TriggerBuilder<T> withIdentity(String name)
public TriggerBuilder<T> withIdentity(String name, String group)
public TriggerBuilder<T> withIdentity(TriggerKey triggerKey)

public TriggerBuilder<T> withDescription(String triggerDescription)
public TriggerBuilder<T> withPriority(int triggerPriority)
public TriggerBuilder<T> modifiedByCalendar(String calName)

// 开始时间默认 new Date(),结束时间默认 null
public TriggerBuilder<T> startAt(Date triggerStartTime)
public TriggerBuilder<T> startNow()
public TriggerBuilder<T> endAt(Date triggerEndTime)

// 设置触发器的类别,默认是只执行一次的 SimpleTrigger
public <SBT extends T> TriggerBuilder<SBT> withSchedule(ScheduleBuilder<SBT> schedBuilder)

public TriggerBuilder<T> forJob(JobKey keyOfJobToFire)
public TriggerBuilder<T> forJob(String jobName)
public TriggerBuilder<T> forJob(String jobName, String jobGroup)
public TriggerBuilder<T> forJob(JobDetail jobDetail)
public TriggerBuilder<T> usingJobData(String dataKey, String value)
public TriggerBuilder<T> usingJobData(JobDataMap newJobDataMap)

public T build()

Scheduler(任务调度控制器)

接口常用 API 方法:

void start()		// 调度器开始工作
void shutdown()		// 关闭调度器
void shutdown(boolean waitForJobsToComplete) 	// 等待所有正在执行的 job 执行完毕后才关闭调度器
void pauseAll()		// 暂停调度器
void resumeAll()	// 恢复调度器

// 绑定/删除触发器和任务
Date scheduleJob(Trigger trigger)
Date scheduleJob(JobDetail jobDetail, Trigger trigger)
void scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace)
void scheduleJobs(Map<JobDetail, Set<? extends Trigger>> triggersAndJobs, boolean replace)
void addJob(JobDetail jobDetail, boolean replace)
boolean deleteJobs(List<JobKey> jobKeys)
boolean unscheduleJob(TriggerKey triggerKey)

SchedulerFactory(调度器工厂)

常用 API 方法:

// 构造方法
public StdSchedulerFactory()
public StdSchedulerFactory(Properties props)
public StdSchedulerFactory(String fileName)

// 获取一个默认的标准调度器
public static Scheduler getDefaultScheduler()
// 获取一个调度器。若调度器工厂未初始化,则获取一个默认的标准调度器
public Scheduler getScheduler()

// 初始化调度器工厂
public void initialize()	// 使用quartz的jar包中默认的quartz.properties文件初始化(默认的标准调度器)
public void initialize(Properties props)
public void initialize(String filename)
public void initialize(InputStream propertiesStream)

核心模块详解

参考:

Job类

Job 类型 有两种:

  • 无状态的(stateless):默认,每次调用时都会创建一个新的 JobDataMap

  • 有状态的(stateful):Job 实现类上标注 @PersistJobDataAfterExecution 注解

    多次 job 调用期间可以持有一些状态信息,这些状态信息存储在 JobDataMap 中


Job 实例的生命周期

  1. 每次当 scheduler 执行 job 时,在调用其 execute(…) 方法之前会创建该类的一个新的实例;

  2. 任务执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收;

    基于这种执行策略,job 必须有一个无参的构造函数(当使用默认的 JobFactory 时创建 job 实例的调用)另外,在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留。

    若想 job 实例增加属性或配置,在 job 的多次执行中跟踪 job 的状态,则可以使用 JobDataMap( JobDetail 对象的一部分)


@DisallowConcurrentExecution 注解:禁止并发执行

  • Quartz 定时任务默认都是并发执行的,不会等待上一次任务执行完毕,只要间隔时间到就会执行,如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。

  • 通过在 Job 实现类上标注 @DisallowConcurrentExecution 注解来实现阻止并发。

    @DisallowConcurrentExecution 注解禁止并发执行多个相同定义的 JobDetail, 这个注解是加在 Job 类上的, 但意思并不是不能同时执行多个 Job, 而是不能并发执行同一个 Job Definition(由JobDetail定义), 但是可以同时执行多个不同的 JobDetail。

    举例说明:有一个Job类,叫做 SayHelloJob, 并在这个 Job 上加了这个注解, 然后在这个 Job 上定义了很多个 JobDetail, 如sayHelloToJoeJobDetail, sayHelloToMikeJobDetail, 那么当 scheduler 启动时, 不会并发执行多个 sayHelloToJoeJobDetail 或者sayHelloToMikeJobDetail, 但可以同时执行 sayHelloToJoeJobDetail 跟 sayHelloToMikeJobDetail。

    测试代码:设定的时间间隔为3秒,但job执行时间是5秒,设置 @DisallowConcurrentExecution以 后程序会等任务执行完毕以后再去执行,否则会在3秒时再启用新的线程执行。


@PersistJobDataAfterExecution 注解:保存 JobDataMap

  • 该注解加在 Job 实现类上。表示当正常执行完 Job 后, JobDataMap 中的数据应该被改动, 以被下一次调用时用。

  • **注意:**当使用 @PersistJobDataAfterExecution 注解时, 为了避免并发时, 存储数据造成混乱, 强烈建议把 @DisallowConcurrentExecution 注解也加上。


JobDetail(任务详情)

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

当一个 trigger 被触发时,与之关联的 JobDetail 实例会被加载,JobDetail 引用的 job 类通过配置在 Scheduler 上的 JobFactory 进行初始化。

  • 默认的 JobFactory 实现,仅仅是调用 job 类的 newInstance() 方法,然后尝试调用 JobDataMap 中的 key 的 setter 方法
  • 也可以自定义 JobFactory 实现,比如让 IOC 或 DI 容器可以创建/初始化 job 实例

Job 和 JobDetail 的描述名称

  • 在 Quartz 的描述语言中,通常将保存后的 JobDetail 称为” job 定义”或者“ JobDetail 实例”,将一个正在执行的 job 称为“ job 实例”或者“ job 定义的实例”。

  • 当使用 “job” 时,一般指代的是 job 定义或者 JobDetail

  • 当提到实现 Job 接口的类时,通常使用“job类”


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

  • durability 属性:表示在没有 Trigger 关联的条件下是否保留,boolean 类型

    如果一个 JobDetail 是非持久的,当没有活跃的 trigger 与之关联的时候,会被自动地从 scheduler 中删除。

    也就是说,非持久的 JobDetail 的生命周期是由 trigger 的存在与否决定的

  • requestsRecovery 属性:标识当任务执行期间,程序被异常终止,程序再次启动时是否再次执行该任务,boolean 类型

    如果一个 job 是可恢复的,并且在其执行的时候,scheduler发生硬关闭(比如运行的进程崩溃了,或者关机了),则当 scheduler 重新启动的时候,该 job 会被重新执行。


Trigger(触发器)

概述

用于触发 Job 的执行。当准备调度一个 job 时,需要创建一个 Trigger 的实例,并设置调度相关的属性。

注意:一个 Trigger 只能绑定一个 JobDetail,而一个 JobDetail 可以和多个 Trigger 绑定。


触发器的优先级(priority)

如果 trigger 很多(或者 Quartz 线程池的工作线程太少),Quartz 可能没有足够的资源同时触发所有的 trigger,这种情况下,可以通过设置 trigger 的 priority(优先级) 属性来优先使用 Quartz 的工作线程。

  • 优先级只有触发器的触发时间一样的时候才有意义
  • 触发器的优先级值默认为5
  • 当一个任务请求恢复执行时,它的优先级和原始优先级是一样的

错过触发(misfire Instructions)

  • 如果 scheduler 关闭了,或者 Quartz 线程池中没有可用的线程来执行 job,此时持久性的 trigger 就会错过(miss)其触发时间,即错过触发(misfire)。
  • 不同类型的 trigger,有不同的 misfire 机制。默认都使用 “智能机制(smart policy)”,即根据 trigger 的类型和配置动态调整行为。
  • 当 scheduler 启动的时候,查询所有错过触发(misfire)的持久性 trigger;然后根据它们各自的 misfire 机制更新 trigger 的信息。

触发器的分类

Quartz 自带了各种不同类型的 Trigger,最常用的主要是 SimpleTrigger 和 CronTrigger。

触发器实例通过 TriggerBuilder 设置主要的属性,通过 TriggerBuilder.withSchedule() 方法设置各种不同类型触发器的特有属性。

  • SimpleTrigger(简单触发器)

    一种最基本的触发器。在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。

    SimpleTrigger 的属性包括开始时间、结束时间、重复次数以及重复的间隔

    • repeatInterval 属性:重复间隔,默认为0
    • repeatCount 属性:重复次数,默认为0,实际执行次数是 repeatCount + 1

    注意:

    • 如果重复间隔为0,trigger 将会以重复次数并发执行(或者以 scheduler 可以处理的近似并发数)

    • endTime 属性的值会覆盖设置重复次数的属性值

      即 可以创建一个 trigger,在终止时间之前每隔10秒执行一次,并不需要去计算在开始时间和终止时间之间的重复次数,只需要设置终止时间并将重复次数设置为 REPEAT_INDEFINITELY(无期限重复)

    Trigger trigger = TriggerBuilder.newTrigger()       
        .withSchedule(            
        	SimpleScheduleBuilder.simpleSchedule()
        	//.withIntervalInHours(1) 	// 每小时执行一次
        	//.withIntervalInMinutes(1) // 每分钟执行一次
        	.repeatForever() 			// 次数不限
        	.withRepeatCount(10) 		// 次数为10次
        )       
        .build();
    

    SimpleTrigger 的 Misfire 策略常量:

    // 触发器默认Misfire策略常量,SimpleTrigger会根据实例的配置及状态,在所有MISFIRE策略中动态选择一种Misfire策略
    int MISFIRE_INSTRUCTION_SMART_POLICY = 0;
    
    int MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1;
    int MISFIRE_INSTRUCTION_FIRE_NOW = 1;
    int MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT = 2;
    int MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT = 3;
    int MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT = 4;
    int MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT = 5;
    
  • CronTrigger(基于 cron 表达式的触发器)

    Trigger trigger = TriggerBuilder.newTrigger()  
        .withSchedule(CronScheduleBuilder.cronSchedule("2 * * * * *"))
        .build();
    

    CronTrigger 的 Misfire 策略常量:

    // 触发器默认Misfire策略常量,由CronTrigger解释为MISFIRE_INSTRUCTION_FIRE_NOW
    int MISFIRE_INSTRUCTION_SMART_POLICY = 0;
    
    int MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY = -1;
    int MISFIRE_INSTRUCTION_FIRE_ONCE_NOW = 1;
    int MISFIRE_INSTRUCTION_DO_NOTHING = 2;
    
  • CalendarIntervalTrigger(日历时间间隔触发器)

    与 SimpleTrigger 类似, 但不同的是,SimpleTrigger 指定的时间间隔底层为毫秒,CalendarIntervalTrigger 支持的间隔单位有秒,分钟,小时,天,月,年,星期。

    CalendarIntervalTrigger的属性有:

    • interval :执行间隔
    • intervalUnit :执行间隔的单位(秒,分钟,小时,天,月,年,星期)
    Trigger trigger = TriggerBuilder.newTrigger()  
        .withSchedule(
        	calendarIntervalSchedule()
            .withIntervalInDays(1) //每天执行一次
            //.withIntervalInWeeks(1) //每周执行一次
    	)
        .build();
    
  • DailyTimeIntervalTrigger(日常时间间隔触发器)

    支持时间间隔类型以及指定星期

    常用属性:

    • startTimeOfDay :每天开始时间
    • endTimeOfDay :每天结束时间
    • daysOfWeek :需要执行的星期
    • interval :执行间隔
    • intervalUnit :执行间隔的单位(秒,分钟,小时,天,月,年,星期)
    • repeatCount: 重复次数
    Trigger trigger = TriggerBuilder.newTrigger()  
        .withSchedule(
    		dailyTimeIntervalSchedule()
                .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) 	// 第天9:00开始
                .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(15, 0)) 	// 15:00 结束 
                .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) // 周一至周五执行
                .withIntervalInHours(1) 	// 每间隔1小时执行一次
                .withRepeatCount(100) 		// 最多重复100次(实际执行100+1次)
    	)
        .build();
    

调度计划中排除时间段

org.quartz.Calendar 对象用于从 trigger 的调度计划中排除时间段。

比如,可以创建一个 trigger,每个工作日的上午 9:30 执行,然后增加一个 Calendar,排除掉所有的商业节日。

任何实现了 org.quartz.Calendar 接口的可序列化对象都可以作为 Quartz Calendar 对象。

Quartz 提供的 Calendar 接口实现类:

  • BaseCalender :为高级的 Calender 实现了基本的功能,实现了 org.quartz.Calender 接口
  • WeeklyCalendar :排除星期中的一天或多天,例如,可用于排除周末
  • MonthlyCalendar :排除月份中的数天,例如,可用于排除每月的最后一天
  • AnnualCalendar :排除一年中一天或多天
  • HolidayCalendar :特别的用于从 Trigger 中排除节假日

使用方式

  • 实例化 Calendar,并加入要排除的日期
  • 通过 addCalendar() 方法注册 Calendar 实例到 scheduler
  • 在定义 trigger 时,进行 Calendar 实例与 trigger 实例的关联

示例:

// 2014-8-15这一天不执行任何任务
AnnualCalendar cal = new AnnualCalendar();
cal.setDayExcluded(new GregorianCalendar(2014, 7, 15), true);
// 注册日历到调度器
scheduler.addCalendar("excludeCalendar", cal, false, false);

TriggerBuilder.newTrigger().modifiedByCalendar("excludeCalendar")....

Scheduler(调度器)

参考:Quartz中Scheduler的理解和使用

概述

调度器(Scheduler)是 Quartz 框架的心脏,用来管理触发器和 Job,并保证 Job 能被触发执行。程序员与框架内部之间的调用都是通过 org.quartz.Scheduler 接口来完成的。

对于 Scheduler 接口的实现,其实只是核心调度(org.quartz.core.QuartzScheduler)的一个代理,对代理的方法进行调用时会传递到底层核心调度实例上。QuartzScheduler 处于 Quartz 框架的根位置,驱动着整个 Quartz 框架。


Schedule 种类

有三种:

  • StdScheduler(标准默认调度器):最常用

    默认值加载是当前工作目录下的”quartz.properties”属性文件。如果加载失败,会去加载 org/quartz 包的”quartz.properties”属性文件。

  • RemoteScheduler (远程调度器)

  • RemoteMBeanScheduler


Scheduler 有两个重要组件:

  • ThreadPool :调度器线程池,所有的任务都会被线程池执行

  • JobStore :储运行时信息的,包括Trigger,Scheduler,JobDetail,业务锁等。

    JobStore 的实现有 RAMJob(内存实现),JobStoreTX( JDBC,事务由 Quartz 管理),JobStoreCMT( JDBC,使用容器事务),ClusteredJobStore(集群实现)等

    注意:Job 和 Trigger 需要存储下来才可以被使用


Schedule 工厂

用于创建 Schedule 实例

种类有两种:

  • StdSchedulerFactory(最常用)
  • DirectSchedulerFactory

Scheduler 的生命周期

  • 从 SchedulerFactory 创建它时开始,到 Scheduler 调用 shutdown() 方法时结束

  • Scheduler 被创建后,可以增加、删除和列举 Job 和 Trigger,以及执行其它与调度相关的操作(如暂停Trigger)。

    但是,Scheduler 只有在调用 start() 方法后,才会真正地触发 trigger(即执行 job)。


Quartz 线程分类

  • Scheduler 调度线程

    主要有:

    • 执行常规调度的线程(Regular Scheduler Thread):轮询查询存储的所有触发器,到达触发时间,就从线程池获取一个空闲的线程,执行与触发器关联的任务

    • 执行错失调度的线程(Misfire Scheduler Thread):Misfire 线程扫描所有的触发器,检查是否有 misfired 的线程,也就是没有被执行错过的线程,有的话根据 misfire 的策略分别处理

  • 任务执行线程


创建调度器

StdScheduler 只提供了一个带参构造方法,此构造需要传递 QuartzScheduler 和 SchedulingContext 两个实例参数。

一般不使用构造方法去创建调度器,而是通过调度器工厂来创建。

调度器工厂接口 org.quartz.SchedulerFactory 提供了两种不同类型的工厂实现,分别是 org.quartz.impl.DirectSchedulerFactoryh 和 org.quartz.impl.StdSchedulerFactory。

  • 使用 StdSchedulerFactory 工厂创建

    此工厂是依赖一系列的属性来决定如何创建调度器实例的。

    获取默认的标准调度器:

    • 此工厂提供了无参数的 initialize() 方法进行初始化,此方法本质是加载 quartz 的 jar 包中默认的quartz.properties 属性文件,具体的加载步骤:
      1. 检查系统属性中是否设置了文件名,通过System.getProperty(“org.quartz.properties”)
      2. 如果没有设置,使用默认的quartz.properties作为要加载的文件名
      3. 然后先从当前工作目录中加载这个文件,如果没有找到,再从系统 classpath 下加载这个文件
    • StdSchedulerFactory 工厂还可以不主动调用 initialize() 方法进行初始化,而是直接使用 StdSchedulerFactory 的静态方法 getDefaultScheduler() 获取一个默认的标准调度器。

    获取自定义调度器:

    自定义属性的提供方式有三种:

    • 通过 java.util.Properties 属性实例
    • 通过外部属性文件提供
    • 通过有属性文件内容的 java.io.InputStream 文件流提供
        public static void main(String[] args) {
          
          
            try {
          
          
                StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
                
                // 第一种方式 通过Properties属性实例创建
                Properties props = new Properties();
                props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, 
                          "org.quartz.simpl.SimpleThreadPool");
                props.put("org.quartz.threadPool.threadCount", 5);
                schedulerFactory.initialize(props);
                
                // 第二种方式 通过传入文件名
                // schedulerFactory.initialize("my.properties");
                
                // 第三种方式 通过传入包含属性内容的文件输入流
                // InputStream is = new FileInputStream(new File("my.properties"));
                // schedulerFactory.initialize(is);
    
                // 获取调度器实例
                Scheduler scheduler = schedulerFactory.getScheduler();
                
            } catch (Exception e) {
          
          
                e.printStackTrace();
            }
        }
    
    • 第一种方式向工厂传入了两个属性,分别是线程池的类名和线程池大小,这两个属性是必须的,因为若使用自定义Properties 属性初始化,工厂是没有给它们指定默认值的。

    • 第二种方式是通过定义一个外部属性文件

      底层实现是:首先通过Thread.currentThread().getContextClassLoader().getResourceAsStream(filename) 获取文件流,然后使用 Properties 实例的 load 方法加载文件流形成属性实例,最后在通过 initialize(props) 初始化完成。

    • 第三种方式就是直接使用 Properties 实例的 load 方法加载文件流形成属性实例,再在通过 initialize(props) 初始化完成。

  • 使用 DirectSchedulerFactory 工厂创建

    此工厂方式创建适用于想绝对控制Scheduler实例的场景。

      public static void main(String[] args) {
          
          
          try {
          
          
            DirectSchedulerFactory schedulerFactory = DirectSchedulerFactory.getInstance();
            // 表示以3个工作线程初始化工厂
            schedulerFactory.createVolatileScheduler(3);
            Scheduler scheduler = schedulerFactory.getScheduler();  
          } catch (SchedulerException e) {
          
          
            e.printStackTrace();
          }
      }
    

    创建步骤:

    1. 通过DirectSchedulerFactory的getInstance方法得到拿到实例
    2. 调用createXXX方法初始化工厂
    3. 调用工厂实例的getScheduler方法拿到调度器实例

    DirectSchedulerFactory 是通过 createXXX 方法传递配置参数来初始化工厂,这种初始化方式是一种硬编码,在工作中用到的情况会很少。


管理调度器

获取调度器实例后,在调度的生命周期中可以做以下工作,例如:启动调度器,设置调度器为 standby 模式,继续或停止调度器。

  • 启动 Scheduler

    当调度器初始化完成,并且 Job 和 Trigger 也注册完成,此时就可以调用 scheduler.start() 启动调度器了。

    start()方法一旦被调用,调度器就开始搜索需要执行的 Job。

  • 设置 standby(待机)模式

    设置 Scheduler 为 standby 模式会让调度器暂停寻找 Job 去执行。

    应用场景举例:当需要重启数据库时可以先将调度器设置为 standby 模式,待数据库启动完成后再通过 start() 启动调度器。

  • 关闭调度器

    调用 shutdown() 或 shutdown(boolean waitForJobsToComplete) 方法停止调度器。

    shutdown 方法调用后,就不能再调用 start 方法了,因为 shutdown 方法会销毁 Scheduler 创建的所有资源(线程、数据库连接等)。

一般情况下,调度器启动后不需要做其他任何事情。


Job Stores(任务存储)

Quartz 提供两种基本作业存储类型:

  • RAMJobStore(内存任务存储):默认情况下 Quartz 会将任务调度存在内存中

    这种方式性能是最好的,因为内存的速度是最快的

    缺点是数据缺乏持久性,当程序崩溃或者重新发布的时候,所有运行信息都会丢失

  • JDBCJobStore(数据库任务存储)

    存到数据库之后,可以做单点也可以做集群

    当任务多了之后,可以统一进行管理(停止、暂停、修改任务)

    关闭或者重启服务器,运行的信息都不会丢失

    缺点是运行速度快慢取决于连接数据库的快慢


Quartz 自带有数据库模式

数据库脚本地址:

  • https://gitee.com/qianwei4712/code-of-shiva/blob/master/quartz/quartz.sql(带注释,gitee)
  • https://lqcoder.com/quartz.sql(不带注释,下载链接)
Table Name Description
QRTZ_CALENDARS 存储Quartz的Calendar信息
QRTZ_CRON_TRIGGERS 存储CronTrigger,包括Cron表达式和时区信息
QRTZ_FIRED_TRIGGERS 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的Trigger组的信息
QRTZ_SCHEDULER_STATE 存储少量的有关Scheduler的状态信息,和别的Scheduler实例
QRTZ_LOCKS 存储程序的悲观锁的信息
QRTZ_JOB_DETAILS 存储每一个已配置的Job的详细信息
QRTZ_JOB_LISTENERS 存储有关已配置的JobListener的信息
QRTZ_SIMPLE_TRIGGERS 存储简单的Trigger,包括重复次数、间隔、以及已触的次数
QRTZ_BLOG_TRIGGERS Trigger作为Blob类型存储
QRTZ_TRIGGER_LISTENERS 存储已配置的TriggerListener的信息
QRTZ_TRIGGERS 存储已配置的Trigger的信息

Listeners(事件监听器)

Listeners:用于根据调度程序中发生的事件执行操作。

Trigger、Job、Scheduler 监听接口

  • TriggerListeners:接收到与触发器(trigger)相关的事件

    发相关的事件包括:触发器触发,触发失灵,触发完成(触发器关闭)

    org.quartz.TriggerListener 接口:

    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);
    }
    
  • JobListeners: 接收与 jobs 相关的事件

    job 相关事件包括:job 即将执行的通知,以及 job 完成执行时的通知。

    org.quartz.JobListener 接口:

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

    与计划程序相关的事件包括:添加job/触发器,删除job/触发器,调度程序中的严重错误,关闭调度程序的通知等。

    org.quartz.SchedulerListener 接口:

    public interface SchedulerListener {
          
          
        public void jobScheduled(Trigger trigger);
        public void jobUnscheduled(String triggerName, String triggerGroup);
        public void triggerFinalized(Trigger trigger);
        public void triggersPaused(String triggerName, String triggerGroup);
        public void triggersResumed(String triggerName, String triggerGroup);
        public void jobsPaused(String jobName, String jobGroup);
        public void jobsResumed(String jobName, String jobGroup);
        public void schedulerError(String msg, SchedulerException cause);
        public void schedulerStarted();
        public void schedulerInStandbyMode();
        public void schedulerShutdown();
        public void schedulingDataCleared();
    }
    

自定义 Listeners

步骤:

  1. 自定义一个实现 org.quartz.TriggerListener 或 JobListener、SchedulerListener 接口的类

    为了方便,自定义监听器也可以继承其子接口 JobListenerSupport 或 TriggerListenerSupport、SchedulerListenerSupport 的实现类,并覆盖感兴趣的事件。

  2. 向调度程序注册监听器,并配置 listener 希望接收事件的 job 或 trigger 的 Matcher

    注:listener 不与 jobs 和触发器一起存储在 JobStore 中。这是因为听众通常是与应用程序的集成点,因此,每次运行应用程序时,都需要重新注册该调度程序。

示例:

  • 添加对所有 job 感兴趣的 JobListener:

    scheduler.getListenerManager().addJobListener(myJobListener, allJobs());
    
  • 添加对特定 job 感兴趣的 JobListener:

    scheduler.getListenerManager().addJobListener(
        myJobListener,KeyMatcher.jobKeyEquals(new JobKey("myJobName""myJobGroup")));
    
  • 添加对特定组的所有 job 感兴趣的 JobListener:

    scheduler.getListenerManager().addJobListener(myJobListener, jobGroupEquals("myJobGroup"));
    
  • 添加对两个特定组的所有 job 感兴趣的 JobListener:

    scheduler.getListenerManager().addJobListener(
        myJobListener, or(jobGroupEquals("myJobGroup"), jobGroupEquals("yourGroup")));
    
  • 添加 SchedulerListener:

    scheduler.getListenerManager().addSchedulerListener(mySchedListener);
    
  • 删除 SchedulerListener:

    scheduler.getListenerManager().removeSchedulerListener(mySchedListener);
    

SchedulerFactoryBean(Spring集成)

参考:

Quartz 的 SchedulerFactory 是标准的工厂类,不太适合在 Spring 环境下使用。

为了保证 Scheduler 能够感知 Spring 容器的生命周期,完成自动启动和关闭的操作,必须让 Scheduler 和 Spring 容器的生命周期相关联。以便在 Spring 容器启动后,Scheduler 自动开始工作,而在 Spring 容器关闭前,自动关闭 Scheduler。为此,Spring 提供了 SchedulerFactoryBean,这个 FactoryBean 大致拥有以下的功能:

  • 以更具 Bean 风格的方式为 Scheduler 提供配置信息
  • 让 Scheduler 和 Spring 容器的生命周期建立关联,相生相息
  • 通过属性配置部分或全部代替 Quartz 自身的配置文件

SchedulerFactoryBean 属性介绍:

  • autoStartup 属性:在 SchedulerFactoryBean 在初始化后是否马上启动 Scheduler,默认为 true

    如果设置为 false,需要手工启动 Scheduler

  • startupDelay 属性:在 SchedulerFactoryBean 初始化完成后,延迟多少秒启动 Scheduler,默认为0,表示马上启动。

    如果并非马上拥有需要执行的任务,可通过 startupDelay 属性让 Scheduler 延迟一小段时间后启动,以便让 Spring 能够更快初始化容器中剩余的Bean

  • triggers 属性:类型为 Trigger[] ,可以通过该属性注册多个 Trigger

  • calendars 属性:类型为 Map,通过该属性向Scheduler注册 Calendar

  • jobDetails 属性:类型为 JobDetail[],通过该属性向 Scheduler 注册 JobDetail

  • schedulerContextMap 属性:可以通过该属性向 Scheduler context 中存一些数据

    spring 容器中的 bean 只能通过 SchedulerFactoryBean 的 setSchedulerContextAsMap() 方法放到 SchedulerContext 里面传入 job 中,在 job 中通过 JobExecutionContext.getScheduler().getContext() 获取存入的 bean


SchedulerFactoryBean 的一个重要功能是允许将 Quartz 配置文件中的信息转移到 Spring 配置文件中

带来的好处是,配置信息的集中化管理,同时开发者不必熟悉多种框架的配置文件结构。回忆一下 Spring 集成 JPA、Hibernate 框架,就知道这是 Spring 在集成第三方框架经常采用的招数之一。

SchedulerFactoryBean 通过以下属性代替框架的自身配置文件:

  • dataSource 属性:当需要使用数据库来持久化任务调度数据时,可以在 Quartz 中配置数据源

    也可以直接在 Spring 中通过 setDataSource() 方法指定一个 Spring 管理的数据源

    如果指定了该属性,即使 quartz.properties 中已经定义了数据源,也会被此 dataSource 覆盖

    配置好数据源 dataSource 并启动应用后,会自动在 Quartz 的QRTZ_LOCKS表中插入以下数据:

    INSERT INTO QRTZ_LOCKS values('TRIGGER_ACCESS');
    INSERT INTO QRTZ_LOCKS values('JOB_ACCESS');
    
  • transactionManager 属性:可以通过该属性设置一个 Spring 事务管理器。

    在设置 dataSource 时,Spring 强烈推荐使用一个事务管理器,否则数据表锁定可能不能正常工作;

  • nonTransactionalDataSource 属性:在全局事务的情况下,如果不希望 Scheduler 执行化数据操作参与到全局事务中,则可以通过该属性指定数据源。

    在 Spring 本地事务的情况下,使用 dataSource 属性就足够了

  • quartzProperties 属性:类型为 Properties,允许开发者在 Spring 中定义 Quartz 的属性。

    其值将覆盖 quartz.properties 配置文件中的设置,这些属性必须是 Quartz 能够识别的合法属性,在配置时,可以需要查看 Quartz 的相关文档。


SchedulerFactoryBean 实现了 InitializingBean 接口

因此在初始化 bean 的时候,会执行 afterPropertiesSet() 方法,该方法将会调用 SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory) 方法创建 Scheduler,通常用 StdSchedulerFactory 。

SchedulerFactory 在创建 quartzScheduler 的过程中,将会读取配置参数,初始化各个组件,关键组件如下:

  • ThreadPool :一般是使用 SimpleThreadPool

    SimpleThreadPool 创建了一定数量的 WorkerThread 实例来使得 Job 能够在线程中进行处理。

    • WorkerThread 是定义在 SimpleThreadPool 类中的内部类,它实质上就是一个线程

    在 SimpleThreadPool 中有三个 list:

    • workers :存放池中所有的线程引用
    • availWorkers :存放所有空闲的线程
    • busyWorkers :存放所有工作中的线程

    线程池的配置参数如下所示:

    org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
    org.quartz.threadPool.threadCount=3 
    org.quartz.threadPool.threadPriority=5
    
  • JobStore :分为存储在内存的 RAMJobStore 和存储在数据库的 JobStoreSupport

    JobStoreSupport 包括 JobStoreTX 和 JobStoreCMT 两种实现

    • JobStoreCMT 是依赖于容器来进行事务的管理
    • JobStoreTX 是自己管理事务

    若要使用集群则要使用 JobStoreSupport 的方式

  • QuartzSchedulerThread :用来进行任务调度的线程

    在初始化的时候 paused=true,halted=false。即 虽然线程开始运行了,但是 paused=true,线程会一直等待,直到调用 start() 方法将 paused 设置为false


SchedulerFactoryBean 实现了 SmartLifeCycle 接口

因此初始化完成后,会执行 start() 方法,该方法将主要会执行以下的几个动作:

  1. 创建 ClusterManager 线程并启动线程:该线程用来进行集群故障检测和处理
  2. 创建 MisfireHandler 线程并启动线程:该线程用来进行 misfire 任务的处理
  3. 设置 QuartzSchedulerThread 的 paused=false,调度线程才真正开始调度

SchedulerFactoryBean 整个启动流程如下图:

在这里插入图片描述


企业级实战案例

参考:任务调度框架 Quartz 用法指南

环境准备

依赖、application 配置文件

依赖

<!--  Quartz 任务调度 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

application 配置文件:

# 开发环境配置
server:
  # 服务器的HTTP端口
  port: 80
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
 
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&useSSL=true
    driver-class-name: com.mysql.cj.jdbc.Driver

Quartz数据库脚本

Quartz 自带有数据库模式

  • 脚本现成脚本:https://gitee.com/qianwei4712/code-of-shiva/blob/master/quartz/quartz.sql

  • 本文统一使用 Cron 方式来创建

    注意:cron 方式需要用到的4张数据表:qrtz_triggers,qrtz_cron_triggers,qrtz_fired_triggers,qrtz_job_details

  • 额外新增保存任务的数据库表:

    DROP TABLE IF EXISTS quartz_job;
    CREATE TABLE quartz_job (
      job_id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务ID',
      job_name varchar(64) NOT NULL DEFAULT '' COMMENT '任务名称',
      job_group varchar(64) NOT NULL DEFAULT 'DEFAULT' COMMENT '任务组名',
      invoke_target varchar(500) NOT NULL COMMENT '调用目标字符串',
      cron_expression varchar(255) DEFAULT '' COMMENT 'cron执行表达式',
      misfire_policy varchar(20) DEFAULT '3' COMMENT '计划执行错误策略(1立即执行 2执行一次 3放弃执行)',
      concurrent char(1) DEFAULT '1' COMMENT '是否并发执行(0允许 1禁止)',
      status char(1) DEFAULT '0' COMMENT '状态(0正常 1暂停)',
      remark varchar(500) DEFAULT '' COMMENT '备注信息',
      PRIMARY KEY (job_id),
      UNIQUE INDEX quartz_job_unique(job_id, job_name, job_group)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='定时任务调度表';
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.hibernate.annotations.DynamicInsert;
    import org.hibernate.annotations.DynamicUpdate;
    import javax.persistence.*;
    import java.io.Serializable;
    
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity
    @DynamicInsert
    @DynamicUpdate
    @Table(name="quartz_job")
    public class QuartzJob implements Serializable {
          
          
        public static final long serialVersionUID = 42L;
    
        /**
         * 任务ID
         */
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name = "job_id", nullable = false)
        private Long jobId;
    
        
        /**
         * 任务名称
         */
        @Column(name = "job_name", length = 64, nullable = false)
        private String jobName;
    
        
        /**
         * 任务组名
         */
        @Column(name = "job_group", length = 64, nullable = false)
        private String jobGroup;
    
        /**
         * 调用目标字符串
         */
        @Column(name = "invoke_target", length = 500, nullable = false)
        private String invokeTarget;
    
        /**
         * cron执行表达式
         */
        @Column(name = "cron_expression", length = 255)
        private String cronExpression;
    
        /**
         * 计划执行错误策略(1立即执行 2执行一次 3放弃执行)
         */
        @Column(name = "misfire_policy", length = 20)
        private String misfirePolicy;
    
        /**
         * 是否并发执行(0允许 1禁止)
         */
        @Column(name = "concurrent")
        private String concurrent;
    
        /**
         * 状态(0正常 1暂停)
         */
        @Column(name = "status")
        private String status;
    
        /**
         * 备注信息
         */
        @Column(name = "remark", length = 500)
        private String remark;
    }
    

SprinUtils工具类

作用:获取 ApplicationContext 中的 bean

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * SpringUtils工具类:获取bean
 */
@Component
public class SpringUtils implements ApplicationContextAware {
    
    
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext arg0) throws BeansException {
    
    
        if (SpringUtils.applicationContext == null) {
    
    
            SpringUtils.applicationContext = arg0;
        }
    }

    // 获取applicationContext
    public static ApplicationContext getApplicationContext() {
    
    
        return applicationContext;
    }

    // 通过name获取 Bean.
    public static Object getBean(String name) {
    
    
        return getApplicationContext().getBean(name);
    }

    // 通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) {
    
    
        return getApplicationContext().getBean(clazz);
    }

    // 通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
    
    
        return getApplicationContext().getBean(name, clazz);
    }
}

ScheduleConstants 静态变量类

静态变量类

import lombok.AllArgsConstructor;
import lombok.Getter;

public class ScheduleConstants {
    
    
    // 计划执行错误策略-默认策略
    public static final String MISFIRE_DEFAULT = "0";
    // 计划执行错误策略-立即执行(立即执行执行所有misfire的任务)
    public static final String MISFIRE_IGNORE_MISFIRES = "1";
    // 计划执行错误策略-执行一次(立即执行一次任务)
    public static final String MISFIRE_FIRE_AND_PROCEED = "2";
    // 计划执行错误策略-放弃执行(什么都不做,等待下次触发)
    public static final String MISFIRE_DO_NOTHING = "3";

    // 任务实例名称
    public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME";
    // 任务内容
    public static final String TASK_PROPERTIES = "TASK_PROPERTIES";


    @AllArgsConstructor
    @Getter
    public enum Status {
    
    

        NORMAL("0"),
        PAUSE("1");

        private String value;
    }
}

任务方法

准备一个任务方法(后面通过反射调用,故此处不用实现 Job 接口):

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@Component("mysqlJob")
public class MysqlJob {
    
    
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    public void execute(String param) {
    
    
        logger.info("执行 Mysql Job,当前时间:{},任务参数:{}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), param);
    }
}

ScheduleConfig 配置代码类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
public class ScheduleConfig {
    
    

    /**
    * SchedulerFactoryBean 继承了 InitializingBean 接口会在类被注入Spring容器后执行 afterPropertiesSet 方法
    */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
    
    
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);

        // quartz参数
        Properties prop = new Properties();
        prop.put("org.quartz.scheduler.instanceName", "shivaScheduler");
        prop.put("org.quartz.scheduler.instanceId", "AUTO");
        // 线程池配置
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        prop.put("org.quartz.threadPool.threadCount", "20");
        prop.put("org.quartz.threadPool.threadPriority", "5");
        // JobStore配置
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
        // 集群配置
        prop.put("org.quartz.jobStore.isClustered", "true");
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");

        // sqlserver 启用
        // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
        factory.setQuartzProperties(prop);

        factory.setSchedulerName("shivaScheduler");
        // 延时启动
        factory.setStartupDelay(1);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        // 可选,QuartzScheduler
        // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
        factory.setOverwriteExistingJobs(true);
        // 设置自动启动,默认为true
        factory.setAutoStartup(true);

        return factory;
    }
}

ScheduleUtils 调度工具类

最核心的代码

import org.quartz.*;

public class ScheduleUtils {
    
    
    /**
     * 得到quartz任务类
     *
     * @param job 执行计划
     * @return 具体执行任务类
     */
    private static Class<? extends Job> getQuartzJobClass(QuartzJob job) {
    
    
        boolean isConcurrent = "0".equals(job.getConcurrent());
        return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
    }

    /**
     * 构建任务触发对象
     */
    public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
    
    
        return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 构建任务键对象
     */
    public static JobKey getJobKey(Long jobId, String jobGroup) {
    
    
        return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
    }

    /**
     * 创建定时任务
     */
    public static void createScheduleJob(Scheduler scheduler, QuartzJob job) throws Exception {
    
    
        // 得到quartz任务类
        Class<? extends Job> jobClass = getQuartzJobClass(job);
        // 构建job信息
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();

        // 表达式调度构建器
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);

        // 按新的cronExpression表达式构建一个新的trigger
        CronTrigger trigger = TriggerBuilder.newTrigger()
            .withIdentity(getTriggerKey(jobId, jobGroup))
            .withSchedule(cronScheduleBuilder).build();

        // 放入参数,运行时的方法可以获取
        jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);

        // 判断是否存在
        if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
    
    
            // 防止创建时存在数据问题 先移除,然后在执行创建操作
            scheduler.deleteJob(getJobKey(jobId, jobGroup));
        }

        scheduler.scheduleJob(jobDetail, trigger);

        // 暂停任务。完成任务与触发器的关联后,如果是暂停状态,会先让调度器停止任务。
        if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
    
    
            scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        }
    }

    /**
     * 设置定时任务策略
     */
    public static CronScheduleBuilder handleCronScheduleMisfirePolicy(QuartzJob job, CronScheduleBuilder cb) throws Exception {
    
    
        switch (job.getMisfirePolicy()) {
    
    
            case ScheduleConstants.MISFIRE_DEFAULT:
                return cb;
            case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
                return cb.withMisfireHandlingInstructionIgnoreMisfires();
            case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
                return cb.withMisfireHandlingInstructionFireAndProceed();
            case ScheduleConstants.MISFIRE_DO_NOTHING:
                return cb.withMisfireHandlingInstructionDoNothing();
            default:
                throw new Exception("The task misfire policy '" + job.getMisfirePolicy()
                    + "' cannot be used in cron schedule tasks");
        }
    }
}

AbstractQuartzJob 抽象任务

这个类将原本 execute 方法执行的任务,下放到了子类重载的 doExecute 方法中

import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import java.util.Date;

@Slf4j
public abstract class AbstractQuartzJob implements Job {
    
    

    /**
     * 线程本地变量
     */
    private static ThreadLocal<Date> threadLocal = new ThreadLocal<>();

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
    
    
        QuartzJob job = new QuartzJob();
        BeanUtils.copyProperties(context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES), job);
        try {
    
    
            before(context, job);
            doExecute(context, job);
            after(context, job, null);
        } catch (Exception e) {
    
    
            log.error("任务执行异常  - :", e);
            after(context, job, e);
        }
    }

    /**
     * 执行前
     *
     * @param context 工作执行上下文对象
     * @param job     系统计划任务
     */
    protected void before(JobExecutionContext context, QuartzJob job) {
    
    
        threadLocal.set(new Date());
    }

    /**
     * 执行后
     *
     * @param context 工作执行上下文对象
     * @param sysJob  系统计划任务
     */
    protected void after(JobExecutionContext context, QuartzJob sysJob, Exception e) {
    
    

    }

    /**
     * 执行方法,由子类重载
     *
     * @param context 工作执行上下文对象
     * @param job     系统计划任务
     * @throws Exception 执行过程中的异常
     */
    protected abstract void doExecute(JobExecutionContext context, QuartzJob job) throws Exception;
}

AbstractQuartzJob 实现类

两个实现类,分了允许并发和不允许并发,差别就是一个是否允许并发任务的注解:

import org.quartz.JobExecutionContext;

public class QuartzJobExecution extends AbstractQuartzJob {
    
    
    @Override
    protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
    
    
        JobInvokeUtil.invokeMethod(job);
    }
}
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;

@DisallowConcurrentExecution
public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
    
    
    @Override
    protected void doExecute(JobExecutionContext context, QuartzJob job) throws Exception {
    
    
        JobInvokeUtil.invokeMethod(job);
    }
}

JobInvokeUtil 反射调用 job 工具类

JobInvokeUtil 通过反射,进行实际的方法调用

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

public class JobInvokeUtil {
    
    
    /**
     * 执行方法
     *
     * @param job 系统任务
     */
    public static void invokeMethod(QuartzJob job) throws Exception {
    
    
        String invokeTarget = job.getInvokeTarget();
        String beanName = getBeanName(invokeTarget);
        String methodName = getMethodName(invokeTarget);
        List<Object[]> methodParams = getMethodParams(invokeTarget);

        if (!isValidClassName(beanName)) {
    
    
            Object bean = SpringUtils.getBean(beanName);
            invokeMethod(bean, methodName, methodParams);
        } else {
    
    
            Object bean = Class.forName(beanName).newInstance();
            invokeMethod(bean, methodName, methodParams);
        }
    }

    /**
     * 调用任务方法
     *
     * @param bean         目标对象
     * @param methodName   方法名称
     * @param methodParams 方法参数
     */
    private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
        throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
        InvocationTargetException {
    
    
        if (!CollectionUtils.isEmpty(methodParams)) {
    
    
            Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
            method.invoke(bean, getMethodParamsValue(methodParams));
        } else {
    
    
            Method method = bean.getClass().getDeclaredMethod(methodName);
            method.invoke(bean);
        }
    }

    /**
     * 校验是否为为class包名
     *
     * @param invokeTarget 名称
     * @return true是 false否
     */
    public static boolean isValidClassName(String invokeTarget) {
    
    
        return StringUtils.countMatches(invokeTarget, ".") > 1;
    }

    /**
     * 获取bean名称
     *
     * @param invokeTarget 目标字符串
     * @return bean名称
     */
    public static String getBeanName(String invokeTarget) {
    
    
        String beanName = StringUtils.substringBefore(invokeTarget, "(");
        return StringUtils.substringBeforeLast(beanName, ".");
    }

    /**
     * 获取bean方法
     *
     * @param invokeTarget 目标字符串
     * @return method方法
     */
    public static String getMethodName(String invokeTarget) {
    
    
        String methodName = StringUtils.substringBefore(invokeTarget, "(");
        return StringUtils.substringAfterLast(methodName, ".");
    }

    /**
     * 获取method方法参数相关列表
     *
     * @param invokeTarget 目标字符串
     * @return method方法相关参数列表
     */
    public static List<Object[]> getMethodParams(String invokeTarget) {
    
    
        String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
        if (StringUtils.isEmpty(methodStr)) {
    
    
            return null;
        }
        String[] methodParams = methodStr.split(",");
        List<Object[]> classs = new LinkedList<>();
        for (String methodParam : methodParams) {
    
    
            String str = StringUtils.trimToEmpty(methodParam);
            // String字符串类型,包含'
            if (StringUtils.contains(str, "'")) {
    
    
                classs.add(new Object[]{
    
    StringUtils.replace(str, "'", ""), String.class});
            }
            // boolean布尔类型,等于true或者false
            else if (StringUtils.equals(str, "true") || StringUtils.equalsIgnoreCase(str, "false")) {
    
    
                classs.add(new Object[]{
    
    Boolean.valueOf(str), Boolean.class});
            }
            // long长整形,包含L
            else if (StringUtils.containsIgnoreCase(str, "L")) {
    
    
                classs.add(new Object[]{
    
    Long.valueOf(StringUtils.replaceChars(str, "L", "")), Long.class});
            }
            // double浮点类型,包含D
            else if (StringUtils.containsIgnoreCase(str, "D")) {
    
    
                classs.add(new Object[]{
    
    Double.valueOf(StringUtils.replaceChars(str, "D", "")), Double.class});
            }
            // 其他类型归类为整形
            else {
    
    
                classs.add(new Object[]{
    
    Integer.valueOf(str), Integer.class});
            }
        }
        return classs;
    }

    /**
     * 获取参数类型
     *
     * @param methodParams 参数相关列表
     * @return 参数类型列表
     */
    public static Class<?>[] getMethodParamsType(List<Object[]> methodParams) {
    
    
        Class<?>[] classs = new Class<?>[methodParams.size()];
        int index = 0;
        for (Object[] os : methodParams) {
    
    
            classs[index] = (Class<?>) os[1];
            index++;
        }
        return classs;
    }

    /**
     * 获取参数值
     *
     * @param methodParams 参数相关列表
     * @return 参数值列表
     */
    public static Object[] getMethodParamsValue(List<Object[]> methodParams) {
    
    
        Object[] classs = new Object[methodParams.size()];
        int index = 0;
        for (Object[] os : methodParams) {
    
    
            classs[index] = (Object) os[0];
            index++;
        }
        return classs;
    }
}

启动程序,查看调度器是否启动

2021-10-06 16:26:05.162  INFO 10764 --- [shivaScheduler]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 1 seconds
2021-10-06 16:26:05.306  INFO 10764 --- [shivaScheduler]] org.quartz.core.QuartzScheduler          : Scheduler shivaScheduler_$_DESKTOP-OKMJ1351633508761366 started.

QuartzSheduleTaskImpl

先将任务设置为暂停状态,数据库插入成功后,再在调度器新增任务,再手动根据 ID启动任务。

import com.duran.ssmtest.schedule.quartz.respositories.QuartzJob;
import com.duran.ssmtest.schedule.quartz.respositories.QuartzJobRespository;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import javax.annotation.PostConstruct;
import java.util.List;

@Slf4j
@Service
public class QuartzSheduleTaskImpl {
    
    

    @Autowired
    private QuartzJobRespository quartzJobRespository;
    @Autowired
    private SchedulerFactoryBean schedulerFactoryBean;

    private Scheduler scheduler;


    /**
     * 项目启动时,初始化定时器
     * 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
     */
    @PostConstruct
    public void init() throws Exception {
    
    
        scheduler = schedulerFactoryBean.getScheduler();

        scheduler.clear();
        List<QuartzJob> jobList = quartzJobRespository.findAll();
        for (QuartzJob job : jobList) {
    
    
            ScheduleUtils.createScheduleJob(scheduler, job);
        }
    }

    /**
     * 新增定时任务
     */
    @Transactional(rollbackFor = Exception.class)
    public int insertJob(QuartzJob job){
    
    
        // 先将任务设置为暂停状态
        job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
        try {
    
    
            Long jobId = quartzJobRespository.save(job).getJobId();
            ScheduleUtils.createScheduleJob(scheduler, job);
            return jobId.shortValue();
        } catch (Exception e) {
    
    
            log.error("failed to insertJob", e);
            // 手动回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return 0;
        }
    }

    /**
     * 修改定时任务的状态:启动任务/暂停任务
     */
    @Transactional(rollbackFor = Exception.class)
    public int changeStatus(Long jobId, String status) throws SchedulerException {
    
    
        QuartzJob job = quartzJobRespository.findById(jobId).orElse(null);
        if (job == null) {
    
    
            return 0;
        }

        job.setStatus(status);
        try {
    
    
            quartzJobRespository.save(job);
        } catch (Exception e) {
    
    
            log.error("failed to changeStatus", e);
            // 手动回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return 0;
        }

        //根据状态来启动或者关闭
        if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) {
    
    
            scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, job.getJobGroup()));
        } else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) {
    
    
            scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, job.getJobGroup()));
        }

        return 1;
    }

    /**
     * 删除定时任务
     */
    @Transactional(rollbackFor = Exception.class)
    public int deleteJob(Long jobId){
    
    
        QuartzJob job = quartzJobRespository.findById(jobId).orElse(null);
        if (job == null) {
    
    
            return 0;
        }
        try {
    
    
            quartzJobRespository.deleteById(jobId);
            scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, job.getJobGroup()));
            return 1;
        } catch (Exception e) {
    
    
            log.error("failed to insertJob", e);
            // 手动回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return 0;
        }
    }
}

新建任务请求

{
    
    
  "concurrent": "1",
  "cronExpression": "0/10 * * * * ?",
  "invokeTarget": "mysqlJob.execute('got it!!!')",
  "jobGroup": "mysqlGroup",
  "jobName": "新增 mysqlJob 任务",
  "misfirePolicy": "1",
  "remark": "",
  "status": "0"
}

猜你喜欢

转载自blog.csdn.net/footless_bird/article/details/126844246
今日推荐