Quartz结合线程池的动态管理

Quartz是一个非常强大的任务调度框架,可以启动,暂停,恢复,删除,更新,在重启时错过执行时间,可设置misfire规则,也就是设置错过了执行时机怎么办,可以跳过这次执行,等待下一个周期再执行,也可以马上重新执行错过的任务等,具体下面再讲。使用方法比schedule稍微麻烦一点,因为动态控制肯定涉及到前端的,这里不讨论前端如何实现,会从简单的一个demo,到一个具体的动态控制demo,记录一下后端如何设计与实现。

项目下载地址:quartz-demo

Quartz几个重要的接口和类

  1. Job :接口,表示一个任务,要执行的具体内容。此接口中只有一个方法, void execute(JobExecutionContext var1);我们需要自己实现execute方法,Quartz也自带一个Job的实现类:QuartzJobBean,我们也可以去继承QuartzJobBean,然后重写executeInternal这个方法。
  2. JobDetail:quartz框架自己的一个类,其实就是一个包装类,一个Wrapper,用来包装一个job的描述信息,唯一id(JobKey),还有如果这个job定时去调用程序中另一个方法需要传递参数时,可以设置JobDataMap,把参数保存在这个map里。用JobBuilder链式创建JobDetail。
// 创建JobDetail实例,并与MyJob类绑定(Job执行内容)
JobDetail jobDetail = JobBuilder.newJob(MyJob.class).withIdentity("job1", "group1").build();

  1. Trigger:接口,翻译过来就是触发器的意思,也可以翻译为扳机,Quartz最常用的两种trigger,SimpleTrigger和CronTrigger这两个接口,都是继承自Trigger,他们都是用来设置你的job何时触发,间隔多久重复触发,重复多少次,何时停止,多个trigger同时触发的优先级,错过触发了怎么处理。SimpleTrigger字面意思就是比较简单的Trigger,适用于简单的设置,CronTrigger可以配置corn表达式,更灵活,需要再控台展示和编辑的时候这个就更适合。Trigger也和JobDetail一样有一个TriggerBuilder,也是链式构造的方式构造Trigger
//构建Trigger实例,每隔1s执行一次 重复两次,一共执行三次
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
                .startNow().withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)
                        .withRepeatCount(2)).build();
  1. Scheduler:接口,英文翻译为计划,可以理解为总指挥,最终的调度都得靠它,我们一般在设置完JobDetail和Trigger的时候,就可以调用scheduler接口的实现类StdScheduler中的public Date scheduleJob(JobDetail jobDetail, Trigger trigger)这样就添加好了一条任务,当时间到了trigger中设定的时间时,任务开始执行,如果要立即执行就调用void triggerJob(JobKey jobKey, JobDataMap data);如果是暂停任务就调用void pauseJob(JobKey jobKey),如果是恢复任务,就调用void resumeJob(JobKey jobKey),如果要删除任务就调用boolean deleteJob(JobKey jobKey)如果要更新任务,就调用public Date rescheduleJob(TriggerKey triggerKey, Trigger newTrigger)
  2. jobStore: quartz框架存储内部执行逻辑的内容,有两种存储方式,1:RAMJobStore,将数据存储于内存中,重启就数据消失。2:JDBCJobSTore,也就是将数据存储于数据库中,如果web项目肯定是用这个了,一共有11张表需要新建,程序中也需要进行配置,这个直接用我下面的sql文件执行就行了,配置下面也会说。
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
//创建job
scheduler.scheduleJob(jobDetail, trigger);
//启动job
scheduler.start();
  1. JobKey与TriggerKey:先看一下他们的源码
public final class JobKey extends Key<JobKey> {
    private static final long serialVersionUID = -6073883950062574010L;

    public JobKey(String name) {
        super(name, (String)null);
    }

    public JobKey(String name, String group) {
        super(name, group);
    }

    public static JobKey jobKey(String name) {
        return new JobKey(name, (String)null);
    }

    public static JobKey jobKey(String name, String group) {
        return new JobKey(name, group);
    }
}
public final class TriggerKey extends Key<TriggerKey> {
    private static final long serialVersionUID = 8070357886703449660L;

    public TriggerKey(String name) {
        super(name, (String)null);
    }

    public TriggerKey(String name, String group) {
        super(name, group);
    }

    public static TriggerKey triggerKey(String name) {
        return new TriggerKey(name, (String)null);
    }

    public static TriggerKey triggerKey(String name, String group) {
        return new TriggerKey(name, group);
    }
}

是表明Job或者Trigger身份的一个对象,里面封装了Job的name和group,TriggerKey同理。就像上面说JobDetail链式构造的时候,要指定.withIdentity(“trigger1”, “triggerGroup1”),参数可以是两个String,也可以是一个JobKey,看一下JobBuilder和TriggerBuilder的源码片段就知道了

public JobBuilder withIdentity(String name) {
        this.key = new JobKey(name, (String)null);
        return this;
    }

    public JobBuilder withIdentity(String name, String group) {
        this.key = new JobKey(name, group);
        return this;
    }

    public JobBuilder withIdentity(JobKey jobKey) {
        this.key = jobKey;
        return this;
    }
public TriggerBuilder<T> withIdentity(String name) {
        this.key = new TriggerKey(name, (String)null);
        return this;
    }

    public TriggerBuilder<T> withIdentity(String name, String group) {
        this.key = new TriggerKey(name, group);
        return this;
    }

    public TriggerBuilder<T> withIdentity(TriggerKey triggerKey) {
        this.key = triggerKey;
        return this;
    }

demo1

main方法中使用

package com.lichong.quartz.job;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

public class PrintWordsJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date());
        System.out.println("PrintWordsJob start at:" + printTime + ", prints: Hello Job-" + new Random().nextInt(100));

    }
}
package com.lichong.quartz.job;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.concurrent.TimeUnit;

public class MyScheduler {

    public static void main(String[] args) throws SchedulerException, InterruptedException {
        // 1、创建调度器Scheduler 如果在spring环境中,可以直接注入Scheduler,@Autowired private Scheduler scheduler;    不需要SchedulerFactory创建
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容)
        JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class).withIdentity("job1", "group1").build();
        // 3、构建Trigger实例,每隔1s执行一次 重复两次,一共执行三次
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
                .startNow().withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)
                        .withRepeatCount(2)).build();

        //4、执行
        scheduler.scheduleJob(jobDetail, trigger);
        System.out.println("--------scheduler start ! ------------");
        scheduler.start();

        //睡眠
        TimeUnit.SECONDS.sleep(10);
        scheduler.shutdown();
        System.out.println("--------scheduler shutdown ! ------------");
    }
}

输出结果
--------scheduler start ! ------------
17:40:07.478 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_KaTeX parse error: Expected group after '_' at position 1796: …QuartzScheduler_̲NON_CLUSTERED shutting down.
17:40:17.480 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler
KaTeX parse error: Expected group after '_' at position 300: …QuartzScheduler_̲_NON_CLUSTERED shutdown complete.
--------scheduler shutdown ! ------------
17:40:17.493 [DefaultQuartzScheduler_Worker-4] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.493 [DefaultQuartzScheduler_Worker-7] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.493 [DefaultQuartzScheduler_Worker-5] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.493 [DefaultQuartzScheduler_Worker-8] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.497 [DefaultQuartzScheduler_Worker-6] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.497 [DefaultQuartzScheduler_Worker-9] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.497 [DefaultQuartzScheduler_Worker-10] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.524 [DefaultQuartzScheduler_Worker-3] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.524 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.
17:40:17.530 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.simpl.SimpleThreadPool - WorkerThread is shut down.

demo2

有了main方法后,如果我们要再spring中使用如何使用呢,在控台中想展示出来实现创建,暂停,恢复,删除,立即执行定时任务该如何做呢。

  1. 首先我们要创建一个springboot的工程,下面给出创建过程截图
    file->new->project
    我用的jdk8,填好项目信息下一步
    这里可以提前选择添加依赖,也可以后面在pom文件里添加,下面会给出具体的依赖信息
    在这里插入图片描述
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 任务调度 -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>

        <!-- MyBatis增强插件 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!-- freemaker模板引擎,用于定义代码生成模板 -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.28</version>
        </dependency>

        <!-- commons工具 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

    </dependencies>

等待依赖自动引入完成后,我们先新建这几个包和文件夹
在这里插入图片描述

  1. 接下来肯定是去配置数据库相关内容,我这里用的是mybatis-plus,我已经离不开它了,下面是job表的建表语句,在这里大家可以先思考一下这个表中相关的一下字段,比如bean_name,method_name,param, 为什么要建这三个字段呢,你应该看出来了这个和反射有关,如果我们在定时任务中创建了一个任务,去定时调用程序中另一个任务该如何做呢,我这里第一反应是反射去实现,所以这里我们建了这三个字段。
CREATE TABLE `tb_job` (
  `JOB_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务id',
  `BEAN_NAME` varchar(50) NOT NULL COMMENT 'spring bean名称',
  `METHOD_NAME` varchar(50) NOT NULL COMMENT '方法名',
  `PARAMS` text COMMENT '参数',
  `CRON_EXPRESSION` varchar(50) NOT NULL COMMENT 'cron表达式',
  `STATUS` char(2) NOT NULL COMMENT '任务状态  0:正常  1:暂停',
  `REMARK` varchar(50) DEFAULT NULL COMMENT '备注',
  `CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`JOB_ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='定时任务表';
  1. 建好了表后,我们需要配置一下application.yml
server:
  port: 80
  tomcat:
    uri-encoding: utf-8

spring:
  datasource:
    username: root
    password: 7546026.li
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/quartz?characterEncoding=UTF-8&autoReconnect=true&autoReconnectForPools=true&useSSL=false&serverTimezone=Asia/Shanghai

4.在启动类上加上这个注解@MapperScan(“com.lichong.quartzdemo.job.mapper”)或者在Mapper文件上加上@Mapper这个注解
在这里插入图片描述
启动一下
在这里插入图片描述
启动成功

5.接下来大家都知道需要做什么,当然是去映射实体与数据库表相关的一些内容了,使用mybatis-plus的自动生成代码,下面给出生成过后的结果,使用过mybatisplus的朋友应该知道怎么生成吧。
在这里插入图片描述
下面给出具体的代码

实体

package com.lichong.quartzdemo.job.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.Date;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 定时任务表
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class TbJob implements Serializable {

    private static final long serialVersionUID = 400066840871805700L;

    /**
     * 任务调度参数 key
     */
    public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY";

    /**
     * 调度状态枚举
     */
    public enum ScheduleStatus {
        /**
         * 正常
         */
        NORMAL("0"),
        /**
         * 暂停
         */
        PAUSE("1");

        private String value;

        ScheduleStatus(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }
    /**
     * 任务id
     */
    @TableId(value = "JOB_ID", type = IdType.AUTO)
    private Long jobId;

    /**
     * spring bean名称
     */
    @TableField("BEAN_NAME")
    private String beanName;

    /**
     * 方法名
     */
    @TableField("METHOD_NAME")
    private String methodName;

    /**
     * 参数
     */
    @TableField("PARAMS")
    private String params;

    /**
     * cron表达式
     */
    @TableField("CRON_EXPRESSION")
    private String cronExpression;

    /**
     * 任务状态  0:正常  1:暂停
     */
    @TableField("STATUS")
    private String status;

    /**
     * 备注
     */
    @TableField("REMARK")
    private String remark;

    /**
     * 创建时间
     */
    @TableField("CREATE_TIME")
    private Date createTime;


}

controller

package com.lichong.quartzdemo.job.controller;


import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.stereotype.Controller;

/**
 * <p>
 * 定时任务表 前端控制器
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
@Controller
@RequestMapping("/job")
public class JobController {

}

service

package com.lichong.quartzdemo.job.service;

import com.lichong.quartzdemo.job.entity.TbJob;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 * 定时任务表 服务类
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
public interface IJobService extends IService<TbJob> {

}

impl

package com.lichong.quartzdemo.job.service.impl;

import com.lichong.quartzdemo.job.entity.TbJob;
import com.lichong.quartzdemo.job.mapper.JobMapper;
import com.lichong.quartzdemo.job.service.IJobService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 定时任务表 服务实现类
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
@Service
public class JobServiceImpl extends ServiceImpl<JobMapper, TbJob> implements IJobService {

}

mapper

package com.lichong.quartzdemo.job.mapper;

import com.lichong.quartzdemo.job.entity.TbJob;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * <p>
 * 定时任务表 Mapper 接口
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
public interface JobMapper extends BaseMapper<TbJob> {

}

  1. 到这里需要创建quartz的框架所需要的表与配置
/*
 Navicat Premium Data Transfer

 Source Server         : localhostMysql
 Source Server Type    : MySQL
 Source Server Version : 80017
 Source Host           : localhost:3306
 Source Schema         : quartz

 Target Server Type    : MySQL
 Target Server Version : 80017
 File Encoding         : 65001

 Date: 02/08/2020 16:09:30
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for qrtz_blob_triggers
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_blob_triggers`;
CREATE TABLE `qrtz_blob_triggers` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `TRIGGER_NAME` varchar(200) NOT NULL,
  `TRIGGER_GROUP` varchar(200) NOT NULL,
  `BLOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_calendars
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_calendars`;
CREATE TABLE `qrtz_calendars` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `CALENDAR_NAME` varchar(200) NOT NULL,
  `CALENDAR` blob NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`CALENDAR_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_cron_triggers
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_cron_triggers`;
CREATE TABLE `qrtz_cron_triggers` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `TRIGGER_NAME` varchar(200) NOT NULL,
  `TRIGGER_GROUP` varchar(200) NOT NULL,
  `CRON_EXPRESSION` varchar(200) NOT NULL,
  `TIME_ZONE_ID` varchar(80) DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_fired_triggers
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_fired_triggers`;
CREATE TABLE `qrtz_fired_triggers` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `ENTRY_ID` varchar(95) NOT NULL,
  `TRIGGER_NAME` varchar(200) NOT NULL,
  `TRIGGER_GROUP` varchar(200) NOT NULL,
  `INSTANCE_NAME` varchar(200) NOT NULL,
  `FIRED_TIME` bigint(13) NOT NULL,
  `SCHED_TIME` bigint(13) NOT NULL,
  `PRIORITY` int(11) NOT NULL,
  `STATE` varchar(16) NOT NULL,
  `JOB_NAME` varchar(200) DEFAULT NULL,
  `JOB_GROUP` varchar(200) DEFAULT NULL,
  `IS_NONCONCURRENT` varchar(1) DEFAULT NULL,
  `REQUESTS_RECOVERY` varchar(1) DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_job_details
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_job_details`;
CREATE TABLE `qrtz_job_details` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `JOB_NAME` varchar(200) NOT NULL,
  `JOB_GROUP` varchar(200) NOT NULL,
  `DESCRIPTION` varchar(250) DEFAULT NULL,
  `JOB_CLASS_NAME` varchar(250) NOT NULL,
  `IS_DURABLE` varchar(1) NOT NULL,
  `IS_NONCONCURRENT` varchar(1) NOT NULL,
  `IS_UPDATE_DATA` varchar(1) NOT NULL,
  `REQUESTS_RECOVERY` varchar(1) NOT NULL,
  `JOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_locks
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_locks`;
CREATE TABLE `qrtz_locks` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `LOCK_NAME` varchar(40) NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_paused_trigger_grps
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_paused_trigger_grps`;
CREATE TABLE `qrtz_paused_trigger_grps` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `TRIGGER_GROUP` varchar(200) NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_scheduler_state
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_scheduler_state`;
CREATE TABLE `qrtz_scheduler_state` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `INSTANCE_NAME` varchar(200) NOT NULL,
  `LAST_CHECKIN_TIME` bigint(13) NOT NULL,
  `CHECKIN_INTERVAL` bigint(13) NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_simple_triggers
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_simple_triggers`;
CREATE TABLE `qrtz_simple_triggers` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `TRIGGER_NAME` varchar(200) NOT NULL,
  `TRIGGER_GROUP` varchar(200) NOT NULL,
  `REPEAT_COUNT` bigint(7) NOT NULL,
  `REPEAT_INTERVAL` bigint(12) NOT NULL,
  `TIMES_TRIGGERED` bigint(10) NOT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_simprop_triggers
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_simprop_triggers`;
CREATE TABLE `qrtz_simprop_triggers` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `TRIGGER_NAME` varchar(200) NOT NULL,
  `TRIGGER_GROUP` varchar(200) NOT NULL,
  `STR_PROP_1` varchar(512) DEFAULT NULL,
  `STR_PROP_2` varchar(512) DEFAULT NULL,
  `STR_PROP_3` varchar(512) DEFAULT NULL,
  `INT_PROP_1` int(11) DEFAULT NULL,
  `INT_PROP_2` int(11) DEFAULT NULL,
  `LONG_PROP_1` bigint(20) DEFAULT NULL,
  `LONG_PROP_2` bigint(20) DEFAULT NULL,
  `DEC_PROP_1` decimal(13,4) DEFAULT NULL,
  `DEC_PROP_2` decimal(13,4) DEFAULT NULL,
  `BOOL_PROP_1` varchar(1) DEFAULT NULL,
  `BOOL_PROP_2` varchar(1) DEFAULT NULL,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for qrtz_triggers
-- ----------------------------
DROP TABLE IF EXISTS `qrtz_triggers`;
CREATE TABLE `qrtz_triggers` (
  `SCHED_NAME` varchar(120) NOT NULL,
  `TRIGGER_NAME` varchar(200) NOT NULL,
  `TRIGGER_GROUP` varchar(200) NOT NULL,
  `JOB_NAME` varchar(200) NOT NULL,
  `JOB_GROUP` varchar(200) NOT NULL,
  `DESCRIPTION` varchar(250) DEFAULT NULL,
  `NEXT_FIRE_TIME` bigint(13) DEFAULT NULL,
  `PREV_FIRE_TIME` bigint(13) DEFAULT NULL,
  `PRIORITY` int(11) DEFAULT NULL,
  `TRIGGER_STATE` varchar(16) NOT NULL,
  `TRIGGER_TYPE` varchar(8) NOT NULL,
  `START_TIME` bigint(13) NOT NULL,
  `END_TIME` bigint(13) DEFAULT NULL,
  `CALENDAR_NAME` varchar(200) DEFAULT NULL,
  `MISFIRE_INSTR` smallint(2) DEFAULT NULL,
  `JOB_DATA` blob,
  PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`),
  KEY `SCHED_NAME` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),
  CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `qrtz_job_details` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建好表后在configure包中创建配置文件ScheduleConfig

package com.lichong.quartzdemo.job.configure;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
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 {
    @Value("${spring.datasource.driver-class-name}")
    private String driver;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${spring.datasource.url}")
    private String url;

    @Bean
    public DataSource getDataSource() {
        DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.driverClassName(driver);
        dataSourceBuilder.url(url);
        dataSourceBuilder.username(username);
        dataSourceBuilder.password(password);
        return dataSourceBuilder.build();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(getDataSource());
        // quartz参数
        Properties prop = new Properties();

        prop.put("org.quartz.scheduler.instanceName", "MyScheduler");
        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.misfireThreshold", "12000");
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
        factory.setQuartzProperties(prop);

        factory.setSchedulerName("FEBS_Scheduler");
        // 延时启动
        factory.setStartupDelay(1);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        // 启动时更新己存在的 Job
        factory.setOverwriteExistingJobs(true);
        // 设置自动启动,默认为 true
        factory.setAutoStartup(true);

        return factory;
    }
}

  1. 接下来是重点,在spring环境中,使用quartz定时去使用反射执行一个方法,我们应该有一个ScheduleUtils,先在service里面的方法里先注入Scheduler,每个方法中再具体去调用这个ScheduleUtils创建更新暂停等方法,每一个job都应该是有一个jobId可以在数据库查找到的,创建人物、更新任务、立即执行任务、有可能需要构建JobDetail、Trigger等信息,所以参数里需要有TbJob,也就是数据库映射的实体表,而暂停、删除、恢复任务,不需要重新构建JobDetail、Trigger,只需要jobId就能直接更新schedule。根据上面讲的Scheduler接口的方法,在这个工具类里面定义
    1. 创建定时任务 createScheduleJob(Scheduler scheduler, TbJob scheduleJob)
    2. 更新定时任务 updateScheduleJob(Scheduler scheduler, TbJob scheduleJob)
    3. 立即执行任务 run(Scheduler scheduler, TbJob scheduleJob)
    4. 暂停任务 pauseJob(Scheduler scheduler, Long jobId)
    5. 删除定时任务 deleteScheduleJob(Scheduler scheduler, Long jobId)
    6. 恢复任务 resumeJob(Scheduler scheduler, Long jobId)

具体实现:

package com.lichong.quartzdemo.job.configure;

import com.lichong.quartzdemo.job.entity.TbJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;

/**
 * 定时任务工具类
 *
 * @author lichong
 */
@Slf4j
public class ScheduleUtils {

    protected ScheduleUtils() {

    }

    private static final String JOB_NAME_PREFIX = "TASK_";

    /**
     * 获取触发器 key
     */
    private static TriggerKey getTriggerKey(Long jobId) {
        return TriggerKey.triggerKey(JOB_NAME_PREFIX + jobId);
    }

    /**
     * 获取jobKey
     */
    private static JobKey getJobKey(Long jobId) {
        return JobKey.jobKey(JOB_NAME_PREFIX + jobId);
    }

    /**
     * 获取表达式触发器
     */
    public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) {
        try {
            return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
        } catch (SchedulerException e) {
            log.error("获取Cron表达式失败", e);
        }
        return null;
    }

    /**
     * 创建定时任务
     */
    public static void createScheduleJob(Scheduler scheduler, TbJob scheduleJob) {
        try {
            // 构建job信息
            JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getJobId()))
                    .build();

            // 表达式调度构建器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
                    .withMisfireHandlingInstructionDoNothing();

            // 按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(scheduleJob.getJobId()))
                    .withSchedule(scheduleBuilder).build();

            // 放入参数,运行时的方法可以获取
            jobDetail.getJobDataMap().put(TbJob.JOB_PARAM_KEY, scheduleJob);

            scheduler.scheduleJob(jobDetail, trigger);

            // 暂停任务
            if (scheduleJob.getStatus().equals(TbJob.ScheduleStatus.PAUSE.getValue())) {
                pauseJob(scheduler, scheduleJob.getJobId());
            }
        } catch (SchedulerException e) {
            log.error("创建定时任务失败", e);
        }
    }

    /**
     * 更新定时任务
     */
    public static void updateScheduleJob(Scheduler scheduler, TbJob scheduleJob) {
        try {
            TriggerKey triggerKey = getTriggerKey(scheduleJob.getJobId());

            // 表达式调度构建器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
                    .withMisfireHandlingInstructionDoNothing();

            CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getJobId());

            if (trigger == null) {
                throw new SchedulerException("获取Cron表达式失败");
            } else {
                // 按新的 cronExpression表达式重新构建 trigger
                trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
                // 参数
                trigger.getJobDataMap().put(TbJob.JOB_PARAM_KEY, scheduleJob);
            }

            scheduler.rescheduleJob(triggerKey, trigger);

            // 暂停任务
            if (scheduleJob.getStatus().equals(TbJob.ScheduleStatus.PAUSE.getValue())) {
                pauseJob(scheduler, scheduleJob.getJobId());
            }

        } catch (SchedulerException e) {
            log.error("更新定时任务失败", e);
        }
    }

    /**
     * 立即执行任务
     */
    public static void run(Scheduler scheduler, TbJob scheduleJob) {
        try {
            // 参数
            JobDataMap dataMap = new JobDataMap();
            dataMap.put(TbJob.JOB_PARAM_KEY, scheduleJob);

            scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
        } catch (SchedulerException e) {
            log.error("执行定时任务失败", e);
        }
    }

    /**
     * 暂停任务
     */
    public static void pauseJob(Scheduler scheduler, Long jobId) {
        try {
            scheduler.pauseJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            log.error("暂停定时任务失败", e);
        }
    }

    /**
     * 恢复任务
     */
    public static void resumeJob(Scheduler scheduler, Long jobId) {
        try {
            scheduler.resumeJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            log.error("恢复定时任务失败", e);
        }
    }

    /**
     * 删除定时任务
     */
    public static void deleteScheduleJob(Scheduler scheduler, Long jobId) {
        try {
            scheduler.deleteJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            log.error("删除定时任务失败", e);
        }
    }
}

TbJob这个实体类是用来传递数据和映射数据库,ScheduleJob是用来真正实现Job接口中execute方法的,下面是ScheduleJob的代码

package com.lichong.quartzdemo.job.configure;

import com.lichong.quartzdemo.job.entity.TbJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * 定时任务
 *
 * @author lichong
 */
@Slf4j
public class ScheduleJob implements Job {

    private ExecutorService service = Executors.newSingleThreadExecutor();

    @Override
    public void execute(JobExecutionContext context) {

        TbJob scheduleJob = (TbJob) context.getJobDetail().getJobDataMap().get(TbJob.JOB_PARAM_KEY);

        long startTime = System.currentTimeMillis();

        try {
            // 执行任务
            log.info("任务准备执行,任务ID:{}", scheduleJob.getJobId());
            ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(),
                    scheduleJob.getParams());
            Future<?> future = service.submit(task);
            future.get();
            long times = System.currentTimeMillis() - startTime;
            log.info("任务执行完毕,任务ID:{} 总共耗时:{} 毫秒", scheduleJob.getJobId(), times);
        } catch (Exception e) {
            log.error("任务执行失败,任务ID:" + scheduleJob.getJobId(), e);
        }
    }
}

JobExecutionContext是quartz框架的上下文,我们创建一个单线程的线程池去执行任务,所以需要写一个ScheduleRunnable实现Runnable,在ScheduleRunnable中重写run方法去调用反射去invoke具体的Object中的method。上面代码中熟悉线程池的朋友都知道线程池的submit方法返回的是一个Future<?>,Future用在线程又返回结果的地方,future.get();获得计算结果或者异常,表示执行完成。

package com.lichong.quartzdemo.job.configure;

import com.lichong.quartzdemo.job.util.SpringContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;

/**
 * 执行定时任务
 *
 * @author lichong
 */
@Slf4j
public class ScheduleRunnable implements Runnable {


    private Object target;
    private Method method;
    private String params;

    ScheduleRunnable(String beanName, String methodName, String params) throws NoSuchMethodException, SecurityException {
        this.target = SpringContextUtil.getBean(beanName);
        this.params = params;

        if (StringUtils.isNotBlank(params)) {
            this.method = target.getClass().getDeclaredMethod(methodName, String.class);
        } else {
            this.method = target.getClass().getDeclaredMethod(methodName);
        }
    }

    @Override
    public void fafa() {
        try {
            ReflectionUtils.makeAccessible(method);
            if (StringUtils.isNotBlank(params)) {
                method.invoke(target, params);
            } else {
                method.invoke(target);
            }
        } catch (Exception e) {
            log.error("执行定时任务失败", e);
        }
    }

}

package com.lichong.quartzdemo.job.util;

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

/**
 * Spring Context 工具类
 * 
 * @author lichong
 *
 */
@Component
public class SpringContextUtil implements ApplicationContextAware {
	private static ApplicationContext applicationContext;

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

	public static Object getBean(String name) {
		return applicationContext.getBean(name);
	}

}
  1. 到这里就基础的内容完成,接下来就是增删改查了,现在回过头去补充controller中的增删改查内容,首先定义一个@RestController的返回通用返回实体Response
package com.lichong.quartzdemo.job.entity;

import org.springframework.http.HttpStatus;

import java.util.HashMap;

public class Response extends HashMap<String, Object> {


    private static final long serialVersionUID = -8713837118340960775L;

    public Response code(HttpStatus status) {
        this.put("code", status.value());
        return this;
    }

    public Response code(String status) {
        this.put("code", status);
        return this;
    }

    public Response message(String message) {
        this.put("message", message);
        return this;
    }

    public Response data(Object data) {
        this.put("data", data);
        return this;
    }

    public Response success() {
        this.code(HttpStatus.OK);
        return this;
    }

    public Response fail() {
        this.code(HttpStatus.INTERNAL_SERVER_ERROR);
        return this;
    }
}

补充Controller中的Restful 方法

package com.lichong.quartzdemo.job.controller;


import com.lichong.quartzdemo.job.entity.Response;
import com.lichong.quartzdemo.job.entity.TbJob;
import com.lichong.quartzdemo.job.service.IJobService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 * 定时任务表 前端控制器
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
@RestController
@RequestMapping("/job")
public class JobController {
    @Autowired
    private IJobService jobService;

    @PostMapping
    public Response addJob(TbJob job) {
        this.jobService.createJob(job);
        return new Response().success();
    }

    @GetMapping("delete/{jobId}")
    public Response deleteJob(@PathVariable String jobId) {
        this.jobService.deleteJobs(jobId);
        return new Response().success();
    }

    @PostMapping("update")
    public Response updateJob(TbJob job) {
        this.jobService.updateJob(job);
        return new Response().success();
    }

	/**
     * 立即执行一次任务
     */
    @GetMapping("run/{jobId}")
    public Response runJob(@PathVariable String jobId) {
        this.jobService.run(jobId);
        return new Response().success();
    }

    @GetMapping("pause/{jobId}")
    public Response pauseJob(@PathVariable String jobId) {
        this.jobService.pause(jobId);
        return new Response().success();
    }

    @GetMapping("resume/{jobId}")
    public Response resumeJob(@PathVariable String jobId) {
        this.jobService.resume(jobId);
        return new Response().success();
    }
}



补充service

package com.lichong.quartzdemo.job.service;

import com.lichong.quartzdemo.job.entity.TbJob;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 * 定时任务表 服务类
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
public interface IJobService extends IService<TbJob> {

    void createJob(TbJob job);

    void deleteJobs(String jobId);

    void updateJob(TbJob job);

    void run(String jobId);

    void pause(String jobId);

    void resume(String jobId);

}


补充impl,在impl里面调用ScheduleUtils中的那些方法,需要注入Scheduler,并且还需要注意一点,如果我们手动改了数据库tb_job表中的数据,那我们也希望启动的时候能更新,所以加入一个@PostConstruct,来更新scheduler

package com.lichong.quartzdemo.job.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lichong.quartzdemo.job.configure.ScheduleUtils;
import com.lichong.quartzdemo.job.entity.TbJob;
import com.lichong.quartzdemo.job.mapper.JobMapper;
import com.lichong.quartzdemo.job.service.IJobService;
import org.quartz.CronTrigger;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.List;

/**
 * <p>
 * 定时任务表 服务实现类
 * </p>
 *
 * @author lichong
 * @since 2020-07-30
 */
@Service
public class JobServiceImpl extends ServiceImpl<JobMapper, TbJob> implements IJobService {

    @Autowired
    private Scheduler scheduler;

    /**
     * 项目启动时,初始化定时器
     */
    @PostConstruct
    public void init() {
        List<TbJob> scheduleJobList = this.list();
        // 如果不存在,则创建
        scheduleJobList.forEach(scheduleJob -> {
            CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJob.getJobId());
            if (cronTrigger == null) {
                ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
            } else {
                ScheduleUtils.updateScheduleJob(scheduler, scheduleJob);
            }
        });
    }
    @Override
    public void createJob(TbJob job) {
        job.setCreateTime(new Date());
        this.save(job);
        ScheduleUtils.createScheduleJob(scheduler, job);
    }

    @Override
    public void deleteJobs(String jobId) {
        ScheduleUtils.deleteScheduleJob(scheduler, Long.valueOf(jobId));
        this.baseMapper.deleteById(jobId);
    }

    @Override
    public void updateJob(TbJob job) {
        ScheduleUtils.updateScheduleJob(scheduler, job);
        this.baseMapper.updateById(job);
    }

    @Override
    public void run(String jobId) {
       ScheduleUtils.run(scheduler, this.getById(jobId));
    }

    @Override
    public void pause(String jobId) {
        ScheduleUtils.pauseJob(scheduler, Long.valueOf(jobId));
        TbJob job = new TbJob();
        job.setJobId(Long.valueOf(jobId));
        job.setStatus(TbJob.ScheduleStatus.PAUSE.getValue());
        this.baseMapper.updateById(job);
    }

    @Override
    public void resume(String jobId) {
        ScheduleUtils.resumeJob(scheduler, Long.valueOf(jobId));
        TbJob job = new TbJob();
        job.setJobId(Long.valueOf(jobId));
        job.setStatus(TbJob.ScheduleStatus.NORMAL.getValue());
        this.baseMapper.updateById(job);
    }
}


最后,我们在写一个测试用的最终被定时任务调用的类,新建一个task包,在这个包里面新建一个TestTask.class

package com.lichong.quartzdemo.job.task;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class TestTask {
    public void testByParam(String params) {
        log.info("我是带参数的test方法,正在被执行,参数为:{}" , params);
    }
    public void testNoParam() {
        log.info("我是不带参数的test1方法,正在被执行");
    }
}

到此所有代码就写完了,下面用postman进行一下测试

1.添加一个定时任务执行TestTask类中的testNoParam方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.测试添加一条定时任务执行TestTask类中testByParam方法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3.测试删除任务,就是简单的数据库删除
在这里插入图片描述
剩下的几个就不展示了,我测试过没问题,到此全篇结束。。。。感谢大家阅读。

写在最后

这是我第一篇博客,前前后后写了好几天才写完,有很多地方可能没说的很清楚,请大家见谅,现在回过头看,篇幅有点太长了,但我觉得写博客是一种回顾所学知识很好的一种方式,以后也会坚持下去。

项目下载地址:quartz-demo


版权声明:本文为lichongsky1原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。