在日常项目研发中,定时任务可谓是必不可少的一环,如果面对任务执行周期固定,业务简单的场景,可直接使用 Spring Boot 内置注解方式实现任务;而如果考虑更为复杂的管理任务信息,在可以通过集成 Quartz 等开源轮子来助力业务研发。
本次主要分享一下 Spring Boot 集成 Quartz 任务框架后,如何实现任务的动态管理,更能够让研发人员专注业务任务的研发,那么就要逐一解决如下疑问。
疑问:是否可以通过 API 动态创建任务呢?
疑问:是否可以通过 API 编辑任务的执行时间呢?
疑问:是否可以通过 API 暂停/恢复任务呢?
疑问:是否可以通过 API 删除任务呢?
疑问:是否可以通过页面完成任务的 CRUD 呢?
考虑到下面的操作是一个大工程,为了方便,重新开启一个 Spring Boot 项目,为了进一步熟练使用 Spring Boot 相关各种 starter,本次选用 MyBatis 作为持久层框架。
1. 核心管理代码
1.1 任务控制器
定义 TaskController,提供用户操作任务的相关 API,例如查询任务列表、添加任务、暂停任务、恢复任务、删除任务。
package com.example.demo.quartz.controller;
import com.example.demo.quartz.common.Result;
import com.example.demo.quartz.service.TaskInfoService;
import com.example.demo.quartz.vo.TaskInfoReq;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 定时任务管理
**/
@RestController
@RequestMapping("/task")
public class TaskController {
@Autowired
private TaskInfoService taskInfoService;
/**定时器列表*/
@PostMapping("/list")
public Result list(@RequestBody TaskInfoReq reqVo) {
return taskInfoService.selectTaskListByPage(reqVo);
}
/**定时器修改*/
@PostMapping("/edit")
public Result edit(@RequestBody TaskInfoReq reqVo) {
return taskInfoService.updateJob(reqVo);
}
/**暂停任务*/
@PostMapping("/pause")
public Result pause(Integer taskId) {
return taskInfoService.pauseJob(taskId);
}
/**增加任务*/
@PostMapping("/add")
public Result add(@RequestBody TaskInfoReq taskInfoReq) {
return taskInfoService.addJob(taskInfoReq);
}
/**恢复任务*/
@PostMapping("/resume")
public Result resume(Integer taskId) {
return taskInfoService.resumeJob(taskId);
}
/**删除任务*/
@PostMapping("/del")
public Result delete(@RequestBody TaskInfoReq reqVo) {
return taskInfoService.delete(reqVo);
}
}
1.2 任务管理
TaskManager 任务管理器,主要接收业务指令,来完成对 Quartz 容器进行操作。
package com.example.demo.quartz.task;
import com.example.demo.quartz.entity.TaskInfo;
import com.example.demo.quartz.utils.SpringContextUtils;
import com.example.demo.quartz.vo.TaskInfoReq;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 任务管理
* 1、添加任务 2、更新任务 3、暂停任务 4、恢复任务
**/
@Component
public class TaskManager {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskManager.class);
public static final String JOB_DEFAULT_GROUP_NAME = "JOB_DEFAULT_GROUP_NAME";
public static final String TRIGGER_DEFAULT_GROUP_NAME = "TRIGGER_DEFAULT_GROUP_NAME";
@Autowired
private Scheduler scheduler;
@Autowired
private SpringContextUtils springContextUtils;
/**
* 添加任务
*/
public boolean addJob(TaskInfoReq taskInfoReq) {
boolean flag = true;
if (!CronExpression.isValidExpression(taskInfoReq.getCron())) {
LOGGER.error("定时任务表达式有误:{}", taskInfoReq.getCron());
return false;
}
try {
String className = springContextUtils.getBean(taskInfoReq.getJobName()).getClass().getName();
JobDetail jobDetail = JobBuilder.newJob().withIdentity(new JobKey(taskInfoReq.getJobName(), JOB_DEFAULT_GROUP_NAME))
.ofType((Class<Job>) Class.forName(className))
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withSchedule(CronScheduleBuilder.cronSchedule(taskInfoReq.getCron()))
.withIdentity(new TriggerKey(taskInfoReq.getJobName(), TRIGGER_DEFAULT_GROUP_NAME))
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
} catch (Exception e) {
LOGGER.error("添加定时任务异常:{}", e.getMessage(), e);
flag = false;
}
return flag;
}
/**
* 更新任务
*/
public boolean updateJob(TaskInfo taskInfo) {
boolean flag = true;
try {
JobKey jobKey = new JobKey(taskInfo.getJobName(), JOB_DEFAULT_GROUP_NAME);
TriggerKey triggerKey = new TriggerKey(taskInfo.getJobName(), TRIGGER_DEFAULT_GROUP_NAME);
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
if (scheduler.checkExists(jobKey) && scheduler.checkExists(triggerKey)) {
Trigger newTrigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withSchedule(CronScheduleBuilder.cronSchedule(taskInfo.getCron()))
.withIdentity(triggerKey)
.build();
scheduler.rescheduleJob(triggerKey, newTrigger);
} else {
LOGGER.info("更新任务失败,任务不存在,任务名称:{},表达式:{}", taskInfo.getJobName(), taskInfo.getCron());
}
LOGGER.info("更新任务成功,任务名称:{},表达式:{}", taskInfo.getJobName(), taskInfo.getCron());
} catch (SchedulerException e) {
LOGGER.error("更新定时任务失败:{}", e.getMessage(), e);
flag = false;
}
return flag;
}
/**
* 暂停任务
*/
public boolean pauseJob(TaskInfo taskInfo) {
try {
scheduler.pauseJob(JobKey.jobKey(taskInfo.getJobName(), JOB_DEFAULT_GROUP_NAME));
LOGGER.info("任务暂停成功:{}", taskInfo.getId());
return true;
} catch (SchedulerException e) {
LOGGER.error("暂停定时任务失败:{}", e.getMessage(), e);
return false;
}
}
/**
* 恢复任务
*/
public boolean resumeJob(TaskInfo taskInfo) {
try {
scheduler.resumeJob(JobKey.jobKey(taskInfo.getJobName(), JOB_DEFAULT_GROUP_NAME));
LOGGER.info("任务恢复成功:{}", taskInfo.getId());
return true;
} catch (SchedulerException e) {
LOGGER.error("恢复定时任务失败:{}", e.getMessage(), e);
return false;
}
}
}
1.3 启动管理
1.3.1 QuartzManager
Spring Boot 容器启动时,加载启动所有任务。
package com.example.demo.quartz.config;
import com.example.demo.quartz.common.EnumTaskEnable;
import com.example.demo.quartz.entity.TaskInfo;
import com.example.demo.quartz.service.TaskInfoService;
import com.example.demo.quartz.vo.TaskInfoReq;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.List;
@Component
public class QuartzManager {
private Logger logger = LoggerFactory.getLogger(QuartzManager.class);
@Autowired
private Scheduler scheduler;
@Autowired
private SpringJobFactory springJobFactory;
@Autowired
private TaskInfoService taskInfoService;
@PostConstruct
public void start() {
//启动所有任务
try {
scheduler.setJobFactory(springJobFactory);
// scheduler.clear();
List<TaskInfo> tasks = taskInfoService.selectTasks();
for (TaskInfo taskInfo : tasks) {
if (EnumTaskEnable.START.getCode().equals(taskInfo.getStatus()) && !StringUtils.isEmpty(taskInfo.getCron())) {
TaskInfoReq data=new TaskInfoReq();
BeanUtils.copyProperties(taskInfo,data);
taskInfoService.addJob(data);
}
}
logger.info("定时任务启动完成");
} catch (SchedulerException e) {
logger.error(e.getMessage(), e);
throw new RuntimeException("定时任务初始化失败");
}
}
}
1.3.2 SpringJobFactory
package com.example.demo.quartz.config;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* 解决spring bean注入Job的问题
*/
@Component
public class SpringJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
// 进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
2. 支撑代码(表、entity、dao、service、utils)
支撑代码主要完成数据库的 CRUD 操作,实现方式很多种,不局限于 MyBatis,主要是抽象思想:能往数据库插入任务记录、查询任务记录就行。
2.1 任务信息表
CREATE TABLE `SC_TASK_INFO` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`cron` varchar(32) DEFAULT NULL COMMENT '定时执行',
`job_name` varchar(256) DEFAULT NULL COMMENT '任务名称',
`status` char(1) DEFAULT '0' COMMENT '任务开启状态 0-关闭 2-开启',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT COMMENT='定时任务表';
2.2 实体类
package com.example.demo.quartz.entity;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class TaskInfo implements Serializable {
private Integer id;
private String cron;
private String jobName;
private String status;
private Date createTime;
private Date updateTime;
}
2.3 TaskInfoDao 定义
package com.example.demo.quartz.dao;
import com.example.demo.quartz.entity.TaskInfo;
import com.example.demo.quartz.vo.TaskInfoReq;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface TaskInfoDao {
TaskInfo selectByJobName(String jobName);
List<TaskInfo> selectAll();
List<TaskInfo> selectTaskInfos(TaskInfoReq taskInfo);
int deleteByPrimaryKey(Integer id);
int insertSelective(TaskInfo record);
TaskInfo selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(TaskInfo record);
}
2.4 TaskInfoService 定义
package com.example.demo.quartz.service;
import com.example.demo.quartz.common.Result;
import com.example.demo.quartz.entity.TaskInfo;
import com.example.demo.quartz.vo.TaskInfoReq;
import java.util.List;
/**
* 定时任务接口
**/
public interface TaskInfoService {
/**获取任务列表分页*/
Result selectTaskListByPage(TaskInfoReq taskInfoReq);
/**添加定时任务*/
Result addJob(TaskInfoReq taskInfoReq);
/**更新任务*/
Result updateJob(TaskInfoReq taskInfoReq);
/**暂停任务*/
Result pauseJob(Integer taskId);
/**恢复任务*/
Result resumeJob(Integer taskId);
/**获取所有任务*/
List<TaskInfo> selectTasks();
/**删除任务*/
Result delete(TaskInfoReq reqVo);
}
2.5 TaskInfoServiceImpl 业务实现类定义
package com.example.demo.quartz.service.impl;
import com.example.demo.quartz.common.CodeMsg;
import com.example.demo.quartz.common.EnumTaskEnable;
import com.example.demo.quartz.common.ResponseFactory;
import com.example.demo.quartz.common.Result;
import com.example.demo.quartz.dao.TaskInfoDao;
import com.example.demo.quartz.entity.TaskInfo;
import com.example.demo.quartz.service.TaskInfoService;
import com.example.demo.quartz.task.TaskManager;
import com.example.demo.quartz.vo.TaskInfoReq;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.quartz.CronExpression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* 定时任务业务实现
**/
@Service
public class TaskInfoServiceImpl implements TaskInfoService {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskInfoServiceImpl.class);
@Resource
private TaskInfoDao taskInfoDao;
@Resource
private TaskManager taskManager;
@Override
public Result selectTaskListByPage(TaskInfoReq taskInfoReq) {
PageHelper.startPage(taskInfoReq.getPageCurrent(), taskInfoReq.getPageSize());
List<TaskInfo> list = taskInfoDao.selectTaskInfos(taskInfoReq);
PageInfo<TaskInfo> pageInfo = new PageInfo<>(list);
return ResponseFactory.build(pageInfo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result updateJob(TaskInfoReq taskInfoReq) {
if (!CronExpression.isValidExpression(taskInfoReq.getCron())) {
LOGGER.error("更新任务失败,表达式有误:{}", taskInfoReq.getCron());
return ResponseFactory.build(CodeMsg.TASK_CRON_ERROR);
}
TaskInfo isExistData = taskInfoDao.selectByJobName(taskInfoReq.getJobName());
//当任务存在,则更改失败
if ((!Objects.isNull(isExistData)) && (!isExistData.getId().equals(taskInfoReq.getId()))) {
return ResponseFactory.build(CodeMsg.TASK_CRON_DOUBLE);
}
TaskInfo data = taskInfoDao.selectByPrimaryKey(taskInfoReq.getId());
if (data == null) {
return ResponseFactory.build(CodeMsg.TASK_NOT_EXITES);
}
BeanUtils.copyProperties(taskInfoReq, data);
data.setUpdateTime(new Date());
taskInfoDao.updateByPrimaryKeySelective(data);
if (!taskManager.updateJob(data)) {
return ResponseFactory.build(CodeMsg.TASK_EXCEPTION);
}
return ResponseFactory.build();
}
@Override
public Result pauseJob(Integer taskId) {
TaskInfo data = taskInfoDao.selectByPrimaryKey(taskId);
if (data == null) {
return ResponseFactory.build(CodeMsg.TASK_NOT_EXITES);
}
if (!taskManager.pauseJob(data)) {
return ResponseFactory.build(CodeMsg.TASK_EXCEPTION);
}
data.setStatus(EnumTaskEnable.STOP.getCode());
taskInfoDao.updateByPrimaryKeySelective(data);
return ResponseFactory.build();
}
@Override
public Result resumeJob(Integer taskId) {
TaskInfo data = taskInfoDao.selectByPrimaryKey(taskId);
if (data == null) {
return ResponseFactory.build(CodeMsg.TASK_NOT_EXITES);
}
if (!taskManager.resumeJob(data)) {
return ResponseFactory.build(CodeMsg.TASK_EXCEPTION);
}
data.setStatus(EnumTaskEnable.START.getCode());
taskInfoDao.updateByPrimaryKeySelective(data);
return ResponseFactory.build();
}
@Override
public Result addJob(TaskInfoReq taskInfoReq) {
if (!taskManager.addJob(taskInfoReq)) {
return ResponseFactory.build(CodeMsg.TASK_EXCEPTION);
}
TaskInfo data = taskInfoDao.selectByJobName(taskInfoReq.getJobName());
//当任务不存在,则返回成功插入
if (Objects.isNull(data)) {
data = new TaskInfo();
BeanUtils.copyProperties(taskInfoReq, data);
data.setCreateTime(new Date());
taskInfoDao.insertSelective(data);
return ResponseFactory.build();
} else {
return ResponseFactory.build(CodeMsg.TASK_CRON_DOUBLE);
}
}
@Override
public Result delete(TaskInfoReq reqVo) {
try {
//TODO 删除任务只是做了暂停,如果是 Quartz Jdbc 模式下添加重复任务可能加不进去,并没有真正删除(可自行调整)
Result result = this.pauseJob(reqVo.getId());
//只有暂停成功的任务才能删除
if (CodeMsg.SUCCESS == result.getCode()) {
taskInfoDao.deleteByPrimaryKey(reqVo.getId());
return ResponseFactory.build();
} else {
return ResponseFactory.build(CodeMsg.TASK_EXCEPTION);
}
} catch (Exception e) {
return ResponseFactory.build(CodeMsg.TASK_EXCEPTION);
}
}
@Override
public List<TaskInfo> selectTasks() {
return taskInfoDao.selectAll();
}
}
2.6 TaskInfoMapper.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="com.example.demo.quartz.dao.TaskInfoDao">
<resultMap id="BaseResultMap" type="com.example.demo.quartz.entity.TaskInfo">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="cron" property="cron" jdbcType="VARCHAR"/>
<result column="job_name" property="jobName" jdbcType="VARCHAR"/>
<result column="status" property="status" jdbcType="CHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, cron, job_name, status, create_time, update_time
</sql>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select
<include refid="Base_Column_List"/>
from sc_task_info
where id = #{id,jdbcType=INTEGER}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
delete from sc_task_info
where id = #{id,jdbcType=INTEGER}
</delete>
<insert id="insert" parameterType="com.example.demo.quartz.entity.TaskInfo">
<selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into sc_task_info (cron, job_name, status,
create_time, update_time)
values (#{cron,jdbcType=VARCHAR}, #{jobName,jdbcType=VARCHAR}, #{status,jdbcType=CHAR},
#{createTime,jdbcType=TIMESTAMP}, #{updateTime,jdbcType=TIMESTAMP})
</insert>
<insert id="insertSelective" parameterType="com.example.demo.quartz.entity.TaskInfo">
<selectKey resultType="java.lang.Integer" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into sc_task_info
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="cron != null">
cron,
</if>
<if test="jobName != null">
job_name,
</if>
<if test="status != null">
status,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="updateTime != null">
update_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="cron != null">
#{cron,jdbcType=VARCHAR},
</if>
<if test="jobName != null">
#{jobName,jdbcType=VARCHAR},
</if>
<if test="status != null">
#{status,jdbcType=CHAR},
</if>
<if test="createTime != null">
#{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updateTime != null">
#{updateTime,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.example.demo.quartz.entity.TaskInfo">
update sc_task_info
<set>
<if test="cron != null">
cron = #{cron,jdbcType=VARCHAR},
</if>
<if test="jobName != null">
job_name = #{jobName,jdbcType=VARCHAR},
</if>
<if test="status != null">
status = #{status,jdbcType=CHAR},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=TIMESTAMP},
</if>
</set>
where id = #{id,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="com.example.demo.quartz.entity.TaskInfo">
update sc_task_info
set cron = #{cron,jdbcType=VARCHAR},
job_name = #{jobName,jdbcType=VARCHAR},
status = #{status,jdbcType=CHAR},
create_time = #{createTime,jdbcType=TIMESTAMP},
update_time = #{updateTime,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=INTEGER}
</update>
<select id="selectByJobName" resultMap="BaseResultMap"
parameterType="java.lang.String">
select * from sc_task_info where job_name=#{jobName}
</select>
<select id="selectAll" resultMap="BaseResultMap">
select * from sc_task_info
</select>
<select id="selectTaskInfos" resultMap="BaseResultMap"
parameterType="com.example.demo.quartz.vo.TaskInfoReq">
select * from sc_task_info
<where>
<if test="searchKey != null and searchKey != ''">job_name like concat('%',concat(trim(#{searchKey}),'%'))
</if>
<if test="status != null and status != ''">and status=#{status}</if>
</where>
</select>
</mapper>
3. 工具类&辅助代码
3.1 TaskInfoReq 类
package com.example.demo.quartz.vo;
import lombok.Data;
/**
* 任务请求类
**/
@Data
public class TaskInfoReq {
/**任务编号*/
private Integer id;
/**任务时间表达式*/
private String cron;
/**任务状态*/
private String status;
/**任务名称*/
private String jobName;
/**每页显示条数*/
private int pageSize=10;
/**当前页数*/
private int pageCurrent=1;
}
3.2 Result 类定义
package com.example.demo.quartz.common;
import lombok.Data;
@Data
public class Result {
private int code;
private String msg;
private Object retData;
}
3.3 响应工具类封装
package com.example.demo.quartz.common;
/**
* 响应工具类
*/
public class ResponseFactory {
private static Result commonBuild(int code, String errmsg) {
Result result = new Result();
result.setCode(code);
if (errmsg == null || errmsg.trim().length() == 0) {
result.setMsg(CodeMsg.getMsg(code));
} else {
result.setMsg(errmsg);
}
return result;
}
public static Result build(int code) {
return commonBuild(code, CodeMsg.getMsg(code));
}
public static Result build() {
return commonBuild(CodeMsg.SUCCESS, null);
}
public static Result build(Object data) {
Result json = commonBuild(CodeMsg.SUCCESS, null);
json.setRetData(data);
return json;
}
}
3.4 任务状态枚举类
package com.example.demo.quartz.common;
public enum EnumTaskEnable {
START("2", "开启"),
STOP("0", "关闭");
private String code;
private String msg;
EnumTaskEnable(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
}
3.5 公共返回码
package com.example.demo.quartz.common;
import java.util.HashMap;
import java.util.Map;
/**
* 公共返回码
*/
public class CodeMsg {
private static final Map<Integer, String> MSG = new HashMap<Integer, String>();
//系统
public static final int SUCCESS = 200;
public static final int ERROR = 500;
//任务
public static final int TASK_NOT_EXITES = 100001;
public static final int TASK_EXCEPTION = 100002;
public static final int TASK_CRON_ERROR = 100003;
public static final int TASK_CRON_DOUBLE = 100004;
static {
//系统
MSG.put(SUCCESS, "请求成功.");
MSG.put(ERROR, "服务器异常.");
//任务
MSG.put(TASK_NOT_EXITES, "定时任务不存在");
MSG.put(TASK_EXCEPTION, "设置定时任务失败");
MSG.put(TASK_CRON_ERROR, "表达式有误");
MSG.put(TASK_CRON_DOUBLE, "定时任务已经存在");
}
public static String getMsg(int errcode) {
return MSG.get(errcode);
}
}
3.6 SpringContextUtils 工具类
package com.example.demo.quartz.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
@Lazy(false)
public class SpringContextUtils implements ApplicationContextAware {
// Spring应用上下文环境
private static ApplicationContext applicationContext;
/**
* 实现ApplicationContextAware接口的回调方法,设置上下文环境
*
* @param applicationContext
*/
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextUtils.applicationContext = applicationContext;
}
/**
* @return ApplicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 获取对象
* 这里重写了bean方法,起主要作用
*
* @param name
* @return Object 一个以所给名字注册的bean的实例
* @throws BeansException
*/
public static <T> T getBean(String name) {
try {
return (T) applicationContext.getBean(name);
} catch (Exception e) {
return null;
}
}
public static <T> T getBean(Class<T> clazz) {
try {
return applicationContext.getBean(clazz);
} catch (Exception e) {
return null;
}
}
}
3.7 配置文件
3.7.1 application.properties
server.port=${random.int[10000,19999]}
#server.port=15158
## 将 Quartz 持久化方式修改为 jdbc
spring.quartz.job-store-type=jdbc
## 实例名称(默认为quartzScheduler)
spring.quartz.properties.org.quartz.scheduler.instanceName=SC_Scheduler
## 实例节点 ID 自动生成
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
## 修改存储内容使用的类
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
## 数据源信息
spring.quartz.properties.org.quartz.jobStore.dataSource=quartz_jobs
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.driver=com.mysql.cj.jdbc.Driver
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.URL=jdbc:mysql://127.0.0.1:3306/quartz_jobs?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.user=root
spring.quartz.properties.org.quartz.dataSource.quartz_jobs.password=123456
## 开启集群,多个 Quartz 实例使用同一组数据库表
spring.quartz.properties.org.quartz.jobStore.isClustered=true
# MySQL 链接信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/quartz_jobs?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## MyBatis 的配置
# Mapper资源文件存放的路径
mybatis.mapper-locations=classpath*:mapper/*.xml
# Dao 接口文件存放的目录
mybatis.type-aliases-package=com.example.demo.quartz.dao
# 开启 debug,输出 SQL
logging.level.com.example.demo.dao=debug
#pagehelper propertis文件分页插件配置
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.params.count=countSql
备注:考虑到部署成本问题,若是单机、内存方式存储任务信息,则只可把 Quartz 相关配置通通去掉。
3.7.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo_job</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo_job</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.4</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</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>
4. 任务代码
定义要执行的业务逻辑任务 DongAoJob 类,这块也就是研发人员重点关注的,后续只需实现 Job 业务就行。
package com.example.demo.quartz.task;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
/**
* 定义一个调度器要执行的任务
*/
@Component
public class DongAoJob extends QuartzJobBean {
private static final Log logger = LogFactory.getLog(DongAoJob.class);
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
logger.info("幼年是盼盼,青年是晶晶,中年是冰墩墩,生活见好逐渐发福");
}
}
5. 程序入口
package com.example.demo.quartz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoJobApplication {
public static void main(String[] args) {
SpringApplication.run(DemoJobApplication.class, args);
}
}
6. 运行验证
其实挂个简单页面就能轻松完成页面化配置任务,本次用 Postman 方式直接调用任务管理的 API。
6.1 添加任务
此时库任务 Id 为7:
控制台:
6.2 查询任务
6.3 编辑任务
控制台输出:
6.4 暂停任务
http://localhost:15158/task/pause?taskId=7
6.5 恢复任务
http://localhost:15158/task/resume?taskId=7
7. 例行回顾
本文是 Spring Boot 项目集成 Quartz 来实现任务的动态管理,主要是代码,感兴趣的可以自行拿去验证改造并用于实践。
玩转 Spring Boot 集成定时任务篇就写到这里,希望大家能够喜欢。
历史系列文章:
玩转 Spring Boot 入门篇 玩转 Spring Boot 集成篇(MySQL、Druid、HikariCP) 玩转 Spring Boot 集成篇(MyBatis、JPA、事务支持) 玩转 Spring Boot 集成篇(Redis) 玩转 Spring Boot 集成篇(Actuator、Spring Boot Admin) 玩转 Spring Boot 集成篇(RabbitMQ) 玩转 Spring Boot 集成篇(@Scheduled、静态、动态定时任务)