调度框架Quartz

引言

针对耗时操作、定时操作和周期操作等,常常会通过异步线程调用执行这些操作。实现定时异步操作有多种方案,比如使用Spring boot异步机制和调度框架等。Spring boot异步机制使用简单,只需定义异步方法便可执行异步操作,但是任务监控、任务复杂操作不易实现。调度框架管理任务更加灵活,提供更加丰富的功能,常见的调度框架有Quartz、ElasticJob和XXL-JOB等

一、Quartz简介

Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵 活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。它有很多特征,如:数据库支持,集群,插件,EJB 作业预构 建,JavaMail 及其它,支持 cron-like 表达式等等。

Quartz 具有如下几个关键概念:

  1. Job 定义了一个任务的执行逻辑。
  2. JobDetail 是 Job 的一个执行实例。
  3. Trigger 是 JobDetail 的执行计划,定义了 JobDetail 什么时候执行、异常情况怎么执行等。
  4. Scheduler是调度容器,用来注册 JobDetail 和 Trigger,管理执行任务的执行、暂停、恢复等操作。
  5. 监听器,分为 JobListener、TriggerListener和SchedulerListener,分别用来监听 JobDetail、Trigger 和Scheduler 的事件并做出增强处理,给任务协调执行提供了支持。

1、Job

通过实现Job接口,定义任务的执行逻辑。在Job接口中,有一个方法 execute(JobExecutionContext var1),JobExecutionContext为任务执行的上下文,可以通过这个参数来获取执行任务实例JobDetail、任务参数JobDataMap、执行计划Trigger和Scheduler等,JobExecutionContext参数为实现任务执行逻辑提供了较高的灵活性 。

java 复制代码
public interface Job {  
    void execute(JobExecutionContext var1) throws JobExecutionException;  
}

2、JobDetail

JobDetail 表示一个 Job 的执行实例,通过 JobBuilder 可以创建一个JobDetail实例,每个JobDetail需要指定一个 Identity 标识,Identity 标识分为 name 和 group 两部分,需要保证调度上下文中 Identity 唯一。

java 复制代码
JobDetail jobDetail = JobBuilder.newJob(jobClass)  
    .withIdentity(name, group)
    // .storeDurably() 用来持久化,即执行该任务后,数据库不会删除该任务,下次定义Trigger时还可以使用
    .setJobData(jobDataMap)  
    .build();

3、Trigger

使用Trigger可以配置JobDetail的执行计划,通过使用 TriggerBuilder 可以创建一个 Trigger 实例,并通过 withIdentity 方法可以指定 JobDetail 实例,通过 withSchedule 方法配置调度策略。

java 复制代码
TriggerBuilder.newTrigger()  
    .withIdentity(jobKey.getName(), jobKey.getGroup())  
    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(seconds).withRepeatCount(1))  
    .build();

ScheduleBuilder用来配置调度策略,ScheduleBuilder具有如下四个子类

4、Scheduler

Scheduler 用来注册JobDetail、Trigger和监听器,可以管理任务的执行生命周期。

java 复制代码
/**
该接口具有较多方法,只列举几个较重要的。通过该接口还可以获取调度容器有哪些JobDetail和Trigger
*/
public interface Scheduler {
// 添加任务
Date scheduleJob(JobDetail var1, Trigger var2) throws SchedulerException;  

// 添加任务,对应的JobDetail已持久化
Date scheduleJob(Trigger var1) throws SchedulerException;

// 修改执行计划
Date rescheduleJob(TriggerKey var1, Trigger var2) throws SchedulerException;

// 删除任务
boolean deleteJob(JobKey var1) throws SchedulerException;

// 暂停任务
void pauseJob(JobKey var1) throws SchedulerException;

// 恢复任务
void resumeJob(JobKey var1) throws SchedulerException;

// 获取监听器,获取的ListenerManager可以注册监听器
ListenerManager getListenerManager() throws SchedulerException;
}

Scheduler 实现类如下图所示,默认使用 StdScheduler

5、监听器

监听器分为 JobListener、TriggerListener 和 SchedulerListener, JobListener 用来监听JobDetail, TriggerListener 用来监听Trigger, SchedulerListener 用来监听 Scheduler。通过使用 Scheduler.getListenerManager 方法获取的 ListenerManager 实例对象可以注册监听器。

java 复制代码
public interface JobListener {  
    String getName();  
    // 任务执行前处理
    void jobToBeExecuted(JobExecutionContext var1);  
    // 在JobDetail即将被执行,但又被TriggerListener否决时会调用该方法
    void jobExecutionVetoed(JobExecutionContext var1);  
    // 任务执行后处理
    void jobWasExecuted(JobExecutionContext var1, JobExecutionException var2);  
}
java 复制代码
public interface TriggerListener {  
    String getName();  
    // 调度器触发时处理
    void triggerFired(Trigger var1, JobExecutionContext var2);  
    // 在Trigger即将被执行,但又被Scheduler否决时会调用该方法
    boolean vetoJobExecution(Trigger var1, JobExecutionContext var2);  
    // 触发器错失触发时处理
    void triggerMisfired(Trigger var1);  
    // 触发器触发并且对应的JobDetail执行完后处理
    void triggerComplete(Trigger var1, JobExecutionContext var2, Trigger.CompletedExecutionInstruction var3);  
}
java 复制代码
public interface SchedulerListener {  
void jobScheduled(Trigger var1);  
  
void jobUnscheduled(TriggerKey var1);  
  
void triggerFinalized(Trigger var1);  
  
void triggerPaused(TriggerKey var1);  
  
void triggersPaused(String var1);  
  
void triggerResumed(TriggerKey var1);  
  
void triggersResumed(String var1);  
  
void jobAdded(JobDetail var1);  
  
void jobDeleted(JobKey var1);  
  
void jobPaused(JobKey var1);  
  
void jobsPaused(String var1);  
  
void jobResumed(JobKey var1);  
  
void jobsResumed(String var1);  
  
void schedulerError(String var1, SchedulerException var2);  
  
void schedulerInStandbyMode();  
  
void schedulerStarted();  
  
void schedulerStarting();  
  
void schedulerShutdown();  
  
void schedulerShuttingdown();  
  
void schedulingDataCleared();  
}

二、Quartz 简单使用

Spring boot 使用 Quartz 有两种任务存储方式,分别是内存和数据库,这里介绍基于数据库存储方式的使用

1、引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

在该依赖中,QuartzAutoConfiguration 提供了 Quartz 的自动配置,该类创建了一个Scheduler Bean,具体类为Stdcheduler,同时会自动扫描 JobDetail 和 Trigger Bean,并将这些 Bean 存放到数据库中。

2、添加 Quartz 配置

在 application.yml 文件中添加配置

yml 复制代码
spring:
  application:
    name: quartz-task
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/quartz_test?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
    username: 
    password: 
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: always # 启动时初始化 Quartz 数据表方式

Quartz 数据库表文件可以查看 org.quartz-scheduler 包,路径为:quartz-x.x.x.jar/org/quartz/impl/jdbcjobstore ,在 SQL脚本中首先会删除表,再创建表。如果需要在项目启动时保留表中数据,可以让项目自己管理 quartz 的 SQL 脚本,不执行 Quartz 的数据库初始化。SQL文件如下图所示

3、定义任务 Job

java 复制代码
@Slf4j
public class HelloWorldJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobKey key = jobDetail.getKey();
        log.info("HelloWorldJob begin. name: {}, group: {}", key.getName(), key.getGroup());
        Trigger trigger = jobExecutionContext.getTrigger();
        JobDataMap jobDataMap = trigger.getJobDataMap();
        Set<Map.Entry<String, Object>> entrySet = jobDataMap.entrySet();
        for (Map.Entry<String, Object> entry : entrySet) {
            log.info("trigger data: key = {}, value = {}.", entry.getKey(), entry.getValue());
        }
        log.info("HelloWorldJob end. name: {}, group: {}", key.getName(), key.getGroup());
    }
}

4、配置任务执行实例

执行任务有两种方式,一种是定义JobDetail和Trigger Bean,并将其注入到Spring容器中;另一种时,调用 Scheduler Bean提供的方法添加任务。

1、方式一

定义JobDetail和Trigger Bean

java 复制代码
@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail helloWorldJobDetail() {
        JobKey jobKey = new JobKey("HelloWorld", "DefaultGroup");
        return JobBuilder.newJob(HelloWorldJob.class)
                .withIdentity(jobKey)
                .storeDurably()
                .build();
    }
    
    @Bean
    public Trigger helloWorldTrigger() {
        JobKey jobKey = new JobKey("HelloWorld", "DefaultGroup");
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        Date now = new Date();
        String nowAsString = sdf.format(now);
        return TriggerBuilder.newTrigger()
                    .forJob(jobKey)
                    .usingJobData("execute time", nowAsString)
                    .startNow()
                    // 隔3秒执行一次,总共执行 3+1 次
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).withRepeatCount(3))
                    .build();
    }
}

执行结果:

2、方式二

调用 Scheduler Bean 方法添加任务、暂停任务等

java 复制代码
@Component
public class SchedulerManager {
    @Autowired
    private Scheduler scheduler;
}

自动注入Scheduler后,可调用它的方法

java 复制代码
// 部分方法,添加任务、暂停任务、恢复任务
public interface Scheduler {

    List<JobExecutionContext> getCurrentlyExecutingJobs() throws ShedulerException;

    ListenerManager getListenerManager() throws SchedulerException;

    Date scheduleJob(JobDetail var1, Trigger var2) throws SchedulerException;

    Date scheduleJob(Trigger var1) throws SchedulerException;

    void scheduleJobs(Map<JobDetail, Set<? extends Trigger>> var1, boolean var2) throws SchedulerException;

    void scheduleJob(JobDetail var1, Set<? extends Trigger> var2, boolean var3) throws SchedulerException;

    boolean unscheduleJob(TriggerKey var1) throws SchedulerException;

    boolean unscheduleJobs(List<TriggerKey> var1) throws SchedulerException;
    // 修改执行计划
    Date rescheduleJob(TriggerKey var1, Trigger var2) throws SchedulerException;

    void addJob(JobDetail var1, boolean var2) throws SchedulerException;

    void addJob(JobDetail var1, boolean var2, boolean var3) throws SchedulerException;

    boolean deleteJob(JobKey var1) throws SchedulerException;

    boolean deleteJobs(List<JobKey> var1) throws SchedulerException;

    void triggerJob(JobKey var1) throws SchedulerException;

    void triggerJob(JobKey var1, JobDataMap var2) throws SchedulerException;

    void pauseJob(JobKey var1) throws SchedulerException;

    void pauseJobs(GroupMatcher<JobKey> var1) throws SchedulerException;

    void pauseTrigger(TriggerKey var1) throws SchedulerException;

    void pauseTriggers(GroupMatcher<TriggerKey> var1) throws SchedulerException;

    void resumeJob(JobKey var1) throws SchedulerException;

    void resumeJobs(GroupMatcher<JobKey> var1) throws SchedulerException;

    void resumeTrigger(TriggerKey var1) throws SchedulerException;

    void resumeTriggers(GroupMatcher<TriggerKey> var1) throws SchedulerException;

    void pauseAll() throws SchedulerException;

    void resumeAll() throws SchedulerException;

    List<String> getJobGroupNames() throws SchedulerException;
}

5、@DisallowConcurrentExecution和@PersistJobDataAfterExecution注释说明

  1. @DisallowConcurrentExecution:用在 Job 接口实现类上,用来保证不会同时执行该 Job 接口实现类执行实例
  2. @PersistJobDataAfterExecution:用在 Job 接口实现类,表示在正常执行完Job后, JobDataMap中的数据应该被改动,下次执行相同的job时,会接收到已经更新的数据

6、如何在 Job 执行逻辑中使用 Spring 容器 Bean

由于 Job 接口实现类不被 Spring 容器管理,无法直接通过自动注入注入Bean,需要定义一个静态方法获取 Spring Bean。

java 复制代码
@Component
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtil.applicationContext = applicationContext;
    }

    /**
     * 根据名称获取bean
     *
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName) {
        if (beanName == null) {
            return null;
        }
        return applicationContext.getBean(beanName);
    }

    public static <T> T getBean(Class<T> clazz) {
        if (clazz == null) {
            return null;
        }
        return applicationContext.getBean(clazz);
    }
}

三、总结

通过使用 Quartz 调度框架,可以灵活的管理任务。在 Quartz 中,具有如下几个关键概念:Job、JobDetail、Trigger、Scheduler 和 Listener。另外,管理、监控任务等操作,关键在于使用 Scheduler 方法。

相关推荐
花哥码天下2 分钟前
Oracle下载JDK无需登录
java·开发语言
考虑考虑10 分钟前
go格式化时间
后端·go
摇滚侠18 分钟前
Spring Boot 3零基础教程,yml语法细节,笔记16
java·spring boot·笔记
wei84406787230 分钟前
本地项目第一次推送到gitee上的完整命令
java·android studio
星球奋斗者33 分钟前
计算机方向如何才能更好的找到工作?(成长心得)
java·后端·考研·软件工程·改行学it
Jabes.yang33 分钟前
互联网大厂Java面试:缓存技术与监控运维的深度探讨
java·面试指南·缓存技术·监控运维
海梨花40 分钟前
【八股笔记】SSM
java·开发语言·笔记·后端·面试·框架
珹洺1 小时前
Java-Spring入门指南(二十五)Android 的历史,认识移动应用和Android 基础知识
android·java·spring
只想码代码1 小时前
什么是程序计数器?
java·jvm
JAVA学习通1 小时前
OJ竞赛平台----C端题目列表
java·开发语言·jvm·vue.js·elasticsearch