SpringBoot整合Quartz实现动态定时任务

SpringBoot整合Quartz实现动态定时任务

简介:

虽然Spring Boot融合了基于注解的定时任务,但是任务的定时器需在代码中配置,代码入侵较强,例如:若需改动某一定时任务的执行周期,则需改动代码再发版,且无法实现具体任务的暂停与启动。

Quartz可实现定时任务配置的持久化,将相关定时任务信息(对应任务类名、任务当前状态、cron表达式)持久化到数据库,从而在页面实现对具体任务的暂停、启动、执行周期的修改等操作而不需要改动代码。

环境:

Java 8、Spring Boot 1.5.13、MyBatis、Quartz 2.3.0

Sql脚本:

1.Quartz框架必须表

-- in your Quartz properties file, you'll need to set org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-- 你需要在你的quartz.properties文件中设置org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-- StdJDBCDelegate说明支持集群,所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务
-- This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM
-- 这是来自quartz的脚本,在MySQL数据库中创建以下的表,修改为使用INNODB而不是MYISAM
-- 你需要在数据库中执行以下的sql脚本
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;

-- 存储每一个已配置的Job的详细信息
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) 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 NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

-- 存储已配置的Trigger的信息
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) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

-- 存储已配置的Simple Trigger的信息
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),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

-- 存储Cron Trigger,包括Cron表达式和时区信息
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(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

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) NULL,
    STR_PROP_2 VARCHAR(512) NULL,
    STR_PROP_3 VARCHAR(512) NULL,
    INT_PROP_1 INT NULL,
    INT_PROP_2 INT NULL,
    LONG_PROP_1 BIGINT NULL,
    LONG_PROP_2 BIGINT NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR(1) NULL,
    BOOL_PROP_2 VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

-- Trigger作为Blob类型存储(用于Quartz用户用JDBC创建他们自己定制的Trigger类型,JobStore并不知道如何存储实例的时候)
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 NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

-- 以Blob类型存储Quartz的Calendar日历信息,quartz可配置一个日历来指定一个时间范围
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;

-- 存储已暂停的Trigger组的信息
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;

-- 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
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 INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID))
ENGINE=InnoDB;

-- 存储少量的有关 Scheduler的状态信息,和别的 Scheduler 实例(假如是用于一个集群中)
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;
-- 存储程序的非观锁的信息(假如使用了悲观锁)
CREATE TABLE QRTZ_LOCKS (
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME))
ENGINE=InnoDB;

CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);

CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);

CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);

commit;

2.新建任务实体表,通过该表控制具体的任务执行

CREATE TABLE `qrtz_job_entity` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `job_class_name` varchar(100) NOT NULL COMMENT '任务对应类名',
  `job_group` varchar(20) NOT NULL COMMENT '任务组名',
  `cron` varchar(50) NOT NULL COMMENT 'cron表达式',
  `description` varchar(50) NOT NULL COMMENT '任务描述',
  `vm_param` varchar(100) DEFAULT NULL COMMENT '参数(暂时不用)',
  `status` varchar(10) NOT NULL COMMENT '任务状态:OPEN/CLOSE',
  `delete_flag` int(1) NOT NULL DEFAULT '0' COMMENT '删除标记',
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
具体代码:

1.加入Quartz maven依赖

<dependency>
   <groupId>org.quartz-scheduler</groupId>
   <artifactId>quartz</artifactId>
   <version>2.3.0</version>
</dependency>

2.配置文件quartz.properties

# 固定前缀org.quartz
# 主要分为scheduler、threadPool、jobStore、plugin等部分

org.quartz.scheduler.instanceName=DefaultQuartzScheduler
org.quartz.scheduler.rmi.export=false
org.quartz.scheduler.rmi.proxy=false
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# threadCount和threadPriority将以setter的形式注入ThreadPool实例
# 并发个数
org.quartz.threadPool.threadCount=5

org.quartz.spi.JobFactory

# 优先级
org.quartz.threadPool.threadPriority=5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
org.quartz.jobStore.misfireThreshold=5000
# 默认存储在内存中
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
# 持久化
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix=qrtz_
org.quartz.jobStore.dataSource=qzDS
org.quartz.dataSource.qzDS.connectionProvider.class=com.zhl.common.conf.DruidConnectionProvider
org.quartz.dataSource.qzDS.driver=com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL=jdbc:mysql://localhost:3306/schedule?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
org.quartz.dataSource.qzDS.user=root
org.quartz.dataSource.qzDS.password=123456
org.quartz.dataSource.qzDS.maxConnection=10

3.Job对象的实例化过程是在Quartz中进行,所以是无法在Job Bean中注入由Spring管理的Bean的,在这里我们通过以下两个类实现将Job Bean自动装配到Spring

package com.zhl.common.conf;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @author hailunZhao
 * @date 2020/7/16 15:50
 */
@Component("myAdaptableJobFactory")
public class MyAdaptableJobFactory extends AdaptableJobFactory {

    /**
     * AutowireCapableBeanFactory 可以将一个对象添加到SpringIOC容器中,并且完成该对象注入
     */
    @Resource
    private AutowireCapableBeanFactory autowireCapableBeanFactory;

    /**
     * 该方法需要将实例化的任务对象手动的添加到SpringIOC容器中并且完成对象的注入
     */
    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        Object object = super.createJobInstance(bundle);
        //将object对象添加到SpringIOC容器中,并完成注入
        this.autowireCapableBeanFactory.autowireBean(object);
        return object;
    }

}
package com.zhl.common.conf;

import com.zhl.common.constant.Base;
import com.zhl.common.utils.PropertyUtil;
import org.quartz.Scheduler;
import org.quartz.ee.servlet.QuartzInitializerListener;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Properties;

/**
 * @author hailunZhao
 * @date 2020/6/12 9:43
 */
@Component
public class QuartzFactoryBean {

    @Resource
    private MyAdaptableJobFactory myAdaptableJobFactory;

    @Bean(name = "schedulerFactoryBean")
    public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("quartzProperties") Properties properties) {
        SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
        factoryBean.setQuartzProperties(properties);
        factoryBean.setJobFactory(myAdaptableJobFactory);
        return factoryBean;
    }

    @Bean(name = "quartzProperties")
    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource(PropertyUtil.getEnvironment() + Base.Properties.QUARTZ));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }

    @Bean
    public QuartzInitializerListener quartzInitializerListener() {
        return new QuartzInitializerListener();
    }

    @Bean
    public Scheduler scheduler(SchedulerFactoryBean schedulerFactoryBean) {
        return schedulerFactoryBean.getScheduler();
    }

}

4.job_entity对应实体

package com.zhl.model.entry;

import lombok.Data;

import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

/**
 * @author hailunZhao
 * @date 2020/7/7 19:21
 */
@Table(name = "qrtz_job_entity")
@Data
public class JobEntity implements Serializable {

    @Id
    private Integer id;

    /**
     * Des:执行job的类名
     */
    private String jobClassName;

    /**
     * Des:job组名
     */
    private String jobGroup;

    /**
     * Des:执行的cron
     */
    private String cron;

    /**
     * Des:job描述信息
     */
    private String description;

    /**
     * Des:vm参数
     */
    private String vmParam;

    /**
     * Des:job的执行状态,这里我设置为OPEN/CLOSE且只有该值为OPEN才会执行该Job
     */
    private String status;

    private int deleteFlag;

    private String createTime;

    private String updateTime;
}

5.Job工具类

package com.zhl.common.utils;

import com.zhl.model.entry.JobEntity;
import org.quartz.*;

/**
 * Job工具类
 *
 * @author hailunZhao
 * @date 2020/6/12 13:56
 */
public class JobUtil {

    /**
     * 获取指定字符串的class
     * 因为定时任务中要求的是一个泛型是job类型的class,只有job类型的对象才会有这个class,所以先实例化对象,强转成job,然后再重新获取class
     *
     * @param jobClassName  jobClassName
     * @return
     */
    public static Class<? extends Job> getClass(String jobClassName) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class<?> aClass = Class.forName(jobClassName);
        Class<? extends Job> aClass1 = ((Job) aClass.newInstance()).getClass();
        return aClass1;
    }

    /**
     * Des:获取JobDataMap.(Job参数对象)
     *
     * @param job   job
     * @return org.quartz.JobDataMap
     * @author hailunZhao
     * @date 2020/7/10 16:17
     */
    public static JobDataMap getJobDataMap(JobEntity job) {
        JobDataMap map = new JobDataMap();
        map.put("id", job.getId());
        map.put("jobClassName", job.getJobClassName());
        map.put("jobGroup", job.getJobGroup());
        map.put("cronExpression", job.getCron());
        map.put("jobDescription", job.getDescription());
        map.put("vmParam", job.getVmParam());
        map.put("status", job.getStatus());
        return map;
    }

    /**
     * Des:获取JobDetail,JobDetail是任务的定义,而Job是任务的执行逻辑,JobDetail里会引用一个Job Class来定义
     *
     * @param job   job
     * @return org.quartz.JobDetail
     * @author hailunZhao
     * @date 2020/7/10 16:17
     */
    public static JobDetail getJobDetail(JobEntity job) throws Exception {
        return JobBuilder.newJob(JobUtil.getClass(job.getJobClassName()))
                .withIdentity(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()))
                .withDescription(job.getDescription())
                .setJobData(getJobDataMap(job))
                .storeDurably()
                .build();
    }

    /**
     * Des:获取Trigger (Job的触发器,执行规则)
     *
     * @param job   job
     * @return org.quartz.Trigger
     * @author hailunZhao
     * @date 2020/7/10 16:17
     */
    public static Trigger getTrigger(JobEntity job) {
        return TriggerBuilder.newTrigger()
                .withIdentity(job.getJobClassName(), job.getJobGroup())
                .withSchedule(CronScheduleBuilder.cronSchedule(job.getCron()))
                .build();
    }

    /**
     * Des:获取JobKey,包含Name和Group
     *
     * @param jobClassName  jobClassName
     * @param jobGroup      jobGroup
     * @return org.quartz.JobKey
     * @author hailunZhao
     * @date 2020/7/10 16:17
     */
    public static JobKey getJobKey(String jobClassName, String jobGroup) {
        return JobKey.jobKey(jobClassName, jobGroup);
    }

    /**
     * Des:TriggerKey,包含Name和Group
     *
     * @param jobClassName  jobClassName
     * @param jobGroup      jobGroup
     * @return org.quartz.TriggerKey
     * @author hailunZhao
     * @date 2020/7/10 16:17
     */
    public static TriggerKey getTriggerKey(String jobClassName, String jobGroup) {
        return TriggerKey.triggerKey(jobClassName, jobGroup);
    }

}

6.service接口与实现类

package com.zhl.service;

import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageInfo;
import com.zhl.common.result.Result;
import com.zhl.model.entry.JobEntity;

import java.util.List;

/**
 * @author hailunZhao
 * @date 2020/7/7 17:39
 */
public interface IDynamicJobService {

   /**
    * Des:任务列表(分页)
    *
    * @param page    页码
    * @param pageSize 条数/页
    * @param className    任务类名
    * @return com.github.pagehelper.PageInfo
    * @author hailunZhao
    * @date 2020/7/10 15:17
    */
   PageInfo<JSONObject> queryJob(Integer page, Integer pageSize, String className) throws Exception;

   /**
    * Des:通过Id获取Job
    *
    * @param id   任务id
    * @return com.zhl.model.entry.JobEntity
    * @author hailunZhao
    * @date 2020/6/12 11:31
    */
   JobEntity getJobEntityById(Integer id);

   /**
    * Des:从数据库中加载获取到所有Job
    *
    * @param
    * @return java.util.List
    * @author hailunZhao
    * @date 2020/6/12 11:31
    */
   List<JobEntity> loadJobs();

   /**
    * Des:添加任务
    *
    * @param jsonObject   任务
    * @return void
    * @author hailunZhao
    * @date 2020/6/12 13:41
    */
   Result addJob(JSONObject jsonObject);

   /**
    * Des:暂停任务
    *
    * @param jobId    任务id
    * @return com.zhl.common.result.Result
    * @author hailunZhao
    * @date 2020/6/12 16:20
    */
   Result pauseJob(Integer jobId);

   /**
    * Des:恢复任务
    *
    * @param jobId    任务id
    * @return com.zhl.common.result.Result
    * @author hailunZhao
    * @date 2020/6/12 16:26
    */
   Result resumeJob(Integer jobId);

   /**
    * Des:恢复任务
    *
    * @param jobId    任务id
    * @return com.zhl.common.result.Result
    * @author hailunZhao
    * @date 2020/6/12 16:26
    */
   Result deleteJob(Integer jobId);

   /**
    * Des:更新触发器时间
    *
    * @param requestJson  任务id cron表达式
    * @return com.zhl.common.result.Result
    * @author hailunZhao
    * @date 2020/6/12 16:29
    */
   Result rescheduleJob(JSONObject requestJson);

}
package com.zhl.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.zhl.common.constant.Base;
import com.zhl.common.constant.ResultCode;
import com.zhl.common.exception.BizException;
import com.zhl.common.result.Result;
import com.zhl.common.utils.Assert;
import com.zhl.common.utils.DateUtil;
import com.zhl.common.utils.JobUtil;
import com.zhl.dao.JobEntryMapper;
import com.zhl.model.entry.JobEntity;
import com.zhl.service.IDynamicJobService;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Condition;
import tk.mybatis.mapper.entity.Example;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 动态定时任务
 *
 * @author hailunZhao
 * @date 2020/6/12 11:16
 */
@Service
public class DynamicJobServiceImpl implements IDynamicJobService {

    @Resource
    private JobEntryMapper jobEntryMapper;

    @Resource
    private Scheduler scheduler;

    @Override
    public PageInfo<JSONObject> queryJob(Integer page, Integer pageSize, String className) throws Exception {
        PageHelper.startPage(page, pageSize, true);
        Condition condition = new Condition(JobEntity.class);
        Example.Criteria criteria = condition.createCriteria();
        criteria.andEqualTo("deleteFlag", 0);
        if (StringUtils.isNotEmpty(className)) {
            criteria.andLike("jobClassName", "%" + className + "%");
        }
        condition.orderBy("id").desc();
        List<JobEntity> list = jobEntryMapper.selectByCondition(condition);
        List<JSONObject> dataList = new ArrayList<>(list.size());
        for (JobEntity job : list) {
            job.setUpdateTime(DateUtil.dateToStr(DateUtil.strToDate(job.getUpdateTime(), DateUtil.FORMAT_2), DateUtil.FORMAT_4));
            JSONObject json = JSONObject.parseObject(JSON.toJSONString(job));
            TriggerKey triggerKey = JobUtil.getTriggerKey(job.getJobClassName(), job.getJobGroup());
            Trigger.TriggerState state = scheduler.getTriggerState(triggerKey);
            json.put("state", state);
            dataList.add(json);
        }
        PageInfo<JSONObject> pageInfo = new PageInfo<JSONObject>(dataList);
        return pageInfo;
    }

    @Override
    public JobEntity getJobEntityById(Integer id) {
        return jobEntryMapper.selectByPrimaryKey(id);
    }

    @Override
    public List<JobEntity> loadJobs() {
        Condition condition = new Condition(JobEntity.class);
        condition.createCriteria().andEqualTo("deleteFlag", 0);
        return jobEntryMapper.selectByCondition(condition);
    }

    @Override
    public Result addJob(JSONObject jsonObject) {
        Assert.hasAllRequired(jsonObject, new String[]{"jobClassName", "jobGroup", "cron", "description", "status"});

        JobEntity job = JSON.toJavaObject(jsonObject, JobEntity.class);
        // cron表达式校验
        if (!CronExpression.isValidExpression(job.getCron())) {
            throw new BizException(ResultCode.PARAM_IS_INVALID);
        }
        try {
            // class是否存在校验
            JobUtil.getClass(job.getJobClassName());
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error(ResultCode.PARAM_IS_INVALID);
        }
        // job是否已存在
        Condition condition = new Condition(JobEntity.class);
        condition.createCriteria().andEqualTo("deleteFlag", 0)
                .andEqualTo("jobClassName", job.getJobClassName());
        int existCount = jobEntryMapper.selectCountByCondition(condition);
        if (existCount>0) {
            throw new BizException(ResultCode.DATA_ALREADY_EXISTED);
        }
        job.setId(null);
        job.setCreateTime(DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2));
        job.setUpdateTime(DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2));
        jobEntryMapper.insert(job);
        // 开启定时器
        if (Base.Job.OPEN.equals(job.getStatus())) {
            try {
                JobDetail jobDetail = JobUtil.getJobDetail(job);
                Trigger trigger = JobUtil.getTrigger(job);
                scheduler.scheduleJob(jobDetail, trigger);
                scheduler.start();
            } catch (Exception e) {
                e.printStackTrace();
                return Result.error(ResultCode.ERROR);
            }
        }
        return Result.success();
    }

    @Override
    public Result pauseJob(Integer jobId) {
        JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
        Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
        try {
            scheduler.pauseJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
        } catch (SchedulerException e) {
            e.printStackTrace();
            return Result.error(ResultCode.ERROR);
        }
        return Result.success();
    }

    @Override
    public Result resumeJob(Integer jobId) {
        JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
        Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
        try {
            scheduler.resumeJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
        } catch (SchedulerException e) {
            e.printStackTrace();
            return Result.error(ResultCode.ERROR);
        }
        return Result.success();
    }

    @Override
    public Result deleteJob(Integer jobId) {
        JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
        Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
        if (Base.Job.OPEN.equals(job.getStatus())) {
            try {
                //先暂停任务
                scheduler.pauseTrigger(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
                //停止任务
                scheduler.unscheduleJob(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
                //删除任务
                scheduler.deleteJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
            } catch (SchedulerException e) {
                e.printStackTrace();
                return Result.error(ResultCode.ERROR);
            }
        }
        job.setStatus(Base.Job.CLOSE);
        job.setDeleteFlag(1);
        jobEntryMapper.updateByPrimaryKey(job);
        return Result.success();
    }

    @Override
    public Result rescheduleJob(JSONObject requestJson) {
        Assert.hasAllRequired(requestJson, new String[]{"id", "cron", "status"});
        Integer jobId = requestJson.getInteger("id");
        String cronExpression = requestJson.getString("cron");
        String status = requestJson.getString("status");
        JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
        Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
        // cron表达式校验
        if (!CronExpression.isValidExpression(cronExpression)) {
            throw new BizException(ResultCode.PARAM_IS_INVALID);
        }
        try {
            // OPEN->OPEN
            if (Base.Job.OPEN.equals(job.getStatus()) && Base.Job.OPEN.equals(status)) {
                //先找到之前的触发器
                TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup());
                //根据表达式获取新的时间规则
                CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
                //获取原始的触发器
                CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
                //新的触发器
                trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
                scheduler.rescheduleJob(triggerKey, trigger);
            }
            // OPEN->CLOSE
            if (Base.Job.OPEN.equals(job.getStatus()) && Base.Job.CLOSE.equals(status)) {
                //先暂停任务
                scheduler.pauseTrigger(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
                //停止任务
                scheduler.unscheduleJob(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
                //删除任务
                scheduler.deleteJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
            }
            // CLOSE->OPEN
            if (Base.Job.CLOSE.equals(job.getStatus()) && Base.Job.OPEN.equals(status)) {
                JobDetail jobDetail = JobUtil.getJobDetail(job);
                Trigger trigger = JobUtil.getTrigger(job);
                scheduler.scheduleJob(jobDetail, trigger);
                scheduler.start();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error(ResultCode.ERROR);
        }
        job.setCron(cronExpression);
        job.setStatus(status);
        jobEntryMapper.updateByPrimaryKey(job);
        return Result.success();
    }

}

7.Rest接口

package com.zhl.controller.quartz;

import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageInfo;
import com.zhl.common.constant.ResultCode;
import com.zhl.common.result.Result;
import com.zhl.service.IDynamicJobService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @author hailunZhao
 * @date 2020/6/12 9:45
 */
@Slf4j
@RestController
@RequestMapping("/job")
public class QuartzController {

    @Resource
    private IDynamicJobService jobService;

    /**
     * Des:任务列表
     *
     * @param pageNum       页码
     * @param pageRow       条数
     * @param className     任务类名
     * @return com.zhl.common.result.Result
     * @author hailunZhao
     * @date 2020/7/9 11:19
     */
    @GetMapping("/query_job")
    public Result queryJob(@RequestParam(name = "pageNum", defaultValue = "1") int pageNum,
                           @RequestParam(name = "pageRow", defaultValue = "15") int pageRow,
                           @RequestParam(name = "className", defaultValue = "") String className) {
        try {
            PageInfo<JSONObject> pageInfo = jobService.queryJob(pageNum, pageRow, className);
            return Result.success(pageInfo);
        }catch (Exception e) {
            log.error(e.getMessage());
            return Result.error(ResultCode.ERROR);
        }
    }

    /**
     * Des:新加任务
     *
     * @param requestJson   job对象
     * @return com.zhl.common.result.Result
     * @author hailunZhao
     * @date 2020/7/9 11:19
     */
    @PostMapping("/add_job")
    public Result addJob(@RequestBody JSONObject requestJson) {
        return jobService.addJob(requestJson);
    }

    /**
     * Des:暂停任务
     *
     * @param id     jobId
     * @return com.zhl.common.result.Result
     * @author hailunZhao
     * @date 2020/7/9 11:20
     */
    @GetMapping("/pause_job")
    public Result pauseJob(Integer id) {
        return jobService.pauseJob(id);
    }

    /**
     * Des:恢复任务
     *
     * @param id     jobId
     * @return com.zhl.common.result.Result
     * @author hailunZhao
     * @date 2020/7/9 11:20
     */
    @GetMapping("/resume_job")
    public Result resumeJob(Integer id) {
        return jobService.resumeJob(id);
    }

    /**
     * Des:删除任务
     *
     * @param id     jobId
     * @return com.zhl.common.result.Result
     * @author hailunZhao
     * @date 2020/7/9 11:20
     */
    @GetMapping("/delete_job")
    public Result deleteJob(Integer id) {
        return jobService.deleteJob(id);
    }

    /**
     * Des:更新触发器的时间
     *
     * @param requestJson     jobId cronExpression
     * @return com.zhl.common.result.Result
     * @author hailunZhao
     * @date 2020/7/9 11:20
     */
    @PostMapping("/reschedule_job")
    public Result rescheduleJob(@RequestBody JSONObject requestJson) {
        return jobService.rescheduleJob(requestJson);
    }

}

8.新建两个定时任务并加入注解@DisallowConcurrentExecution,该注解的作用是禁止同一任务并发执行,不同任务之间的并发执行是不影响的

package com.zhl.task;

import com.zhl.common.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.util.Date;

/**
 * @author hailunZhao
 * @date 2020/6/12 9:51
 */
@Slf4j
@DisallowConcurrentExecution
public class HelloJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("hello job执行了, 时间是: {}, 线程是: {}", DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2), Thread.currentThread().getName());
    }

}
package com.zhl.task;

import com.zhl.common.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.util.Date;

/**
 * @author hailunZhao
 * @date 2020/6/12 9:51
 */
@Slf4j
@DisallowConcurrentExecution
public class TaskJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("task job执行了, 时间是: {}, 线程是: {}", DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2), Thread.currentThread().getName());
    }

}

9.将以上两个任务的信息加入表qrtz_job_entity

INSERT INTO `qrtz_job_entity` (`job_class_name`, `job_group`, `cron`, `description`, `vm_param`, `status`, `delete_flag`, `create_time`, `update_time`) VALUES ('com.zhl.task.HelloJob', 'helloworld', '0/2 * * * * ? ', '第一个', '', 'CLOSE', '0', '2020-07-09 17:10:56', '2020-07-10 17:23:42');
INSERT INTO `qrtz_job_entity` (`job_class_name`, `job_group`, `cron`, `description`, `vm_param`, `status`, `delete_flag`, `create_time`, `update_time`) VALUES ('com.zhl.task.TaskJob', 'helloworld', '0/5 * * * * ? ', 'test', NULL, 'CLOSE', '0', '2020-07-09 17:10:56', '2020-07-09 17:11:09');

10.项目启动初始化已有任务

package com.zhl.common.conf;

import com.zhl.common.utils.JobUtil;
import com.zhl.model.entry.JobEntity;
import com.zhl.service.IDynamicJobService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Set;

/**
 * Des:项目启动执行
 *
 * @author hailunZhao
 * @date 2020/7/14 16:52
 */
@Slf4j
@Component
@Order(2)
public class StartRunner implements CommandLineRunner {

    @Resource
    private Scheduler scheduler;

    @Resource
    private IDynamicJobService jobService;

    @Override
    public void run(String... arg0) throws Exception {
        log.info("程序启动...");
        reStartAllJobs();
    }

    /**
     * 重新启动所有的job
     */
    private void reStartAllJobs() throws Exception {
        // 只允许一个线程进入操作
        synchronized (log) {
            Set<JobKey> set = scheduler.getJobKeys(GroupMatcher.anyGroup());
            // 暂停所有JOB
            scheduler.pauseJobs(GroupMatcher.anyGroup());
            // 删除从数据库中注册的所有JOB
            for (JobKey jobKey : set) {
                scheduler.unscheduleJob(TriggerKey.triggerKey(jobKey.getName(), jobKey.getGroup()));
                scheduler.deleteJob(jobKey);
            }
            // 从数据库中注册的所有JOB
            for (JobEntity job : jobService.loadJobs()) {
                log.info("Job register name : {} , group : {} , cron : {}, status : {}",
                  job.getJobClassName(), job.getJobGroup(), job.getCron(), job.getStatus());
                JobDetail jobDetail = JobUtil.getJobDetail(job);
                if (job.getStatus().equals("OPEN")) {
                    scheduler.scheduleJob(jobDetail, JobUtil.getTrigger(job));
                } else {
                    log.info("Job jump name : {} , Because {} status is {}", job.getJobClassName(), job.getJobClassName(), job.getStatus());
                }
            }
        }
    }

}

11.前端页面设计如下,按钮调用相应接口即可实现任务的修改、暂停、恢复
SpringBoot整合Quartz实现动态定时任务_第1张图片
SpringBoot整合Quartz实现动态定时任务_第2张图片

总结:

Quartz与Spring的结合很好的实现了对定时任务的动态灵活配置,与Spring Boot自带的定时任务相比更加灵活,可控性更强,但也有不足之处,因为Job信息是持久化到数据库,当任务数量众多时势必在性能上会稍逊半分。

你可能感兴趣的:(java,quartz,spring,boot,数据库,mysql)