定时调度框架Quartz使用

使用背景

在最近的项目中遇到一个需要使用到动态定时任务的需求,即定时任务的调用时间不是在某个固定时间自动执行,而是由用户控制,并且需要持久化。因此在网上搜了一下,发现了一个基于Java开发的Quartz定时任务调度框架,很符合我的需求,因此记录一下便于以后再次使用。

Quartz相关概念

quartz的使用过程主要涉及到以下几个概念:

1. Job

Job 是一个抽象接口,用户可以实现该接口,并且重写execute方法,在execute方法内,是用户需要实现的具体的功能

2. JobDetail

JobDetail 是对于Job的描述

3. JobDataMap

JobDataMap 是一个Map对象,用于存放Job需要的参数

4. Trigger

Trigger 用于触发定时任务

5. Scheduler

Scheduler 用于调度 JobDetail 和 Trigger

配置

首先创建了一个基础的SpringBoot项目(虽然Quartz不需要依赖SpringBoot也可以运行)
Maven依赖如下:

点击查看代码
    <dependencies>
        <!--    SpringBoot依赖    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.7.18</version>
        </dependency>
        <!--    SpringBootTest依赖    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.7.18</version>
        </dependency>
        <!--    Quartz依赖    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
            <version>2.7.18</version>
        </dependency>
        <!--    数据库依赖,用于持久化定时任务    -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.7.18</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.18</version>
        </dependency>
        <!--    Lombok    -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
    </dependencies>

项目结构如下

项目结构

在项目启动后,SpringBoot已经维护了一个Scheduler

下面将以预售商品自动到期自动下架作为例子,使用Quartz框架

1. 创建并且调度任务


在 CommodityAutoOfflineJob 类中实现了Job接口,并且模拟下架商品的逻辑

点击查看代码
/**
 * 商品自动下架定时任务
 *
 * @author panlijun
 * @since 2024/10/31 21:14
 */
@Slf4j
public class AutoOfflineCommodityJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("开始执行商品自动下架定时任务");
        try {
            Thread.sleep(5_000L);
        } catch (InterruptedException e) {
            log.error("商品自动下架定时任务执行失败", e);
            throw new RuntimeException(e);
        }
        log.info("商品自动下架定时任务执行完毕");
    }
}

在 TestController 中创建了JobDetail 、Trigger 并且使用Scheduler对任务进行调度

点击查看代码
/**
 * @author panlijun
 * @since 2024/10/31 21:26
 */
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

    @Resource
    private Scheduler scheduler;


    @SneakyThrows
    @RequestMapping("/creatAutoOfflineCommodityJob")
    public String test(@RequestParam String endTime) {
        log.info("endTime:{}",endTime);

        log.info("创建JobDetail");
        JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class)
                .withIdentity("autoOfflineCommodityJob", "Commodity")
                .build();

        log.info("创建Trigger");
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("autoOfflineCommodityTrigger", "Commodity")
                .startAt(format.parse(endTime))
                .build();

        log.info("调度任务");
        scheduler.scheduleJob(jobDetail, trigger);

        return "定时任务将于" + endTime + "执行";
    }
}

2. 使用自定义参数

在一些业务中,执行定时任务需要依靠一些具体的参数才能执行,上面的代码就不能满足需要了,因此对代码进行修改如下:

在Controller中额外接受要需要下架的商品id

public String test(@RequestParam String endTime, @RequestParam Integer commodityId)

创建JobDataMap对象

log.info("创建JobDataMap");
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("COMMODITY_ID", commodityId);

将接收到的商品id放入Map,并在创建JobDetail时作为参数传入

log.info("创建JobDetail");
JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class)
.withIdentity("autoOfflineCommodityJob", "Commodity")
.usingJobData(jobDataMap)
.build();

点击查看代码
    @SneakyThrows
    @RequestMapping("/creatAutoOfflineCommodityJob")
    public String test(@RequestParam String endTime,
                       @RequestParam Integer commodityId) {
        log.info("endTime:{}", endTime);

        log.info("创建JobDataMap");
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("COMMODITY_ID", commodityId);

        log.info("创建JobDetail");
        JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class)
                .withIdentity("autoOfflineCommodityJob", "Commodity")
                .usingJobData(jobDataMap)
                .build();

        log.info("创建Trigger");
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("autoOfflineCommodityTrigger", "Commodity")
                .startAt(format.parse(endTime))
                .build();

        log.info("调度任务");
        scheduler.scheduleJob(jobDetail, trigger);

        return "定时任务将于" + endTime + "执行";
    }

并在定时任务中获取传入的参数

JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
Integer commodityId = (Integer) jobDataMap.get("COMMODITY_ID");

点击查看代码
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("开始执行商品自动下架定时任务");
        try {
            JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
            Integer commodityId = (Integer) jobDataMap.get("COMMODITY_ID");
            log.info("要下架的商品ID为:{}", commodityId);
            Thread.sleep(5_000L);
        } catch (InterruptedException e) {
            log.error("商品自动下架定时任务执行失败", e);
            throw new RuntimeException(e);
        }
        log.info("商品自动下架定时任务执行完毕");
    }

定时任务执行结果如下:


3. 定时任务持久化

Quartz默认将定时任务的数据保存在内存,每次系统重启都会丢失待运行的定时任务,这显然是不能接受的,因此需要对定时任务进行持久化,好在Quartz提供了对定时任务持久化的方法。

3.1 创建相关数据库表结构

以Mysql为例,需要创建以下表:

创建的表的SQL脚本可以在Quartz的代码仓库中找到,链接如下

https://github.com/quartz-scheduler/quartz/releases

SQL脚本就在下载文件的 quartz-2.3.2\quartz-core\src\main\resources\org\quartz\impl\jdbcjobstore 文件夹下

该文件夹下有很多种类的数据库的脚本,上图中框出的文件是MySQL的脚本

3.2 在项目中添加配置数据库和Quartz相关配置

点击查看代码
spring:
  application:
    name: quartz-study
  # 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring_task_test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
    username: root
    password: 123456
  # quartz 持久化配置
  quartz:
    # 默认为memory,选择jdbc时,可以选择将定时任务持久化到数据库
    job-store-type: jdbc
    jdbc:
      # 每次启动项目时是否初始化表, 建议设置为never, 并且手动运行SQL脚本, 初始化数据库
      initialize-schema: never

3.3 修改代码,持久化定时任务

在创建JobDetail时设置.storeDurably(true),就能把定时任务持久化到数据库

点击查看代码
        log.info("创建JobDetail");
        JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class)
                .withIdentity("autoOfflineCommodityJob", "Commodity")
                .storeDurably(true)
                .usingJobData(jobDataMap)
                .build();

再次重启项目并调用接口

并查看数据库,发现数据库中增加了一条JobDetail数据

4. 恢复中断定时任务

在定时任务运行过程中,进程被终止了,在重启项目后,是不会重新执行被中断的定时任务的

如果需要恢复运行中被中断的定时任务,只需要设置 .requestRecovery(true) 就可以在重启时重新执行被中断的定时任务

点击查看代码
        log.info("创建JobDetail");
        JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class)
                .withIdentity("autoOfflineCommodityJob", "commodity")
                .usingJobData(jobDataMap)
                .requestRecovery(true)
                .storeDurably(true)
                .build();

注意事项

在Quartz中,JobDetail .withIdentity("autoOfflineCommodityJob", "commodity")中的两个参数也就是namegroup构成的二元组是不可以重复,在以下情况

  1. 不使用持久化定时任务时,上一个同名 JobDetail 还未完成

  2. 使用持久化定时任务时,创建同名定时任务,即使上一个定时任务已经完成

会出现无法创建定时任务的情况

因此,如果可以复用JobDetail,则尽量复用,当无法复用JobDetail时,则需要给JobDetail不同的namegroup

点击查看代码
        // 根据雪花算法生成ID
        Snowflake snowflake = IdUtil.getSnowflake();

        log.info("创建JobDetail");
        JobDetail jobDetail = JobBuilder.newJob(AutoOfflineCommodityJob.class)
                .withIdentity(snowflake.nextIdStr() + "_Job", "commodity")
                .usingJobData(jobDataMap)
                .requestRecovery(true)
                .storeDurably(true)
                .build();

        log.info("创建Trigger");
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(snowflake.nextIdStr() + "_Trigger", "Commodity")
                .startAt(format.parse(endTime))
                .build();
posted @ 2024-11-01 00:30  awqaear  阅读(163)  评论(0)    收藏  举报