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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM