Laughing
spring boot Quartz基于持久化存储的动态管理
精彩回顾
我们在spring boot quartz定时任务基本使用及介绍和spring boot quartz持久化存储分别对quartz基本信息及持久化存储进行了介绍。
这篇文章我们继续介绍给予持久化存储实现任务的动态管理。
创建表结构
为了存储我们自己动态创建的任务,除了spring boot quartz持久化存储
介绍的添加quartz表结构之外,我们还需要添加一个自己的表。以下是MySQL的表结构,其他类型的数据库请按需修改。
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80021
Source Host : localhost:3306
Source Schema : quartz
Target Server Type : MySQL
Target Server Version : 80021
File Encoding : 65001
Date: 30/09/2020 13:34:24
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for task_quartz
-- ----------------------------
DROP TABLE IF EXISTS `task_quartz`;
CREATE TABLE `task_quartz` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`job_group` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '任务分组',
`job_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '任务名',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '任务描述',
`cron_expression` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'cron表达式',
`job_class_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
`job_status` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '1' COMMENT '任务状态',
`create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '创建者',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`modify_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '更新者',
`modify_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `NameWithGroup`(`job_group`, `job_name`) USING BTREE COMMENT '任务及分组唯一'
) ENGINE = MyISAM AUTO_INCREMENT = 68 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
创建工程,添加依赖
如果要实现任务的动态管理,这里单独借助一张表,存储任务的信息。
先介绍一个依赖的情况:
- MySQL数据库
- mybatis执行sql
- quartz依赖
lombok
具体项目依赖如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
修改配置文件
application.yaml
server: port: 8080 spring: profiles: active: dev
application-dev.yaml
spring: datasource: url: jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&&serverTimezone=UTC username: root password: root type: com.zaxxer.hikari.HikariDataSource quartz: job-store-type: jdbc mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: net.xiangcaowuyu.quartztask.entity configuration: map-underscore-to-camel-case: true
主要配置信息:
- MySQL数据库连接
- mybatis配置
job-store-type
存储到数据库
增加mybatis相关操作
TaskQuartzMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="net.xiangcaowuyu.quartztask.mapper.TaskQuartzMapper">
<resultMap id="BaseResultMap" type="net.xiangcaowuyu.quartztask.entity.TaskQuartz">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="job_group" jdbcType="VARCHAR" property="jobGroup"/>
<result column="job_name" jdbcType="VARCHAR" property="jobName"/>
<result column="description" jdbcType="VARCHAR" property="description"/>
<result column="cron_expression" jdbcType="VARCHAR" property="cronExpression"/>
<result column="job_class_name" jdbcType="VARCHAR" property="jobClassName"/>
<result column="job_status" jdbcType="VARCHAR" property="jobStatus"/>
<result column="create_by" jdbcType="VARCHAR" property="createBy"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="modify_by" jdbcType="VARCHAR" property="modifyBy"/>
<result column="modify_time" jdbcType="TIMESTAMP" property="modifyTime"/>
</resultMap>
<sql id="Base_Column_List">
id, job_group, job_name, description, cron_expression, job_class_name, job_status,
create_by, create_time, modify_by, modify_time
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from task_quartz
where id = #{id,jdbcType=BIGINT}
</select>
<select id="selectByJobNameAndJobGroup" resultType="net.xiangcaowuyu.quartztask.entity.TaskQuartz">
select
<include refid="Base_Column_List"/>
from task_quartz
where job_name = #{jonName,jdbcType=VARCHAR} and job_group = #{jobGroup,jdbcType=VARCHAR}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from task_quartz
where id = #{id,jdbcType=BIGINT}
</delete>
<delete id="deleteByJobNameAndJobGroup">
delete from task_quartz
where job_name = #{jonName,jdbcType=VARCHAR} and job_group = #{jobGroup,jdbcType=VARCHAR}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="net.xiangcaowuyu.quartztask.entity.TaskQuartz"
useGeneratedKeys="true">
insert into task_quartz (job_group, job_name, description,
cron_expression, job_class_name, job_status,
create_by, create_time, modify_by,
modify_time)
values (#{jobGroup,jdbcType=VARCHAR}, #{jobName,jdbcType=VARCHAR}, #{description,jdbcType=VARCHAR},
#{cronExpression,jdbcType=VARCHAR}, #{jobClassName,jdbcType=VARCHAR}, #{jobStatus,jdbcType=VARCHAR},
#{createBy,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP}, #{modifyBy,jdbcType=VARCHAR},
#{modifyTime,jdbcType=TIMESTAMP})
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id"
parameterType="net.xiangcaowuyu.quartztask.entity.TaskQuartz" useGeneratedKeys="true">
insert into task_quartz
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="jobGroup != null">
job_group,
</if>
<if test="jobName != null">
job_name,
</if>
<if test="description != null">
description,
</if>
<if test="cronExpression != null">
cron_expression,
</if>
<if test="jobClassName != null">
job_class_name,
</if>
<if test="jobStatus != null">
job_status,
</if>
<if test="createBy != null">
create_by,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="modifyBy != null">
modify_by,
</if>
<if test="modifyTime != null">
modify_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="jobGroup != null">
#{jobGroup,jdbcType=VARCHAR},
</if>
<if test="jobName != null">
#{jobName,jdbcType=VARCHAR},
</if>
<if test="description != null">
#{description,jdbcType=VARCHAR},
</if>
<if test="cronExpression != null">
#{cronExpression,jdbcType=VARCHAR},
</if>
<if test="jobClassName != null">
#{jobClassName,jdbcType=VARCHAR},
</if>
<if test="jobStatus != null">
#{jobStatus,jdbcType=VARCHAR},
</if>
<if test="createBy != null">
#{createBy,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
#{createTime,jdbcType=TIMESTAMP},
</if>
<if test="modifyBy != null">
#{modifyBy,jdbcType=VARCHAR},
</if>
<if test="modifyTime != null">
#{modifyTime,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="net.xiangcaowuyu.quartztask.entity.TaskQuartz">
update task_quartz
<set>
<if test="jobGroup != null">
job_group = #{jobGroup,jdbcType=VARCHAR},
</if>
<if test="jobName != null">
job_name = #{jobName,jdbcType=VARCHAR},
</if>
<if test="description != null">
description = #{description,jdbcType=VARCHAR},
</if>
<if test="cronExpression != null">
cron_expression = #{cronExpression,jdbcType=VARCHAR},
</if>
<if test="jobClassName != null">
job_class_name = #{jobClassName,jdbcType=VARCHAR},
</if>
<if test="jobStatus != null">
job_status = #{jobStatus,jdbcType=VARCHAR},
</if>
<if test="createBy != null">
create_by = #{createBy,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=TIMESTAMP},
</if>
<if test="modifyBy != null">
modify_by = #{modifyBy,jdbcType=VARCHAR},
</if>
<if test="modifyTime != null">
modify_time = #{modifyTime,jdbcType=TIMESTAMP},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="net.xiangcaowuyu.quartztask.entity.TaskQuartz">
update task_quartz
set job_group = #{jobGroup,jdbcType=VARCHAR},
job_name = #{jobName,jdbcType=VARCHAR},
description = #{description,jdbcType=VARCHAR},
cron_expression = #{cronExpression,jdbcType=VARCHAR},
job_class_name = #{jobClassName,jdbcType=VARCHAR},
job_status = #{jobStatus,jdbcType=VARCHAR},
create_by = #{createBy,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=TIMESTAMP},
modify_by = #{modifyBy,jdbcType=VARCHAR},
modify_time = #{modifyTime,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>
TaskQuartz.java
/**
* @author laughing
* @date 2020/9/30
* @site https://www.lisen.org
*/
@Data
public class TaskQuartz implements Serializable {
private Long id;
/**
* 任务分组
*/
private String jobGroup;
/**
* 任务名
*/
private String jobName;
/**
* 任务描述
*/
private String description;
/**
* cron表达式
*/
private String cronExpression;
/**
* 任务执行时调用哪个类的方法 包名+类名
*/
private String jobClassName;
/**
* 任务状态
*/
private String jobStatus;
/**
* 创建者
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String modifyBy;
/**
* 更新时间
*/
private Date modifyTime;
private static final long serialVersionUID = 1L;
}
TaskQuartzService.java
/**
* @author laughing
* @date 2020/9/30
* @site https://www.lisen.org
*/
public interface TaskQuartzService {
/**
* 根据主键删除
* @param id 主键
* @return 删除行数
*/
int deleteByPrimaryKey(Long id);
/**
* 插入
* @param record 实体
* @return 成功返回1
*/
int insert(TaskQuartz record);
/**
* 插入修改的值
* @param record 实体
* @return 成功返回1
*/
int insertSelective(TaskQuartz record);
/**
* 根据主键获取
* @param id 主键
* @return 实体
*/
TaskQuartz selectByPrimaryKey(Long id);
/**
* 更新
* @param record 实体
* @return 更新成功返回1
*/
int updateByPrimaryKeySelective(TaskQuartz record);
/**
* 更新所有值
* @param record 实体
* @return 成功返回1
*/
int updateByPrimaryKey(TaskQuartz record);
/**
* 根据名称及分组查找
* @param jobName 任务名称
* @param jobGroup 任务分组
* @return 任务
*/
TaskQuartz selectByJobNameAndJobGroup(@Param("jonName") String jobName, @Param("jobGroup") String jobGroup);
/**
* 根据名称及分组查找
* @param jobName 任务名称
* @param jobGroup 任务分组
* @return 任务
*/
int deleteByJobNameAndJobGroup(@Param("jonName") String jobName,@Param("jobGroup") String jobGroup);
}
TaskQuartzServiceImpl.java
/**
* @author laughing
* @date 2020/9/30
* @site https://www.lisen.org
*/
@Service
public class TaskQuartzServiceImpl implements TaskQuartzService {
@Resource
TaskQuartzMapper taskQuartzMapper;
/**
* 根据主键删除
*
* @param id 主键
* @return 删除行数
*/
@Override
public int deleteByPrimaryKey(Long id) {
return taskQuartzMapper.deleteByPrimaryKey(id);
}
/**
* 插入
*
* @param record 实体
* @return 成功返回1
*/
@Override
public int insert(TaskQuartz record) {
return taskQuartzMapper.insert(record);
}
/**
* 插入修改的值
*
* @param record 实体
* @return 成功返回1
*/
@Override
public int insertSelective(TaskQuartz record) {
return taskQuartzMapper.insertSelective(record);
}
/**
* 根据主键获取
*
* @param id 主键
* @return 实体
*/
@Override
public TaskQuartz selectByPrimaryKey(Long id) {
return taskQuartzMapper.selectByPrimaryKey(id);
}
/**
* 更新
*
* @param record 实体
* @return 更新成功返回1
*/
@Override
public int updateByPrimaryKeySelective(TaskQuartz record) {
return taskQuartzMapper.updateByPrimaryKeySelective(record);
}
/**
* 更新所有值
*
* @param record 实体
* @return 成功返回1
*/
@Override
public int updateByPrimaryKey(TaskQuartz record) {
return taskQuartzMapper.updateByPrimaryKey(record);
}
/**
* 根据名称及分组查找
*
* @param jobName 任务名称
* @param jobGroup 任务分组
* @return 任务
*/
@Override
public TaskQuartz selectByJobNameAndJobGroup(String jobName, String jobGroup) {
return taskQuartzMapper.selectByJobNameAndJobGroup(jobName,jobGroup);
}
/**
* 根据名称及分组查找
*
* @param jobName 任务名称
* @param jobGroup 任务分组
* @return 任务
*/
@Override
public int deleteByJobNameAndJobGroup(String jobName, String jobGroup) {
return taskQuartzMapper.deleteByJobNameAndJobGroup(jobName,jobGroup);
}
}
增加测试任务
PrintJob.java
/**
* @author laughing
* @date 2020/9/30
* @site https://www.lisen.org
*/
public class PrintJob implements Job {
private final Logger logger = LoggerFactory.getLogger(PrintJob.class);
/**
* 执行任务
*
* @param jobExecutionContext
* @throws JobExecutionException
*/
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
logger.info("Hello Job执行时间: " + new Date() + " Blog:" + jobExecutionContext.getJobDetail().getJobDataMap().get("blog"));
Thread.sleep(1000 * 5);
System.out.println("================执行完成========================");
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试新增任务
新增任务时,我们并没有创建quartz相关表,只是存储到task_quartz表中,任务启动时,插入quartz表
/**
* 新增或者保存任务
* 任务只是存储到task_quartz表中,任务启动时,插入quartz表
*
* @param taskQuartz 实体
* @return 结果
*/
@RequestMapping("saveQuartz")
public Result save(@RequestBody TaskQuartz taskQuartz) {
TaskQuartz task = taskQuartzService.selectByJobNameAndJobGroup(taskQuartz.getJobName(), taskQuartz.getJobGroup());
if (task == null) {
task = new TaskQuartz();
BeanUtils.copyProperties(taskQuartz, task);
taskQuartzService.insertSelective(task);
} else {
taskQuartzService.updateByPrimaryKeySelective(taskQuartz);
}
return Result.ok();
}
测试启动任务
任务启动时,启动任务时,将任务信息持久化到quartz表中
/**
* 启用任务
* 启动任务时,将任务信息持久化到quartz表中
*
* @param taskQuartz 实体
* @return 结果
*/
@RequestMapping("startJob")
public Result startJob(@RequestBody TaskQuartz taskQuartz) {
try {
JobKey jobKey = getJobKeyByTaskQuartz(taskQuartz);
// 存在先删除
if (scheduler.checkExists(jobKey)) {
scheduler.deleteJob(jobKey);
}
Class classz = Class.forName(taskQuartz.getJobClassName());
JobDetail jobDetail = JobBuilder.newJob()
.withDescription(taskQuartz.getDescription())
.withIdentity(jobKey)
.usingJobData("blog", "https://www.xiangcaowuyu.net")
.ofType(classz).build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("Trigger_" + taskQuartz.getJobName(), "TriggerGroup_" + taskQuartz.getJobGroup())
.withSchedule(CronScheduleBuilder.cronSchedule(taskQuartz.getCronExpression()))
.startNow()
.build();
scheduler.scheduleJob(jobDetail, trigger);
} catch (Exception exception) {
return Result.error(exception.getMessage());
}
return Result.ok();
}
通过postman测试,我们可以观察控制台,任务时10s执行一次
暂停任务
/**
* 暂停任务
*
* @param taskQuartz 实体
* @return 结果
*/
@RequestMapping("shutdown")
public Result shutdown(@RequestBody TaskQuartz taskQuartz) {
try {
JobKey jobKey = getJobKeyByTaskQuartz(taskQuartz);
scheduler.pauseJob(jobKey);
return Result.ok();
} catch (Exception ex) {
return Result.error(ex.getMessage());
}
}
恢复任务
/**
* 恢复任务
*
* @param taskQuartz 实体
* @return 结果
*/
@RequestMapping("resumeJob")
public Result resumeJob(@RequestBody TaskQuartz taskQuartz) {
try {
JobKey jobKey = getJobKeyByTaskQuartz(taskQuartz);
scheduler.resumeJob(jobKey);
return Result.ok();
} catch (Exception ex) {
return Result.error(ex.getMessage());
}
}
删除任务
/**
* 删除任务
*
* @param taskQuartz 实体
* @return 结果
*/
@RequestMapping("removeJob")
public Result removeJob(@RequestBody TaskQuartz taskQuartz) {
try {
JobKey jobKey = getJobKeyByTaskQuartz(taskQuartz);
scheduler.deleteJob(jobKey);
taskQuartzService.deleteByJobNameAndJobGroup(taskQuartz.getJobName(),taskQuartz.getJobGroup());
return Result.ok();
} catch (Exception ex) {
return Result.error(ex.getMessage());
}
}
香草物语
https://www.xiangcaowuyu.net/java/spring-boot-quartz-base-dynamic-management-of-persistent-storage.html(转载时请注明本文出处及文章链接)
现在可以了哦
{!{data:image/webp;base64,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}!}