SpringBoot2.x集成Quartz实现定时任务管理(持久化到数据库)

1. Quartz简介

  Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目。
  Quartz是一个完全由Java编写的开源作业调度框架,为在Java应用程序中进行作业调度提供了简单却强大的机制。
  Quartz可以与J2EE与J2SE应用程序相结合也可以单独使用。
  Quartz允许程序开发人员根据时间的间隔来调度作业。
  Quartz实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。
  Quartz官网:http://www.quartz-scheduler.org/

2. Quartz核心概念

  • Job
      Job表示一个工作,要执行的具体内容。
  • JobDetail
      JobDetail表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail还包含了这个任务调度的方案和策略。
  • Trigger
      Trigger代表一个调度参数的配置,什么时候去调。
  • Scheduler
      Scheduler代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了。

3. 初始化数据库

  Quartz采用持久化到数据库方式,需要创建官网提供的11张表。因此,可以在官网下载对应的版本,根据路径src\org\quartz\impl\jdbcjobstore找到对应数据库类型的脚本,例如Mysql为:tables_mysql.sql
  Mysql相关的表及系统需要的表脚本如下,请先创建数据库:quartzdemo,并初始化数据库表结构及数据。

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_qrtz_blob_triggers
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_blob_triggers`;
CREATE TABLE `t_qrtz_blob_triggers`  (
  `sched_name` varchar(120) NOT NULL,
  `trigger_name` varchar(190) NOT NULL,
  `trigger_group` varchar(190) NOT NULL,
  `blob_data` blob NULL,
  PRIMARY KEY (`sched_name`, `trigger_name`, `trigger_group`) USING BTREE,
  INDEX `sched_name`(`sched_name`, `trigger_name`, `trigger_group`) USING BTREE,
  CONSTRAINT `t_qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `t_qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT
);

-- ----------------------------
-- Records of t_qrtz_blob_triggers
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_calendars
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_calendars`;
CREATE TABLE `t_qrtz_calendars`  (
  `sched_name` varchar(120) NOT NULL,
  `calendar_name` varchar(190) NOT NULL,
  `calendar` blob NOT NULL,
  PRIMARY KEY (`sched_name`, `calendar_name`) USING BTREE
);

-- ----------------------------
-- Records of t_qrtz_calendars
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_cron_triggers
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_cron_triggers`;
CREATE TABLE `t_qrtz_cron_triggers`  (
  `sched_name` varchar(120) NOT NULL,
  `trigger_name` varchar(190) NOT NULL,
  `trigger_group` varchar(190) NOT NULL,
  `cron_expression` varchar(120) NOT NULL,
  `time_zone_id` varchar(80) NULL DEFAULT NULL,
  PRIMARY KEY (`sched_name`, `trigger_name`, `trigger_group`) USING BTREE,
  CONSTRAINT `t_qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `t_qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT
);

-- ----------------------------
-- Records of t_qrtz_cron_triggers
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_fired_triggers
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_fired_triggers`;
CREATE TABLE `t_qrtz_fired_triggers`  (
  `sched_name` varchar(120) NOT NULL,
  `entry_id` varchar(95) NOT NULL,
  `trigger_name` varchar(190) NOT NULL,
  `trigger_group` varchar(190) NOT NULL,
  `instance_name` varchar(190) NOT NULL,
  `fired_time` bigint(0) NOT NULL,
  `sched_time` bigint(0) NOT NULL,
  `priority` int(0) NOT NULL,
  `state` varchar(16) NOT NULL,
  `job_name` varchar(190) NULL DEFAULT NULL,
  `job_group` varchar(190) NULL DEFAULT NULL,
  `is_nonconcurrent` varchar(1) NULL DEFAULT NULL,
  `requests_recovery` varchar(1) NULL DEFAULT NULL,
  PRIMARY KEY (`sched_name`, `entry_id`) USING BTREE,
  INDEX `idx_qrtz_ft_trig_inst_name`(`sched_name`, `instance_name`) USING BTREE,
  INDEX `idx_qrtz_ft_inst_job_req_rcvry`(`sched_name`, `instance_name`, `requests_recovery`) USING BTREE,
  INDEX `idx_qrtz_ft_j_g`(`sched_name`, `job_name`, `job_group`) USING BTREE,
  INDEX `idx_qrtz_ft_jg`(`sched_name`, `job_group`) USING BTREE,
  INDEX `idx_qrtz_ft_t_g`(`sched_name`, `trigger_name`, `trigger_group`) USING BTREE,
  INDEX `idx_qrtz_ft_tg`(`sched_name`, `trigger_group`) USING BTREE
);

-- ----------------------------
-- Records of t_qrtz_fired_triggers
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_job_details
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_job_details`;
CREATE TABLE `t_qrtz_job_details`  (
  `sched_name` varchar(120) NOT NULL,
  `job_name` varchar(190) NOT NULL,
  `job_group` varchar(190) NOT NULL,
  `description` varchar(250) NULL 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 NULL,
  PRIMARY KEY (`sched_name`, `job_name`, `job_group`) USING BTREE,
  INDEX `idx_qrtz_j_req_recovery`(`sched_name`, `requests_recovery`) USING BTREE,
  INDEX `idx_qrtz_j_grp`(`sched_name`, `job_group`) USING BTREE
);

-- ----------------------------
-- Records of t_qrtz_job_details
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_locks
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_locks`;
CREATE TABLE `t_qrtz_locks`  (
  `sched_name` varchar(120) NOT NULL,
  `lock_name` varchar(40) NOT NULL,
  PRIMARY KEY (`sched_name`, `lock_name`) USING BTREE
);

-- ----------------------------
-- Records of t_qrtz_locks
-- ----------------------------
INSERT INTO `t_qrtz_locks` VALUES ('clusteredScheduler', 'STATE_ACCESS');
INSERT INTO `t_qrtz_locks` VALUES ('clusteredScheduler', 'TRIGGER_ACCESS');

-- ----------------------------
-- Table structure for t_qrtz_paused_trigger_grps
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_paused_trigger_grps`;
CREATE TABLE `t_qrtz_paused_trigger_grps`  (
  `sched_name` varchar(120) NOT NULL,
  `trigger_group` varchar(190) NOT NULL,
  PRIMARY KEY (`sched_name`, `trigger_group`) USING BTREE
);

-- ----------------------------
-- Records of t_qrtz_paused_trigger_grps
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_scheduler_state
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_scheduler_state`;
CREATE TABLE `t_qrtz_scheduler_state`  (
  `sched_name` varchar(120) NOT NULL,
  `instance_name` varchar(190) NOT NULL,
  `last_checkin_time` bigint(0) NOT NULL,
  `checkin_interval` bigint(0) NOT NULL,
  PRIMARY KEY (`sched_name`, `instance_name`) USING BTREE
);

-- ----------------------------
-- Records of t_qrtz_scheduler_state
-- ----------------------------
INSERT INTO `t_qrtz_scheduler_state` VALUES ('clusteredScheduler', 'C3Stones-PC', 1600918524362, 10000);

-- ----------------------------
-- Table structure for t_qrtz_simple_triggers
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_simple_triggers`;
CREATE TABLE `t_qrtz_simple_triggers`  (
  `sched_name` varchar(120) NOT NULL,
  `trigger_name` varchar(190) NOT NULL,
  `trigger_group` varchar(190) NOT NULL,
  `repeat_count` bigint(0) NOT NULL,
  `repeat_interval` bigint(0) NOT NULL,
  `times_triggered` bigint(0) NOT NULL,
  PRIMARY KEY (`sched_name`, `trigger_name`, `trigger_group`) USING BTREE,
  CONSTRAINT `t_qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `t_qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT
);

-- ----------------------------
-- Records of t_qrtz_simple_triggers
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_simprop_triggers
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_simprop_triggers`;
CREATE TABLE `t_qrtz_simprop_triggers`  (
  `sched_name` varchar(120) NOT NULL,
  `trigger_name` varchar(190) NOT NULL,
  `trigger_group` varchar(190) NOT NULL,
  `str_prop_1` varchar(512) NULL DEFAULT NULL,
  `str_prop_2` varchar(512) NULL DEFAULT NULL,
  `str_prop_3` varchar(512) NULL DEFAULT NULL,
  `int_prop_1` int(0) NULL DEFAULT NULL,
  `int_prop_2` int(0) NULL DEFAULT NULL,
  `long_prop_1` bigint(0) NULL DEFAULT NULL,
  `long_prop_2` bigint(0) NULL DEFAULT NULL,
  `dec_prop_1` decimal(13, 4) NULL DEFAULT NULL,
  `dec_prop_2` decimal(13, 4) NULL DEFAULT NULL,
  `bool_prop_1` varchar(1) NULL DEFAULT NULL,
  `bool_prop_2` varchar(1) NULL DEFAULT NULL,
  PRIMARY KEY (`sched_name`, `trigger_name`, `trigger_group`) USING BTREE,
  CONSTRAINT `t_qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `trigger_name`, `trigger_group`) REFERENCES `t_qrtz_triggers` (`sched_name`, `trigger_name`, `trigger_group`) ON DELETE RESTRICT ON UPDATE RESTRICT
);

-- ----------------------------
-- Records of t_qrtz_simprop_triggers
-- ----------------------------

-- ----------------------------
-- Table structure for t_qrtz_triggers
-- ----------------------------
DROP TABLE IF EXISTS `t_qrtz_triggers`;
CREATE TABLE `t_qrtz_triggers`  (
  `sched_name` varchar(120) NOT NULL,
  `trigger_name` varchar(190) NOT NULL,
  `trigger_group` varchar(190) NOT NULL,
  `job_name` varchar(190) NOT NULL,
  `job_group` varchar(190) NOT NULL,
  `description` varchar(250) NULL DEFAULT NULL,
  `next_fire_time` bigint(0) NULL DEFAULT NULL,
  `prev_fire_time` bigint(0) NULL DEFAULT NULL,
  `priority` int(0) NULL DEFAULT NULL,
  `trigger_state` varchar(16) NOT NULL,
  `trigger_type` varchar(8) NOT NULL,
  `start_time` bigint(0) NOT NULL,
  `end_time` bigint(0) NULL DEFAULT NULL,
  `calendar_name` varchar(190) NULL DEFAULT NULL,
  `misfire_instr` smallint(0) NULL DEFAULT NULL,
  `job_data` blob NULL,
  PRIMARY KEY (`sched_name`, `trigger_name`, `trigger_group`) USING BTREE,
  INDEX `idx_qrtz_t_j`(`sched_name`, `job_name`, `job_group`) USING BTREE,
  INDEX `idx_qrtz_t_jg`(`sched_name`, `job_group`) USING BTREE,
  INDEX `idx_qrtz_t_c`(`sched_name`, `calendar_name`) USING BTREE,
  INDEX `idx_qrtz_t_g`(`sched_name`, `trigger_group`) USING BTREE,
  INDEX `idx_qrtz_t_state`(`sched_name`, `trigger_state`) USING BTREE,
  INDEX `idx_qrtz_t_n_state`(`sched_name`, `trigger_name`, `trigger_group`, `trigger_state`) USING BTREE,
  INDEX `idx_qrtz_t_n_g_state`(`sched_name`, `trigger_group`, `trigger_state`) USING BTREE,
  INDEX `idx_qrtz_t_next_fire_time`(`sched_name`, `next_fire_time`) USING BTREE,
  INDEX `idx_qrtz_t_nft_st`(`sched_name`, `trigger_state`, `next_fire_time`) USING BTREE,
  INDEX `idx_qrtz_t_nft_misfire`(`sched_name`, `misfire_instr`, `next_fire_time`) USING BTREE,
  INDEX `idx_qrtz_t_nft_st_misfire`(`sched_name`, `misfire_instr`, `next_fire_time`, `trigger_state`) USING BTREE,
  INDEX `idx_qrtz_t_nft_st_misfire_grp`(`sched_name`, `misfire_instr`, `next_fire_time`, `trigger_group`, `trigger_state`) USING BTREE,
  CONSTRAINT `t_qrtz_triggers_ibfk_1` FOREIGN KEY (`sched_name`, `job_name`, `job_group`) REFERENCES `t_qrtz_job_details` (`sched_name`, `job_name`, `job_group`) ON DELETE RESTRICT ON UPDATE RESTRICT
);

-- ----------------------------
-- Records of t_qrtz_triggers
-- ----------------------------

-- ----------------------------
-- Table structure for t_sys_job
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_job`;
CREATE TABLE `t_sys_job`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `job_name` varchar(100) NULL DEFAULT NULL COMMENT '任务名称',
  `cron_expression` varchar(255) NULL DEFAULT NULL COMMENT 'cron表达式',
  `bean_class` varchar(255) NULL DEFAULT NULL COMMENT '任务执行类(包名+类名)',
  `status` varchar(10) NULL DEFAULT NULL COMMENT '任务状态',
  `job_group` varchar(50) NULL DEFAULT NULL COMMENT '任务分组',
  `job_data_map` varchar(1000) NULL DEFAULT NULL COMMENT '参数',
  `create_user_id` int(0) NULL DEFAULT NULL COMMENT '创建人ID',
  `create_date` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `update_user_id` int(0) NULL DEFAULT NULL COMMENT '更新人ID',
  `update_date` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `remarks` varchar(255) NULL DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`) USING BTREE
) AUTO_INCREMENT = 3 COMMENT = '定时任务';

-- ----------------------------
-- Records of t_sys_job
-- ----------------------------
INSERT INTO `t_sys_job` VALUES (1, 'TestJob', '0/5 * * * * ?', 'com.c3stones.job.biz.TestJob', 'NONE', 'default', '{\"username\":\"zhangsan\", \"age\":18}', 1, '2020-09-25 15:22:32', 1, '2020-09-25 15:22:32', '测试定时任务1');
INSERT INTO `t_sys_job` VALUES (2, 'Test2Job', '0 * * * * ?', 'com.c3stones.job.biz.Test2Job', 'NONE', 'default', '{\"username\":\"lisi\", \"age\":20}', 1, '2020-09-25 15:22:54', 1, '2020-09-25 15:22:54', '测试定时任务2');

-- ----------------------------
-- Table structure for t_sys_user
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_user`;
CREATE TABLE `t_sys_user`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `username` varchar(50) NULL DEFAULT NULL COMMENT '用户名称',
  `nickname` varchar(100) NULL DEFAULT NULL COMMENT '用户昵称',
  `password` varchar(255) NULL DEFAULT NULL COMMENT '用户密码',
  PRIMARY KEY (`id`) USING BTREE
)AUTO_INCREMENT = 3 COMMENT = '系统用户';

-- ----------------------------
-- Records of t_sys_user
-- ----------------------------
INSERT INTO `t_sys_user` VALUES (1, 'user', 'C3Stones', '$2a$10$WXEPqxjMwY6d6A0hkeBtGu.acRRWUOJmX7oLUuYMHF1VWWUm4EqOC');
INSERT INTO `t_sys_user` VALUES (2, 'system', '管理员', '$2a$10$dmO7Uk9/lo1D5d1SvCGgWuB050a0E2uuBDNITEpWFiIfCg.3UbA8y');

SET FOREIGN_KEY_CHECKS = 1;

4. 示例代码

  本文在之前博客SpringBoot + Layui +Mybatis-plus实现简单后台管理系统(内置安全过滤器)的示例项目spring-boot-layui-demo基础上增加了任务调度菜单,因此请先下载相关工程。

  • 修改pom.xml
      引入依赖spring-boot-starter-quartz即可实现SpringBoot与Quartz集成。
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.c3stones</groupId>
	<artifactId>spring-boot-quartz-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-quartz-demo</name>
	<description>Spring Boot Quartz Demo</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.8.RELEASE</version>
		<relativePath />
	</parent>

	<dependencies>
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.3.1</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.4.1</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.jsoup</groupId>
			<artifactId>jsoup</artifactId>
			<version>1.11.3</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</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>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
  • 配置文件application.yml添加quartz相关配置
server:
  port: 8080
  servlet:
    session:
      timeout: 1800s
  
spring:
  jackson:
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
  datasource:
      driverClassName: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/quartzdemo?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
      username: root
      password: 123456
  thymeleaf:
    prefix: classpath:/view/
    suffix: .html
    encoding: UTF-8
    servlet:
      content-type: text/html
    # 生产环境设置true
    cache: false
  quartz:
    properties:
      org:
        quartz:
          scheduler:
            instanceName: clusteredScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: t_qrtz_
            isClustered: false
            clusterCheckinInterval: 10000
            useProperties: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true
    job-store-type: jdbc

# Mybatis-plus配置
mybatis-plus:
   mapper-locations: classpath:mapper/*.xml
   global-config:
      db-config:
         id-type: AUTO
#   configuration:
#      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 日志配置
logging:
  config: classpath:logback-spring.xml
       
# 信息安全
security:
  web:
    excludes:
      - /login
      - /logout
      - /images/**
      - /jquery/**
      - /layui/**
  xss:
    enable: true
    excludes:
      - /login
      - /logout
      - /images/*
      - /jquery/*
      - /layui/*
  sql:
    enable: true
    excludes:
      - /images/*
      - /jquery/*
      - /layui/*
  csrf:
    enable: true
    excludes:
  • 创建调度器配置类
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

/**
 * 调度器配置类
 * 
 * @author CL
 *
 */
@Configuration
public class SchedulerConfig implements SchedulerFactoryBeanCustomizer {

	@Autowired
	private DataSource dataSource;

	@Override
	public void customize(SchedulerFactoryBean schedulerFactoryBean) {
		// 启动延时
		schedulerFactoryBean.setStartupDelay(10);
		// 自动启动任务调度
		schedulerFactoryBean.setAutoStartup(true);
		// 是否覆盖现有作业定义
		schedulerFactoryBean.setOverwriteExistingJobs(true);
		// 配置数据源
		schedulerFactoryBean.setDataSource(dataSource);
	}

}
  • 创建全局用户工具类
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.c3stones.sys.entity.User;

/**
 * 用户工具类
 * 
 * @author CL
 *
 */
public class UserUtils {

	/**
	 * 获取当前用户
	 * 
	 * @return
	 */
	public static User get() {
		return (User) getSession().getAttribute("user");
	}

	/**
	 * 获取session
	 * 
	 * @return
	 */
	public static HttpSession getSession() {
		return getRequest().getSession();
	}

	/**
	 * 获取request
	 * 
	 * @return
	 */
	public static HttpServletRequest getRequest() {
		ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
				.getRequestAttributes();
		return requestAttributes.getRequest();
	}

}
  • 创建实体
import java.io.Serializable;
import java.time.LocalDateTime;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;

/**
 * 定时任务
 * 
 * @author CL
 *
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "t_sys_job")
@EqualsAndHashCode(callSuper = false)
public class Job extends Model<Job> implements Serializable {

	private static final long serialVersionUID = 1L;

	/**
	 * ID
	 */
	@TableId(type = IdType.AUTO)
	private Integer id;

	/**
	 * 任务名称
	 */
	private String jobName;

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

	/**
	 * 任务执行类(包名+类名)
	 */
	private String beanClass;

	/**
	 * 任务状态(0-停止,1-运行)
	 */
	private String status;

	/**
	 * 任务分组
	 */
	private String jobGroup;

	/**
	 * 参数
	 */
	private String jobDataMap;

	/**
	 * 下一次执行时间
	 */
	@TableField(exist = false)
	private LocalDateTime nextfireDate;

	/**
	 * 创建人ID
	 */
	private Integer createUserId;

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

	/**
	 * 更新人ID
	 */
	private Integer updateUserId;

	/**
	 * 更新时间
	 */
	private LocalDateTime updateDate;

	/**
	 * 描述
	 */
	private String remarks;

}
  • 创建定时任务处理器
import java.text.ParseException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.quartz.CronExpression;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.c3stones.job.entity.Job;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * 定时任务管理器
 * 
 * @author CL
 *
 */
@Slf4j
@Component
public class QuartzHandler {

	@Autowired
	private Scheduler scheduler;

	/**
	 * 新增定义任务
	 * 
	 * @param job   定义任务
	 * @param clazz 任务执行类
	 * @return
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public boolean start(Job job, Class clazz) {
		boolean result = true;
		try {
			String jobName = job.getJobName();
			String jobGroup = job.getJobGroup();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
			CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
			if (null == cronTrigger) {
				// 处理参数
				Map<String, String> map = new HashMap<>(5);
				String jobDataMap = job.getJobDataMap();
				if (StrUtil.isNotBlank(jobDataMap)) {
					if (JSONUtil.isJson(jobDataMap)) {
						Map parseMap = JSONUtil.toBean(jobDataMap, Map.class);
						parseMap.forEach((k, v) -> {
							map.put(String.valueOf(k), String.valueOf(v));
						});
					}
				}
				// 启动定时任务
				JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(jobName, jobGroup)
						.setJobData(new JobDataMap(map)).build();
				cronTrigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup)
						.withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression())).build();
				scheduler.scheduleJob(jobDetail, cronTrigger);
				if (!scheduler.isShutdown()) {
					scheduler.start();
				}
			} else {
				// 重启定时任务
				cronTrigger = cronTrigger.getTriggerBuilder().withIdentity(triggerKey)
						.withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression())).build();
				scheduler.rescheduleJob(triggerKey, cronTrigger);
			}
		} catch (SchedulerException e) {
			log.info("新增定时任务异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 暂停定时任务
	 * 
	 * @param job 定时任务
	 * @return
	 */
	public boolean pasue(Job job) {
		boolean result = true;
		try {
			String jobName = job.getJobName();
			String jobGroup = job.getJobGroup();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
			Trigger trigger = scheduler.getTrigger(triggerKey);
			JobKey jobKey = trigger.getJobKey();
			scheduler.pauseJob(jobKey);
		} catch (SchedulerException e) {
			log.info("暂停定时任务异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 重启定时任务
	 * 
	 * @param job 定时任务
	 * @return
	 */
	public boolean restart(Job job) {
		boolean result = true;
		try {
			String jobName = job.getJobName();
			String jobGroup = job.getJobGroup();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
			Trigger trigger = scheduler.getTrigger(triggerKey);
			scheduler.rescheduleJob(triggerKey, trigger);
		} catch (SchedulerException e) {
			log.info("重启定时任务异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 立即执行一次
	 * 
	 * @param job 定时任务
	 * @return
	 */
	public boolean trigger(Job job) {
		boolean result = true;
		try {
			String jobName = job.getJobName();
			String jobGroup = job.getJobGroup();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
			Trigger trigger = scheduler.getTrigger(triggerKey);
			JobKey jobKey = trigger.getJobKey();
			scheduler.triggerJob(jobKey);
		} catch (SchedulerException e) {
			log.info("立即执行一次异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 修改触发时间表达式
	 * 
	 * @param job               定时任务
	 * @param newCronExpression 新的cron表达式
	 * @return
	 */
	public boolean updateCronExpression(Job job, String newCronExpression) {
		boolean result = true;
		try {
			String jobName = job.getJobName();
			String jobGroup = job.getJobGroup();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
			CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
			job.setCronExpression(newCronExpression);
			CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
			cronTrigger = cronTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder)
					.build();
			scheduler.rescheduleJob(triggerKey, cronTrigger);
		} catch (SchedulerException e) {
			log.info("修改触发时间表达式异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 删除定时任务
	 * 
	 * @param job 定时任务
	 * @return
	 */
	public boolean delete(Job job) {
		boolean result = true;
		try {
			String jobName = job.getJobName();
			String jobGroup = job.getJobGroup();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
			Trigger trigger = scheduler.getTrigger(triggerKey);
			JobKey jobKey = trigger.getJobKey();
			// 停止触发器
			scheduler.pauseTrigger(triggerKey);
			// 移除触发器
			scheduler.unscheduleJob(triggerKey);
			// 删除任务
			scheduler.deleteJob(jobKey);
		} catch (SchedulerException e) {
			log.info("删除定时任务异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/***
	 * 判断是否存在定时任务
	 * 
	 * @param job 定时任务
	 * @return
	 */
	public boolean has(Job job) {
		boolean result = true;
		try {
			if (!scheduler.isShutdown()) {
				String jobName = job.getJobName();
				String jobGroup = job.getJobGroup();
				TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
				Trigger trigger = scheduler.getTrigger(triggerKey);
				result = (trigger != null) ? true : false;
			} else {
				result = false;
			}
		} catch (SchedulerException e) {
			log.info("判断是否存在定时任务异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 获得定时任务状态
	 * 
	 * @param job 定时任务
	 * @return
	 */
	public String getStatus(Job job) {
		String status = StrUtil.EMPTY;
		try {
			String jobName = job.getJobName();
			String jobGroup = job.getJobGroup();
			TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
			TriggerState triggerState = scheduler.getTriggerState(triggerKey);
			status = triggerState.toString();
		} catch (Exception e) {
			log.info("获得定时任务状态异常:{}", e.getMessage());
		}
		return StrUtil.isNotEmpty(status) ? status : TriggerState.NONE.toString();
	}

	/**
	 * 启动调度器
	 * 
	 * @return
	 */
	public boolean startScheduler() {
		boolean result = true;
		try {
			scheduler.start();
		} catch (SchedulerException e) {
			log.info("启动调度器异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 关闭调度器
	 * 
	 * @return
	 */
	public boolean standbyScheduler() {
		boolean result = true;
		try {
			if (!scheduler.isShutdown()) {
				scheduler.standby();
			}
		} catch (SchedulerException e) {
			log.info("关闭调度器异常:{}", e.getMessage());
			result = false;
		}
		return result;
	}

	/**
	 * 判断调度器是否为开启状态
	 * 
	 * @return
	 */
	public boolean isStarted() {
		boolean result = true;
		try {
			result = scheduler.isStarted();
		} catch (SchedulerException e) {
			log.info("判断调度器是否为开启状态异常:{}", e.getMessage());
		}
		return result;
	}

	/**
	 * 判断调度器是否为关闭状态
	 * 
	 * @return
	 */
	public boolean isShutdown() {
		boolean result = true;
		try {
			result = scheduler.isShutdown();
		} catch (SchedulerException e) {
			log.info("判断调度器是否为关闭状态异常:{}", e.getMessage());
		}
		return result;
	}

	/**
	 * 判断调度器是否为待机状态
	 * 
	 * @return
	 */
	public boolean isInStandbyMode() {
		boolean result = true;
		try {
			result = scheduler.isInStandbyMode();
		} catch (SchedulerException e) {
			log.info("判断调度器是否为待机状态异常:{}", e.getMessage());
		}
		return result;
	}

	/**
	 * 获得下一次执行时间
	 * 
	 * @param cronExpression cron表达式
	 * @return
	 */
	public LocalDateTime nextfireDate(String cronExpression) {
		LocalDateTime localDateTime = null;
		try {
			if (StrUtil.isNotEmpty(cronExpression)) {
				CronExpression ce = new CronExpression(cronExpression);
				Date nextInvalidTimeAfter = ce.getNextInvalidTimeAfter(new Date());
				localDateTime = Instant.ofEpochMilli(nextInvalidTimeAfter.getTime()).atZone(ZoneId.systemDefault())
						.toLocalDateTime();
			}
		} catch (ParseException e) {
			log.info("获得下一次执行时间异常:{}", e.getMessage());
		}
		return localDateTime;
	}

}
  • 创建Mapper
import org.apache.ibatis.annotations.Mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.c3stones.job.entity.Job;

/**
 * 定时任务Mapper
 * 
 * @author CL
 *
 */
@Mapper
public interface JobMapper extends BaseMapper<Job> {

}
  • 创建Service
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.c3stones.job.entity.Job;

/**
 * 定时任务Service
 * 
 * @author CL
 *
 */
public interface JobService extends IService<Job> {

	/**
	 * 查询列表数据
	 * 
	 * @param job     系统用户
	 * @param current 当前页
	 * @param size    每页显示条数
	 * @return
	 */
	public Page<Job> listData(Job job, long current, long size);

}
  • 创建Service实现类
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.c3stones.job.config.QuartzHandler;
import com.c3stones.job.entity.Job;
import com.c3stones.job.mapper.JobMapper;
import com.c3stones.job.service.JobService;

import cn.hutool.core.util.StrUtil;

/**
 * 定时任务Service实现
 * 
 * @author CL
 *
 */
@Service
public class JobServiceImpl extends ServiceImpl<JobMapper, Job> implements JobService {

	@Autowired
	private QuartzHandler quartzHandler;

	/**
	 * 查询列表数据
	 * 
	 * @param job     系统用户
	 * @param current 当前页
	 * @param size    每页显示条数
	 * @return
	 */
	@Override
	public Page<Job> listData(Job job, long current, long size) {
		QueryWrapper<Job> queryWrapper = new QueryWrapper<>();
		if (StrUtil.isNotBlank(job.getJobName())) {
			queryWrapper.like("job_name", job.getJobName());
		}
		Page<Job> page = baseMapper.selectPage(new Page<>(current, size), queryWrapper);
		List<Job> records = page.getRecords();

		// 处理定时任务数据
		for (int i = 0; i < records.size(); i++) {
			Job j = records.get(i);
			// 获取下一次执行时间
			j.setNextfireDate(quartzHandler.nextfireDate(j.getCronExpression()));

			// 更新状态
			String status = quartzHandler.getStatus(j);
			if (!(status).equals(j.getStatus())) {
				j.setStatus(status);
				super.updateById(j);
			}

			records.set(i, j);
		}
		page.setRecords(records);
		return page;
	}
}
  • 创建Controller
import java.time.LocalDateTime;

import org.quartz.Trigger.TriggerState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.c3stones.common.vo.Response;
import com.c3stones.job.config.QuartzHandler;
import com.c3stones.job.entity.Job;
import com.c3stones.job.service.JobService;
import com.c3stones.sys.entity.User;
import com.c3stones.sys.utils.UserUtils;

/**
 * 定时任务Controller
 * 
 * @author CL
 *
 */
@Controller
@RequestMapping(value = "job")
public class JobController {

	@Autowired
	private QuartzHandler quartzHandler;

	@Autowired
	private JobService jobService;

	/**
	 * 查询列表
	 * 
	 * @return
	 */
	@RequestMapping(value = "list")
	public String list() {
		return "pages/job/jobList";
	}

	/**
	 * 查询列表数据
	 * 
	 * @param user    系统用户
	 * @param current 当前页
	 * @param size    每页显示条数
	 * @return
	 */
	@RequestMapping(value = "listData")
	@ResponseBody
	public Response<Page<Job>> listData(Job job, @RequestParam(name = "page") long current,
			@RequestParam(name = "limit") long size) {
		Page<Job> page = jobService.listData(job, current, size);
		return Response.success(page);
	}

	/**
	 * 更新
	 * 
	 * @param job 定时任务
	 * @return
	 */
	@RequestMapping(value = "update")
	@ResponseBody
	public Response<Boolean> update(Job job) {
		Assert.notNull(job.getId(), "ID不能为空");
		User user = UserUtils.get();
		if (user != null) {
			job.setUpdateUserId(user.getId());
		}
		LocalDateTime now = LocalDateTime.now();
		job.setUpdateDate(now);
		boolean result = jobService.updateById(job);
		Job queryJob = jobService.getById(job.getId());
		String status = quartzHandler.getStatus(queryJob);
		if (!(TriggerState.NONE.toString()).equals(status)) {
			result = quartzHandler.updateCronExpression(queryJob, queryJob.getCronExpression());
		}
		return Response.success("更新" + (result ? "成功" : "失败"), result);
	}

	/**
	 * 删除
	 * 
	 * @param job 定时任务
	 * @return
	 */
	@RequestMapping(value = "delete")
	@ResponseBody
	public Response<Boolean> delete(Job job) {
		Assert.notNull(job.getId(), "ID不能为空");
		Job queryJob = jobService.getById(job.getId());
		boolean result = true;
		if (!(TriggerState.NONE.toString()).equals(queryJob.getStatus())) {
			result = quartzHandler.delete(queryJob);
		}
		if (result) {
			result = jobService.removeById(job.getId());
		}
		return Response.success("删除" + (result ? "成功" : "失败"), result);
	}

	/**
	 * 启动
	 * 
	 * @param job 定时任务
	 * @return
	 * @throws ClassNotFoundException
	 */
	@RequestMapping(value = "start")
	@ResponseBody
	public Response<Boolean> start(Job job) throws ClassNotFoundException {
		Assert.notNull(job.getId(), "ID不能为空");
		Job queryJob = jobService.getById(job.getId());
		Assert.notNull(queryJob, "定时任务不存在");
		Class<?> clazz = Class.forName(queryJob.getBeanClass());
		Assert.notNull(clazz, "未找到任务执行类");
		boolean result = quartzHandler.start(queryJob, clazz);
		return Response.success("启动" + (result ? "成功" : "失败"), result);
	}

	/**
	 * 暂停
	 * 
	 * @param job 定时任务
	 * @return
	 */
	@RequestMapping(value = "pasue")
	@ResponseBody
	public Response<Boolean> pasue(Job job) {
		Assert.notNull(job.getId(), "ID不能为空");
		Job queryJob = jobService.getById(job.getId());
		Assert.notNull(queryJob, "定时任务不存在");

		String status = quartzHandler.getStatus(queryJob);
		if (!((TriggerState.NORMAL.toString()).equals(status) || (TriggerState.PAUSED.toString()).equals(status)
				|| (TriggerState.BLOCKED.toString()).equals(status))) {
			return Response.success("当前状态不可暂停", false);
		}
		if ((TriggerState.PAUSED.toString()).equals(status)) {
			return Response.success("已暂停", false);
		}

		boolean result = quartzHandler.pasue(queryJob);
		return Response.success("暂停" + (result ? "成功" : "失败"), result);
	}

	/**
	 * 立即执行
	 * 
	 * @param job 定时任务
	 * @return
	 */
	@RequestMapping(value = "trigger")
	@ResponseBody
	public Response<Boolean> trigger(Job job) {
		Assert.notNull(job.getId(), "ID不能为空");
		Job queryJob = jobService.getById(job.getId());
		Assert.notNull(queryJob, "定时任务不存在");

		String status = quartzHandler.getStatus(queryJob);
		if (!((TriggerState.NORMAL.toString()).equals(status) || (TriggerState.PAUSED.toString()).equals(status)
				|| (TriggerState.COMPLETE.toString()).equals(status))) {
			return Response.success("当前状态不可立即执行", false);
		}

		boolean result = quartzHandler.trigger(queryJob);
		return Response.success("立即执行" + (result ? "成功" : "失败"), result);
	}

	/**
	 * 判断定时器是否为待机模式
	 */
	@RequestMapping(value = "isInStandbyMode")
	@ResponseBody
	public Response<Boolean> isInStandbyMode() {
		boolean result = quartzHandler.isInStandbyMode();
		return Response.success(result);
	}

	/**
	 * 启动定时器
	 * 
	 * @return
	 */
	@RequestMapping(value = "startScheduler")
	@ResponseBody
	public Response<Boolean> startScheduler() {
		boolean result = quartzHandler.startScheduler();
		return Response.success("启动定时器" + (result ? "成功" : "失败"), result);
	}

	/**
	 * 待机定时器
	 * 
	 * @return
	 */
	@RequestMapping(value = "standbyScheduler")
	@ResponseBody
	public Response<Boolean> standbyScheduler() {
		boolean result = quartzHandler.standbyScheduler();
		return Response.success("关闭定时器" + (result ? "成功" : "失败"), result);
	}

	/**
	 * 新增
	 * 
	 * @return
	 */
	@RequestMapping(value = "add")
	public String add() {
		return "pages/job/jobAdd";
	}

	/**
	 * 保存
	 * 
	 * @return
	 */
	@RequestMapping(value = "save")
	@ResponseBody
	public Response<Boolean> save(Job job) {
		User user = UserUtils.get();
		if (user != null) {
			job.setCreateUserId(user.getId());
			job.setUpdateUserId(user.getId());
		}
		LocalDateTime now = LocalDateTime.now();
		job.setCreateDate(now);
		job.setUpdateDate(now);
		boolean result = jobService.save(job);
		return Response.success(result);
	}

}
  • 主页index.html配置菜单
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>C3Stones</title>
    <link th:href="@{/images/favicon.ico}" rel="icon">
	<link th:href="@{/layui/css/layui.css}" rel="stylesheet" />
	<link th:href="@{/layui/css/admin.css}" rel="stylesheet" />
	<script th:src="@{/layui/layui.js}"></script>
	<script th:src="@{/layui/js/index.js}" data-main="home"></script>
</head>
<body class="layui-layout-body">
    <div class="layui-layout layui-layout-admin">
        <div class="layui-header custom-header">
            <ul class="layui-nav layui-layout-left">
                <li class="layui-nav-item slide-sidebar" lay-unselect>
                    <a href="javascript:;" class="icon-font"><i class="ai ai-menufold"></i></a>
                </li>
            </ul>
            <ul class="layui-nav layui-layout-right">
                <li class="layui-nav-item">
                    <a href="javascript:;">[[${user?.nickname}]]</a>
                    <dl class="layui-nav-child">
                        <dd><a th:href="@{/logout}">退出</a></dd>
                    </dl>
                </li>
            </ul>
        </div>

        <div class="layui-side custom-admin">
            <div class="layui-side-scroll">
                <div class="custom-logo">
                    <img alt="" th:src="@{/images/logo.jpg}">
                    <h1>C3Stones</h1>
                </div>
                <ul id="Nav" class="layui-nav layui-nav-tree">
                    <li class="layui-nav-item">
                        <a href="javascript:;">
                            <i class="layui-icon">&#xe68e;</i>
                            <em>主页</em>
                        </a>
                        <dl class="layui-nav-child">
                            <dd><a th:href="@{/view}">控制台</a></dd>
                        </dl>
                    </li>
                    <li class="layui-nav-item">
                        <a href="javascript:;">
                            <i class="layui-icon">&#xe716;</i>
                            <em>系统管理</em>
                        </a>
                        <dl class="layui-nav-child">
                            <dd><a th:href="@{/user/list}">用户管理</a></dd>
                        </dl>
                        <dl class="layui-nav-child">
                            <dd><a th:href="@{/job/list}">任务调度</a></dd>
                        </dl>
                    </li>
                </ul>

            </div>
        </div>

        <div class="layui-body">
             <div class="layui-tab app-container" lay-allowClose="true" lay-filter="tabs">
                <ul id="appTabs" class="layui-tab-title custom-tab"></ul>
                <div id="appTabPage" class="layui-tab-content"></div>
            </div>
        </div>

        <div class="layui-footer">
            <p>© 2020 - C3Stones Blog : <a href="https://www.cnblogs.com/cao-lei/" target="_blank">https://www.cnblogs.com/cao-lei/</a></p>
        </div>
        <div class="mobile-mask"></div>
    </div>
</body>
</html>
  • 新增定时任务列表页面jobList.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <link th:href="@{/layui/css/layui.css}" rel="stylesheet" />
    <link th:href="@{/layui/css/view.css}" rel="stylesheet" />
    <script th:src="@{/layui/layui.all.js}"></script>
    <script th:src="@{/jquery/jquery-2.1.4.min.js}"></script>
    <script th:src="@{/layui/js/view.js}"></script>
    <title></title>
</head>
<body class="layui-view-body">
	<div class="layui-content">
	    <div class="layui-row">
			<div class="layui-card">
                <div class="layui-card-header">
                	<i class="layui-icon mr5">&#xe66f;</i>任务调度(定时器状态:<label id="schedulerStatus"></label>)
                	<button class="layui-btn layui-btn-xs layui-hide" data-type="startScheduler">启动定时器</button>
					<button class="layui-btn layui-btn-xs layui-btn-danger layui-hide" data-type="standbyScheduler">定时器待机</button>
                	<button class="layui-btn layui-btn-xs layui-btn-normal pull-right mt10" data-type="add"><i class="layui-icon mr5">&#xe654;</i>新增</button>	
                </div>
                <div class="layui-card-body">
                	<div class="searchTable">
					 任务名称:
					 <div class="layui-inline mr5">
					 	<input class="layui-input" name="jobName" autocomplete="off">
					 </div>
					 <button class="layui-btn" data-type="reload">查询</button>
					 <button class="layui-btn layui-btn-primary" data-type="reset">重置</button>
					</div>
                	<table class="layui-hide" id="jobDataTable" lay-filter="config"></table>
					<script type="text/html" id="operation">
						<a class="layui-btn layui-btn-xs " lay-event="start">启动</a>
						<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="pasue">暂停</a>
						<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="trigger">立即执行</a>
						<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
					</script>
                </div>
            </div>
        </div>
    </div>
</body>
<script>
var element = layui.element;
var table = layui.table;
var layer = layui.layer;
table.render({
	id: 'jobTable'
	,elem: '#jobDataTable'
    ,url: '[[@{/job/listData}]]'
    ,cellMinWidth: 100
   	,page: {
  		layout: ['prev', 'page', 'next', 'count', 'skip', 'limit']
  	    ,groups: 5
  	    ,first: false
  	    ,last: false
	}
    ,cols: [
    	[
	      {field:'id', title: 'ID', width: 50}
	      ,{field:'jobName', title: '任务名称', width: 120}
	      ,{field:'cronExpression', title: '周期表达式', edit: 'text', width: 100}
	      ,{field:'beanClass', title: '任务执行类', width: 250}
	      ,{field:'jobDataMap', title: '参数', width: 200}
	      ,{field:'status', title: '状态', templet: '#statusTemp', width: 80, align: 'center'}
	      ,{field:'jobGroup', title: '分组', templet: '#groupTemp', width: 60, align: 'center'}
	      ,{field:'nextfireDate', title: '下一次执行时间', width: 160, align: 'center'}
	      ,{field:'remarks', title: '描述', width: 200}
	      ,{fixed: 'right', title:'操作', align: 'center', toolbar: '#operation', width:240}
    	]
   	]
    ,response: {
        statusCode: 200
    }
    ,parseData: function(res){
    	return {
    		"code": res.code
            ,"msg": res.msg
            ,"count": res.data.total
            ,"data": res.data.records
    	};
    }
});

active = {
	add: function() {
		layer.open({
    		type: 2,
    		area: ['90%', '90%'],
    		title: '新增',
    		content: '[[@{/}]]job/add'
    	});
	},
	reload: function() {
		table.reload('jobTable', {
			page: {
				curr: 1
			}
			,where: {
				jobName : $("input[name='jobName']").val()
			}
		}, 'data');
	},
	reset: function() {
		$(".searchTable .layui-input").val("");
	},
	startScheduler: function() {
		$.ajax({
	        url : "[[@{/}]]job/startScheduler",
	        data : {},
	        type : "post",
	        dataType : "json",
	        error : function(data) {
	        	errorHandle(data);
	        },
	        success : function(data) {
	        	getSchedulerStatus();
	        	msg(data);
	        	refresh();
	        }
	    });
	},
	standbyScheduler: function() {
		$.ajax({
	        url : "[[@{/}]]job/standbyScheduler",
	        data : {},
	        type : "post",
	        dataType : "json",
	        error : function(data) {
	        	errorHandle(data);
	        },
	        success : function(data) {
	        	getSchedulerStatus();
	        	msg(data);
	        	refresh();
	        }
	    });
	}
};

// 按钮事件
$('.layui-btn').on('click', function(){
    var type = $(this).data('type');
    active[type] ? active[type].call(this) : '';
});

//监听行工具事件
table.on('tool(config)', function(obj){
	var row = obj.data;
	if (obj.event === 'start') {
		$.ajax({
	        url : "[[@{/}]]job/start",
	        data : {'id': row.id},
	        type : "post",
	        dataType : "json",
	        error : function(data) {
	        	errorHandle(data);
	        },
	        success : function(data) {
	        	msg(data);
	        	refresh();
	        }
	    });
	} if (obj.event == 'pasue') {
		$.ajax({
	        url : "[[@{/}]]job/pasue",
	        data : {'id': row.id},
	        type : "post",
	        dataType : "json",
	        error : function(data) {
	        	errorHandle(data);
	        },
	        success : function(data) {
	        	msg(data);
	        	refresh();
	        }
	    });
	} if (obj.event == 'trigger') {
		$.ajax({
	        url : "[[@{/}]]job/trigger",
	        data : {'id': row.id},
	        type : "post",
	        dataType : "json",
	        error : function(data) {
	        	errorHandle(data);
	        },
	        success : function(data) {
	        	msg(data);
	        	refresh();
	        }
	    });
	} else if(obj.event === 'del') {
		layer.confirm("确认删除吗?", {icon: 3, title:'提示'}, function(index) {
			layer.close(index);
			$.ajax({
		        url : "[[@{/}]]job/delete",
		        data : {'id': row.id},
		        type : "post",
		        dataType : "json",
		        error : function(data) {
		        	errorHandle(data);
		        },
		        success : function(data) {
		        	refresh();
		        }
		    });
		});
    }
});

table.on('edit(config)', function(obj){
    var value = obj.value;
    if (isEmpty(value)) {
    	layer.msg("不能为空", {icon: 2});
    	refresh();
    	return;
    }
    $.ajax({
        url : "[[@{/}]]job/update",
        data : {'id': obj.data.id, 'cronExpression' : value},
        type : "post",
        dataType : "json",
        error : function(data) {
        	errorHandle(data);
        },
        success : function(data) {
        	msg(data);
        	refresh();
        }
    });
});

// 获取定时器状态
$(function(){getSchedulerStatus();});
function getSchedulerStatus() {
	$.ajax({
        url : "[[@{/}]]job/isInStandbyMode",
        data : {},
        type : "post",
        dataType : "json",
        error : function(data) {
        	errorHandle(data);
        },
        success : function(data) {
        	if (!data.data) { // 启动状态
        		$("button[data-type='startScheduler']").addClass("layui-hide");
        		$("button[data-type='standbyScheduler']").removeClass("layui-hide");
        		$("#schedulerStatus").html("<span class='text-green'>启动中</span>");
        	} else { // 待机状态
        		$("button[data-type='startScheduler']").removeClass("layui-hide");
        		$("button[data-type='standbyScheduler']").addClass("layui-hide");
        		$("#schedulerStatus").html("<span class='text-orange'>待机中</span>");
        	}
        }
    });
}
</script>
<script type="text/html" id="statusTemp">
	{{#  if(d.status === 'NONE'){ }}
    	<span class="text-purple">未启动</span>
  	{{#  } else if(d.status === 'NORMAL') { }}
		<span class="text-green">正常</span>
  	{{#  } else if(d.status === 'PAUSED') { }}
		<span class="text-orange">暂停</span>
  	{{#  } else if(d.status === 'COMPLETE') { }}
		<span class="text-aqua">完成</span>
  	{{#  } else if(d.status === 'ERROR') { }}
		<span class="text-red">异常</span>
  	{{#  } else if(d.status === 'BLOCKED') { }}
		<span class="text-maroon">锁定</span>
  	{{#  } else { }}
		<span class="text-gray">未知</span>
	{{#  } }}
</script>
<script type="text/html" id="groupTemp">
	{{#  if(d.jobGroup === 'default'){ }}
    	默认
  	{{#  } else if(d.jobGroup === 'system') { }}
		系统
  	{{#  } else { }}
		未知
	{{#  } }}
</script>
</html>
  • 新增定时任务新增页面jobAdd.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <link th:href="@{/layui/css/layui.css}" rel="stylesheet" />
    <link th:href="@{/layui/css/view.css}" rel="stylesheet" />
    <script th:src="@{/layui/layui.all.js}"></script>
    <script th:src="@{/jquery/jquery-2.1.4.min.js}"></script>
    <script th:src="@{/jquery/jquery-form.js}"></script>
    <script th:src="@{/layui/js/view.js}"></script>
    <title></title>
</head>
<body class="layui-view-body">
	<div class="layui-row">
    	<div class="layui-card">
        	<form class="layui-form layui-card-body layui-form-pane" method="post" th:action="@{/job/save}">
        		<input type="hidden" name="status" value="NONE">
				<div class="layui-form-item">
					<div class="layui-inline mr0" style="width: 49.7%">
						<label class="layui-form-label"><i>*</i>任务名称</label>
						<div class="layui-input-block">
							<input type="text" name="jobName" id="jobName" maxlength="30" lay-verify="required" class="layui-input">
						</div>
					</div>
					<div class="layui-inline mr0" style="width: 49.8%">
						<label class="layui-form-label"><i>*</i>任务分组</label>
						<div class="layui-input-block">
							<select name="jobGroup">
								<option value="default">默认</option>
								<option value="system">系统</option>
							</select>
						</div>
					</div>
				</div>
				<div class="layui-form-item">
					<label class="layui-form-label">任务描述</label>
					<div class="layui-input-block">
						<input type="text" name="remarks" maxlength="50" class="layui-input">
					</div>
				</div>
				<div class="layui-form-item">
    				<label class="layui-form-label"><i>*</i>执行类</label>
					<div class="layui-input-inline width-460">
						<input type="text" name="beanClass" lay-verify="required" maxlength="200" class="layui-input">
	    			</div>
					<div class="layui-form-mid layui-word-aux">包名 + 类名,示例:com.c3stones.job.biz.TestJob</div>
  				</div>
				<div class="layui-form-item">
    				<label class="layui-form-label">参数</label>
					<div class="layui-input-inline width-460">
						<input type="text" name="jobDataMap" placeholder="JSON数据格式" maxlength="1000" autocomplete="off" class="layui-input">
	    			</div>
					<div class="layui-form-mid layui-word-aux">示例:{"username":"zhangsan", "age":18}</div>
  				</div>
				<div class="layui-form-item">
    				<label class="layui-form-label"><i>*</i>表达式</label>
					<div class="layui-input-inline width-460">
						<input type="text" name="cronExpression" placeholder="例如:0/5 * * * * ?" lay-verify="required" maxlength="200" class="layui-input">
	    			</div>
					<div class="layui-form-mid layui-word-aux"><a class="text-blue" href="https://cron.qqe2.com/" target="_blank">在线Cron表达式生成器</a></div>
  				</div>
				<div class="layui-form-item">
	                <button type="submit" class="layui-btn" lay-submit lay-filter="*">提交</button>
	                <button type="reset" class="layui-btn layui-btn-primary">重置</button>
              	</div>
			</form>
		</div>
	</div>
</body>
<script>
var form = layui.form;
var layer = layui.layer;

form.render();

// 提交表单
form.on('submit(*)', function(data){
	$(".layui-form").ajaxForm({
		error: function(data){
			errorHandle(data);
		},
		success: function(data) {
			parent.location.reload();
			var index = parent.layer.getFrameIndex(window.name);
			parent.layer.close(index);
		}
	});
});
</script>
</html>

5. 测试

  • 创建两种类型Job
    • 实现Job接口
    import java.time.LocalDateTime;
    
    import org.quartz.Job;
    import org.quartz.JobDataMap;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import com.c3stones.job.service.JobService;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 测试定时任务
     * 
     * @author CL
     *
     */
    @Slf4j
    // @DisallowConcurrentExecution //不并发执行
    public class TestJob implements Job {
    
    	@Autowired
    	private JobService jobService;
    
    	@Override
    	public void execute(JobExecutionContext context) throws JobExecutionException {
    		JobDataMap jobDataMap = context.getMergedJobDataMap();
    		log.info("定时任务1 => 定时任务定时任务数量 => {},参数值 => {},当前时间 => {}", jobService.count(),
    				"{ username=" + jobDataMap.get("username") + ", age=" + jobDataMap.get("age") + " }",
    				LocalDateTime.now());
    	}
    
    }    
    
    • 继承QuartzJobBean类
    import java.time.LocalDateTime;
    
    import org.quartz.JobDataMap;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.quartz.QuartzJobBean;
    
    import com.c3stones.job.service.JobService;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * 测试定时任务
     * 
     * @author CL
     *
     */
    @Slf4j
    // @DisallowConcurrentExecution //不并发执行
    public class Test2Job extends QuartzJobBean {
    
    	@Autowired
    	private JobService jobService;
    
    	@Override
    	protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
    		JobDataMap jobDataMap = context.getMergedJobDataMap();
    		log.info("定时任务2 => 定时任务数量 => {},参数值 => {},当前时间 => {}", jobService.count(),
    				"{ username=" + jobDataMap.get("username") + ", age=" + jobDataMap.get("age") + " }",
    				LocalDateTime.now());
    	}
    
    }
    
  • 配置定时任务
      浏览器访问:http://127.0.0.1:8080/login,填写用户信息user/123456登录系统,点击菜单:系统管理>任务调度,通过新增页面,添加两个定时任务。配置完成页面如下:

      顶部按钮:定时器待机启动定时器为定时器操作按钮,即对所有定时任务有效。当定时器状态为启动中时,定时器待机显示,点击定时器状态变为待机中,所有定时任务待机;反之,所有定时任务可正常触发。
      右侧操作栏按钮:启动暂停立即执行删除,仅对当前定时任务有效。新增完的定时任务为未启动状态,点击启动按钮即可触发定时任务,点击暂停按钮即可暂停定时任务,点击立即执行按钮即可立即执行一次定时任务,点击删除按钮即可删除定时任务。
  • 点击操作按钮,观察控制台日志打印

6. 项目地址

  spring-boot-quartz-demo

posted @ 2020-09-25 15:40  C3Stones  阅读(2572)  评论(0编辑  收藏  举报