Quartz任务调度框架设计与实现

189 阅读5分钟

spring-boot-starter-quartz

设计

  • 系统表设计
create table sys_quartz_job
(
    id          varchar(32) comment 'id' primary key,
    jobClass    varchar(64)                        not null comment '任务类名',
    cron        varchar(64)                        not null comment 'cron表达式',
    params      varchar(512)                       null comment '参数',
    description varchar(128)                       null comment '任务描述',
    status      tinyint  default 0                 not null comment '任务状态 0-正常 1-暂停',
    createTime  datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    updateTime  datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '定时任务';

方法设计

  • 查询定时任务
  • 添加定时任务
  • 更新定时任务
  • 暂停定时任务
  • 恢复定时任务
  • 删除定时任务
  • 立即执行一次任务
  • 启动后应该自动将正常任务注册

添加定时任务->暂停定时任务->立即执行一次任务->恢复定时任务->->删除定时任务 (测试流程)


代码实现

  • 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  • 配置
spring:
  # 定时任务
  quartz:
    # 任务信息存储至数据库
    job-store-type: jdbc
    jdbc:
      # 自动生成表,若已有表数据请务必关闭,第一次可选ALWAYS(ALWAYS/EMBEDDED/NEVER)
      initialize-schema: NEVER
    properties:
      org:
        quartz:
          scheduler:
            # 允许调度程序节点一次获取(触发)的最大触发器数
            batchTriggerAcquisitionMaxCount: 5
          jobStore:
            # 加锁调度
            acquireTriggersWithinLock: true
            # “容忍”触发器经过下一次触发时间的毫秒数
            misfireThreshold: 10000
  • 启动类加
@EnableScheduling
  • 实体类
package com.lty.model.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 定时任务
 */
@Data
@TableName("sys_quartz_job")
public class QuartzJob {

    /**
     * id
     */
    @TableId(type = IdType.ASSIGN_UUID)
    private String id;

    /**
     * 任务类名
     */
    private String jobClass;

    /**
     * Cron表达式
     */
    private String cron;

    /**
     * 参数
     */
    private String params;

    /**
     * 任务描述
     */
    private String description;

    /**
     * 任务状态(0-正常,1-暂停)
     */
    private Integer status;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
  • 常量
package com.lty.constant;

/**
 * 定时任务常量
 */
public class QuartzJobConstant {

    /**
     * 定时任务状态-正常
     */
    public static final Integer JOB_STATUS_NORMAL = 0;

    /**
     * 定时任务状态-暂停
     */
    public static final Integer JOB_STATUS_PAUSED = 1;

    /**
     * 定时任务组-默认
     */
    public static final String JOB_GROUP_DEFAULT = "JOB_GROUP";

    /**
     * 定时任务参数-任务参数key
     */
    public static final String JOB_PARAMS_KEY = "params";
}

MapperXML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lty.mapper.QuartzJobMapper">

</mapper>

Mapper

package com.lty.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lty.model.entity.QuartzJob;

public interface QuartzJobMapper extends BaseMapper<QuartzJob> {
}

Service

package com.lty.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.lty.model.entity.QuartzJob;
import org.quartz.SchedulerException;

public interface QuartzJobService extends IService<QuartzJob> {
    /**
     * 添加定时任务
     */
    void addJob(QuartzJob job) throws SchedulerException;

    /**
     * 更新定时任务
     */
    void updateJob(QuartzJob job) throws SchedulerException;

    /**
     * 删除定时任务
     */
    void deleteJob(String jobId) throws SchedulerException;

    /**
     * 暂停定时任务
     */
    void pauseJob(String jobId) throws SchedulerException;

    /**
     * 恢复定时任务
     */
    void resumeJob(String jobId) throws SchedulerException;

    /**
     * 立即执行定时任务
     */
    void runJobNow(String jobId) throws SchedulerException;
}

ServiceImpl

package com.lty.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lty.mapper.QuartzJobMapper;
import com.lty.model.entity.QuartzJob;
import com.lty.service.QuartzJobService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class QuartzJobServiceImpl extends ServiceImpl<QuartzJobMapper, QuartzJob> implements QuartzJobService {
    private final Scheduler scheduler;

    @Override
    public void addJob(QuartzJob job) throws SchedulerException {
        // 1. 验证Cron表达式
        if (!CronExpression.isValidExpression(job.getCron())) {
            throw new IllegalArgumentException("无效的Cron表达式: " + job.getCron());
        }

        // 2. 创建JobDetail
        JobDetail jobDetail = JobBuilder.newJob(getJobClass(job.getJobClass()))
                .withIdentity(job.getId(), "JOB_GROUP")
                .withDescription(job.getDescription())
                .build();

        // 3. 设置参数
        if (job.getParams() != null) {
            jobDetail.getJobDataMap().put("params", job.getParams());
        }

        // 4. 创建CronTrigger
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(job.getId() + "_TRIGGER", "TRIGGER_GROUP")
                .withSchedule(CronScheduleBuilder.cronSchedule(job.getCron())
                        .withMisfireHandlingInstructionDoNothing()) // 错过触发的处理策略
                .build();

        // 5. 注册任务
        scheduler.scheduleJob(jobDetail, trigger);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateJob(QuartzJob job) throws SchedulerException {
        updateById(job); // 更新数据库记录
    }

    @Override
    public void deleteJob(String jobId) throws SchedulerException {
        // 1. 暂停该任务的触发器(Trigger)
        scheduler.pauseTrigger(TriggerKey.triggerKey(jobId));
        // 2. 解除该触发器与任务的绑定关系(从调度器中移除 Trigger)
        scheduler.unscheduleJob(TriggerKey.triggerKey(jobId));
        // 3. 最终删除 Job 本身
        scheduler.deleteJob(new JobKey(jobId, "JOB_GROUP"));
        // 删除数据库记录
        removeById(jobId);
    }

    @Override
    public void pauseJob(String jobId) throws SchedulerException {
        // 1. 暂停任务
        scheduler.pauseJob(new JobKey(jobId, "JOB_GROUP"));
        // 2. 更新数据库状态
        QuartzJob job = getById(jobId);
        if (Objects.nonNull(job)) {
            job.setStatus(1); // 1-暂停
            updateById(job);
        }
    }

    @Override
    public void resumeJob(String jobId) throws SchedulerException {
        // 1. 恢复任务
        scheduler.resumeJob(new JobKey(jobId, "JOB_GROUP"));
        // 2. 更新数据库状态
        QuartzJob job = getById(jobId);
        if (Objects.nonNull(job)) {
            job.setStatus(0); // 0-正常
            updateById(job);
        }
    }

    @Override
    public void runJobNow(String jobId) throws SchedulerException {
        JobKey jobKey = new JobKey(jobId);
        scheduler.triggerJob(jobKey);
        log.info("立即执行任务: {}", jobId);
    }

    // 获取Job类
    private Class<? extends Job> getJobClass(String className) {
        try {
            return (Class<? extends Job>) Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("找不到Job类: " + className, e);
        }
    }
}

controller

package com.lty.controller;

import cn.hutool.core.util.IdUtil;
import com.lty.model.entity.QuartzJob;
import com.lty.service.QuartzJobService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/job")
public class QuartzJobController {

    private final QuartzJobService jobService;

    public QuartzJobController(QuartzJobService jobService) {
        this.jobService = jobService;
    }

    /**
     * 获取所有任务列表
     */
    @PostMapping("/list")
    public List<QuartzJob> listJobs() {
        return jobService.list();
    }

    /**
     * 添加任务
     */
    @PostMapping("/add")
    public String addJob(@RequestBody QuartzJob job) {
        try {
            job.setId(IdUtil.simpleUUID());
            jobService.addJob(job);
            // 保存到数据库
            jobService.save(job);
            return "任务添加成功";
        } catch (Exception e) {
            return "任务添加失败: " + e.getMessage();
        }
    }

    /**
     * 更新任务
     */
    @PostMapping("/update")
    public String updateJob(@RequestBody QuartzJob job) {
        try {
            jobService.updateJob(job);
            return "任务更新成功";
        } catch (Exception e) {
            return "任务更新失败: " + e.getMessage();
        }
    }

    /**
     * 删除任务
     */
    @PostMapping("/{jobId}/delete")
    public String deleteJob(@PathVariable String jobId) {
        try {
            jobService.deleteJob(jobId);
            return "任务删除成功";
        } catch (Exception e) {
            return "任务删除失败: " + e.getMessage();
        }
    }

    /**
     * 暂停任务
     */
    @PostMapping("/{jobId}/pause")
    public String pauseJob(@PathVariable String jobId) {
        try {
            jobService.pauseJob(jobId);
            return "任务暂停成功";
        } catch (Exception e) {
            return "任务暂停失败: " + e.getMessage();
        }
    }

    /**
     * 恢复任务
     */
    @PostMapping("/{jobId}/resume")
    public String resumeJob(@PathVariable String jobId) {
        try {
            jobService.resumeJob(jobId);
            return "任务恢复成功";
        } catch (Exception e) {
            return "任务恢复失败: " + e.getMessage();
        }
    }

    /**
     * 立即执行任务
     */
    @PostMapping("/{jobId}/runNow")
    public String runJobNow(@PathVariable String jobId) {
        try {
            jobService.runJobNow(jobId);
            return "任务已立即执行";
        } catch (Exception e) {
            return "任务立即执行失败: " + e.getMessage();
        }
    }


    /**
     * 重启定时任务
     */
    @PostMapping("/restart")
    public void restart() {
        try {
            List<QuartzJob> list = jobService.list();
            for (QuartzJob job : list) {
                if (job.getStatus() == 0) {
                    // 0-正常,重新添加任务
                    jobService.addJob(job);
                } else if (job.getStatus() == 1) {
                    // 1-暂停,暂停任务
                    jobService.pauseJob(job.getId());
                }
            }
        } catch (Exception e) {
        }
    }
}

使用测试

专门设置一个job文件夹存放各种定时任务实现

示例定时任务

package com.lty.job;

import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

/**
 * 示例定时任务
 * 该类实现了Quartz的Job接口,用于执行定时任务。
 */
@Slf4j
public class DemoJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取参数
        String params = context.getJobDetail().getJobDataMap().getString("params");
        
        log.info("执行定时任务: {}, 参数: {}", 
                context.getJobDetail().getKey().getName(), params);
        
        // 执行具体业务逻辑...
    }
}

添加任务测试参数

{
    "jobClass": "com.lty.job.DemoJob",
    "cron": "0/10 * * * * ?",
    "params": "testParam",
    "description": "每10秒执行一次的测试任务",
    "status": 0
}

Tip

项目参考地址:gitee.com/liang-tian-…

需要多个“类似”任务

方案 1:使用不同的类名

方案 2:使用同一类名 + 不同参数(推荐)

方案 3:自定义 Group 分组