原文链接:http://pengfeiguo.com/article/16
什么是Quartz?
一个定时任务调度框架,简单易用,功能强大可以使实现定时任务的。
优点:
- 支持集群下定时任务处理
- 支持任务并发阻塞(上一个任务完成后,才能继续下一个任务)
- 支持通过API对任务的操作,例如新增任务、修改、启动、暂停、停止(可以在代码中进行调用,而无需修改配置文件再次部署)
- 支持的数据库种类被较多
目标
- 在Spring Boot中集成Quartz
- 使用MySql数据库(程序自动导入,无需人工执行脚本)
- 使用Spring 自身配置的数据源(不再单独配置qz数据源)
- 通过代码实现动态化添加、修改、暂停、终止job
开发环境
- JDK版本1.8
- Spring Boot 版本:2.3.3.RELEASE
- 开发工具:eclipse
开发实践
本章节将从Pom依赖配置开始,直到成功运行起该程序为止,为各位朋友提供真实可行的代码实现
pom配置
关于Pom.xml中的配置,有两种方式,第一种使用spring-boot封装的依赖,第二种使用org.quartz的依赖
第一种方式:
<!--引入quartz定时框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
这种依赖的好处是你不需要考虑qz的版本号,Spring boot会根据自身的版本来适应不同的quartz版本,但是缺点也很明显,你无法使用其他版本的quartz(不同版本的QZ结构稍有差异),而且在这个封装的依赖中,其实里面也仅仅是指定了org.quartz的依赖,并没有其他的配置或者逻辑
第二种方式
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.2</version>
</dependency>
这种方式是直接引用的quartz,就是第一种方式中的配置的依赖,但是在这里你就可以自己选择quartz的版本(注意:不同版本的qz结构有差异,Spring boot读取文件时有可能会出现问题。例如在Spring boot 2.3.3.RELEASE中,2.2.1的版本自动生成表结构时会报错--jar包中没有对应的sql脚本,而2.3.2就不存在这个问题)
您可以根据自己的需求来选择哪种依赖方式(以上两种只需要使用其中任何一种即可,无需都使用)
quartz.yml配置
################### Quartz配置 start ##################################################
server:
port: 8080
servlet:
context-path: /quartz
spring:
application:
name: demo
#连接池配置
datasource:
#账号配置
url: jdbc:mysql://localhost:3306/qz_table?allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
#hikari数据库连接池
hikari:
pool-name: Retail_HikariCP
minimum-idle: 5 #最小空闲连接数量
idle-timeout: 180000 #空闲连接存活最大时间,默认600000(10分钟)
maximum-pool-size: 10 #连接池最大连接数,默认是10
auto-commit: true #此属性控制从池返回的连接的默认自动提交行为,默认值:true
max-lifetime: 1800000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
connection-timeout: 30000 #数据库连接超时时间,默认30秒,即30000
connection-test-query: SELECT 1
quartz:
# dataSource:
# default:
# driver: com.mysql.jdbc.Driver
# URL: jdbc:mysql://localhost:3306/jobconfig?useUnicode=true&characterEncoding=utf8
# user: root
# password: 12345678
# maxConnections: 5
#相关属性配置
properties:
org:
quartz:
scheduler:
instanceName: quartzScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: qrtz_
isClustered: false
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
#数据库方式
job-store-type: JDBC
#初始化表结构
jdbc:
initialize-schema: never
#mybatis配置
mybatis:
type-aliases-package: com.example.demo.entity
mapper-locations: classpath:mapper/*.xml
#分页配置, pageHelper是物理分页插件
pagehelper:
#4.0.0以后版本可以不设置该参数,该示例中是5.1.4
helper-dialect: mysql
#启用合理化,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页
reasonable: true
################### Quartz配置 End ###################################################
注意:
- 本文配置使用的是yml文件方式,如果您想使用.properties方式,请将上面的冒号改成等号
- 本文中使用的quartz没有单独配置数据源,而是使用的您在Spring boot中已经配置的数据源(自动识别,可以看下源码,默认支持C3p0,hikari等几种)
- 本文中默认表结构初始化方式为initialize-schema: never(共有三种方式:always-每次启动都初始化数据库,never-不初始化表结构,embedded,您可以在首次运行时将此方式设置为always,然后再改成never)
- 在这个配置中dataSource注释掉了,如果您需要quartz单独配置一套数据源,请放开此部分注释
- 注意,quartz.yml需要被Spring boot发现(或者直接配置到application.yml中),否则即使配置了也不会起作用,而是使用默认的RAM(内存)去保存定时任务的数据(速度快但是应用重启后会丢失,我在此处就栽跟头了)
到这一步,配置已经完成,接下来就该具体的代码了
逻辑代码实现
该部分java类的结构如下:
common包
common包中的Result.java为返回结果类(controller中使用,如您不想使用,请替换为自己的逻辑)
package com.example.demo.common;
import java.util.HashMap;
import java.util.Map;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: 响应结果类
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020
*/
public class Result extends HashMap<String, Object> {
public Result() {
put("code", 200);
}
public static Result error() {
return error(500, "未知异常,请联系管理员");
}
public static Result error(String msg) {
return error(500, msg);
}
public static Result error(int code, String msg) {
Result r = new Result();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static Result ok(Object msg) {
Result r = new Result();
r.put("msg", msg);
return r;
}
public static Result ok(Map<String, Object> map) {
Result r = new Result();
r.putAll(map);
return r;
}
public static Result ok() {
return new Result();
}
@Override
public Result put(String key, Object value) {
super.put(key, value);
return this;
}
}
constant包
这个包中的类存的是quartz中的job和trigger的名字常量
package com.example.demo.constant;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: 常量类
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020
*/
public class GloabalConstant {
public static final String QZ_JOB_GROUP_NAME = "JOB_GROUP_NAME";
public static final String QZ_TRIGGER_GROUP_NAME = "TRIGGER_GROUP_NAME";
}
entity包
该包中的实体类为封装添加的job信息的实体类,大家可以根据自己的需求改成自己想要的字段
package com.example.demo.entity;
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import lombok.Data;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: qzmodel。
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020
*/
@Data
public class QuartzJobModule {
/**
* 触发器开始时间
*/
private Date startTime;
/**
* 触发器结束时间
*/
private Date endTime;
/**
* job名称
*/
private String jobName;
/**
* job组名
*/
private String jobGroupName;
/**
* 定时器名称
*/
private String triggerName;
/**
* 定时器组名
*/
private String triggerGroupName;
/**
* 执行定时任务的具体操作
*/
private Class jobClass;
/**
* cron表达式
*/
private String cron;
/**
* job的附加信息
*/
private JobDataMap jobDataMap = new JobDataMap();
/**
* 校验
* @return
*/
public boolean verify(){
return !(StringUtils.isEmpty(jobName)
|| StringUtils.isEmpty(jobGroupName)
|| StringUtils.isEmpty(triggerName)
|| StringUtils.isEmpty(triggerGroupName)
|| StringUtils.isEmpty(cron)
// || CollectionUtils.isEmpty(jobDataMap)
|| ObjectUtils.isEmpty(startTime)
|| ObjectUtils.isEmpty(endTime)
|| !ClassUtils.hasMethod(Job.class, "execute", JobExecutionContext.class)
);
}
}
utils包
这个里面有三个类,CronUtil.java为cron工具类,提供将时间转为cron表达式的工具;DateUtils.java为日期工具类,提供日期格式化的工具类;QuartzJobComponent.java是最重要的,提供job的CRUD操作,前两个类为封装QuartzJobModule中的属性提供的,可以自己实现,但是第三个类最好不要修改,可以直接拿来用。
CronUtil.java
package com.example.demo.utils;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: cron生成工具类
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020
*/
public class CronUtil {
/**
* @param batchScheduleModel
* @return
* @Desc 转化cron表达式
*/
public static String convertCronExpression(Date startDate, Date endDate, String[] weeks) {
StringBuffer sb = new StringBuffer();
sb.append(convertSeconds()).append(" ").append(convertMinutes(startDate)).append(" ")
.append(convertHours(startDate, endDate)).append(" ").append(convertDay(startDate, endDate)).append(" ")
.append(convertMonth(startDate, endDate)).append(" ").append(convertWeek(weeks));
return sb.toString();
}
/**
* 获取定时任务开始时间
*
* @param batchScheduleModel
* @return
* @throws ParseException
*/
public static Date getStartDate(Date startDate) throws ParseException {
String yyyyMMddS = DateUtils.date2String(startDate, "yyyyMMdd");
Calendar startCal = Calendar.getInstance();
startCal.setTime(startDate);
int startHour = startCal.get(Calendar.HOUR_OF_DAY);// 小时(calendar.HOUR
// 12小时制,calendar.HOUR_OF_DAY 24小时)
int startMin = startCal.get(Calendar.MINUTE);// 分
String startTime = "2359";
if (startHour < Integer.parseInt(startTime.substring(0, 2))) {
startTime = startHour + "" + startMin;
}
return DateUtils.string2Date(yyyyMMddS + startTime + "00", "yyyyMMddHHmmss");
}
/**
* 获取定时任务结束时间
*
* @param batchScheduleModel
* @return
* @throws ParseException
*/
public static Date getEndDate(Date endDate) throws ParseException {
String yyyyMMddE = DateUtils.date2String(endDate, "yyyyMMdd");
Calendar startCal = Calendar.getInstance();
startCal.setTime(endDate);
int startHour = startCal.get(Calendar.HOUR_OF_DAY);// 小时(calendar.HOUR
// 12小时制,calendar.HOUR_OF_DAY 24小时)
int startMin = startCal.get(Calendar.MINUTE);// 分
String endTime = "0000";
if (startHour < Integer.parseInt(endTime.substring(0, 2))) {
endTime = startHour + "" + startMin;
}
return DateUtils.string2Date(yyyyMMddE + endTime + "00", "yyyyMMddHHmmss");
}
/**
* 判断当前时间是否在规则时间范围内
*
* @param timesEntityList
* @return
*/
public static boolean isInRuleTimes(Date startDate, Date endDate) {
Date date = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int hour = cal.get(Calendar.HOUR_OF_DAY);// 小时(calendar.HOUR
// 12小时制,calendar.HOUR_OF_DAY 24小时)
int minute = cal.get(Calendar.MINUTE);// 分
Calendar startCal = Calendar.getInstance();
startCal.setTime(date);
int startHour = startCal.get(Calendar.HOUR_OF_DAY);// 小时(calendar.HOUR
// 12小时制,calendar.HOUR_OF_DAY 24小时)
int startMinute = startCal.get(Calendar.MINUTE);// 分
Calendar endCal = Calendar.getInstance();
endCal.setTime(date);
int endHour = endCal.get(Calendar.HOUR_OF_DAY);// 小时(calendar.HOUR
// 12小时制,calendar.HOUR_OF_DAY 24小时)
int endMinute = cal.get(Calendar.MINUTE);// 分
if (startHour < hour && hour < endHour) {
return true;
}
if (startHour == hour && hour == endHour && startMinute < minute && minute < endMinute) {
return true;
}
if (startHour == hour && startMinute < minute) {
return true;
}
if (endHour == hour && minute < endMinute) {
return true;
}
return false;
}
/**
* 抽取cron中的hour
*
* @param batchRuleTimeEntityList
* @return
*/
public static String convertHours(Date startDay, Date endDay) {
Calendar startCal = Calendar.getInstance();
startCal.setTime(startDay);
int startHour = startCal.get(Calendar.HOUR_OF_DAY);// 小时(calendar.HOUR
// 12小时制,calendar.HOUR_OF_DAY 24小时)
Calendar endCal = Calendar.getInstance();
endCal.setTime(endDay);
int endHour = endCal.get(Calendar.HOUR_OF_DAY);// 小时(calendar.HOUR
// 12小时制,calendar.HOUR_OF_DAY 24小时)
StringBuffer sb = new StringBuffer();
sb.append(startHour).append("-").append(endHour);
return sb.toString();
}
/**
* 抽取cron中的minute
*
* @param batchRuleTimeEntityList
* @return
*/
public static String convertMinutes(Date startDay) {
return "* ";
/*
* StringBuffer sb = new StringBuffer();
* batchRuleTimeEntityList.forEach((b)->{
* String start = b.getStartTime();
* String end = b.getEndTime();
* String minS = start.substring(3,5);
* String minE = end.substring(3,5);
* sb.append(minS).append("-").append(minE).append(",");
* });
* return sb.deleteCharAt(sb.length()-1).toString();
*/
}
/**
* 抽取cron中的seconds
*
* @return
*/
public static String convertSeconds() {
return "1";
}
/**
* 抽取cron中的day, 在这个项目里直接返回?
*
* @param startDate
* @param endDate
* @return
*/
public static String convertDay(Date startDate, Date endDate) {
return "?";
/*
* String start = DateUtil.formatDate(DateUtil.YYYYMMDD, startDate);
* String end = DateUtil.formatDate(DateUtil.YYYYMMDD, endDate);
* String dayS = start.substring(6,8);
* String dayE = end.substring(6,8);
* return dayS + "-" + dayE;
*/
}
/**
* 抽取cron中的month
*
* @param startDate
* @param endDate
* @return
*/
public static String convertMonth(Date startDate, Date endDate) {
Calendar startCal = Calendar.getInstance();
startCal.setTime(startDate);
// 获取月份(因为在格里高利历和罗马儒略历一年中第一个月为JANUARY,它为0,最后一个月取决于一年中的月份数,所以这个值初始为0,所以需要加1)
int startMonth = startCal.get(Calendar.MONTH) + 1;
Calendar endCal = Calendar.getInstance();
endCal.setTime(endDate);
// 获取月份(因为在格里高利历和罗马儒略历一年中第一个月为JANUARY,它为0,最后一个月取决于一年中的月份数,所以这个值初始为0,所以需要加1)
int endMonth = endCal.get(Calendar.MONTH) + 1;
return startMonth + "-" + endMonth;
}
/**
* 抽取cron中的week
*
* @param dayOfWeeks
* @return
*/
public static String convertWeek(String[] dayOfWeeks) {
StringBuffer sb = new StringBuffer();
for (String dayOfWeek : dayOfWeeks) {
sb.append(dayOfWeek).append(",");
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
/**
* 抽取cron中的week
*
* @param dayOfWeeks
* @return
*/
public static String convertWeek(String dayOfWeeks) {
StringBuffer sb = new StringBuffer();
String[] split = dayOfWeeks.split(",");
for (String str : split) {
sb.append(str).append(",");
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
}
}
DateUtils.java
package com.example.demo.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.util.StringUtils;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: 日期工具类
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020
*/
public class DateUtils {
/**
* 模式 :yyyyMMddHHmmss
*/
private static final String YYYYMMDD_HHMMSS = "yyyyMMddHHmmss";
/**
* 模式 :yyyyMMdd
*/
private static final String YYYYMMDD = "yyyyMMdd";
/**
* 方法说明:日期类型按照指定格式转成字符串.
*
* @param date
* 日期
* @param pattern
* 日期格式
* @return
*/
public static String date2String(Date date, String pattern) {
if (null == date) {
date = new Date();
}
if (StringUtils.isEmpty(pattern)) {
pattern = "yyyy-MM-dd HH:mm:ss";
}
try {
return getDateFormat(pattern).format(date);
}
catch (Exception e) {
throw e;
}
}
/**
* 方法说明:获取指定模式pattern的SimpleDateFormat对象.
*
* @param pattern
* 日期格式
* @return
*/
private static SimpleDateFormat getDateFormat(String pattern) {
return new SimpleDateFormat(pattern);
}
/**
* 方法说明:获取默认模式"yyyyMMdd"的SimpleDateFormat对象.
*
* @return
*/
private static SimpleDateFormat getDateFormat() {
return new SimpleDateFormat(YYYYMMDD);
}
/**
* 日期转换
*
* @param srcStr
* 日期字符串
* @param pattern
* 日期格式
* @return
* @throws ParseException
*/
public static String stringFormat(String srcStr, String pattern) throws ParseException {
Date date = string2Date(srcStr);
return date2String(date, pattern);
}
/**
* 方法说明:日期类型转成yyyyMMdd格式字符串.
*
* @param date
* 日期
* @return
*/
public static String date2String(Date date) {
return date2String(date, YYYYMMDD);
}
/**
* 方法说明:字符串转日期类型.
*
* @param date
* 日期字符串
* @return
* @throws ParseException
* @throws Exception
*/
public static Date string2Date(String date) throws ParseException {
if (date.length() != 16) {
return getDateFormat().parse(date);
}
else {
return getDateFormat(YYYYMMDD_HHMMSS).parse(date);
}
}
/**
* 按照转换规则将日期字符串转换为Date类型的时间
*
* @param dateString
* 要转换的日期字符串
* @param format
* 转换的格式,例如:YYYYMMDD
* @return 转换后的Date类型的日期
* @throws ParseException
* @throws BusinessException
* 异常
*/
public static Date string2Date(String dateString, String format) throws ParseException {
SimpleDateFormat sd1 = new SimpleDateFormat(format);
Date date = sd1.parse(dateString);
return date;
}
}
QuartzJobComponent.java
package com.example.demo.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.TriggerUtils;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.example.demo.constant.GloabalConstant;
import com.example.demo.entity.QuartzJobModule;
import lombok.extern.slf4j.Slf4j;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: qz工具类
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020 。
*/
@Slf4j
@Component
public class QuartzJobComponent {
@Autowired
private Scheduler scheduler;
/**
* @Description: 添加一个定时任务
* @param quartzModel
*/
public void addJob(QuartzJobModule quartzModel) {
if (quartzModel.verify()) {
try {
JobDetail job = JobBuilder.newJob(quartzModel.getJobClass())
.withIdentity(quartzModel.getJobName(), quartzModel.getJobGroupName())
.setJobData(quartzModel.getJobDataMap()).build();
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzModel.getCron());
// 按新的cronExpression表达式构建一个新的trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(quartzModel.getTriggerName(), quartzModel.getTriggerGroupName())
.startAt(quartzModel.getStartTime()).endAt(quartzModel.getEndTime()).withSchedule(
scheduleBuilder)
.build();
scheduler.scheduleJob(job, trigger);
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
}
}
catch (SchedulerException e) {
log.error("Add quartz job error, jobName = {}", quartzModel.getJobName());
}
}
else {
log.error("QuartzModel is invalid!");
}
}
/**
* @Description: 修改一个任务的触发时间(使用默认的任务组名,触发器名,触发器组名)
* @param jobName
* @param cron
*/
public void modifyJobTime(String jobName, String cron, Date startDate, Date endDate) {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, GloabalConstant.QZ_TRIGGER_GROUP_NAME);
try {
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (trigger == null) {
return;
}
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(cron)) {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
// 按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey)
.startAt(startDate)
.endAt(endDate).withSchedule(scheduleBuilder).build();
// 按新的trigger重新设置job执行
scheduler.rescheduleJob(triggerKey, trigger);
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description:修改任务,(可以修改任务名,任务类,触发时间)
* 原理:移除原来的任务,添加新的任务
* @param oldJobName
* :原任务名
* @param jobName
* @param jobclass
* @param cron
*/
public void modifyJob(String oldJobName, String jobName, Class jobclass, String cron) {
/*
* removeJob(oldJobName);
* addJob(jobName, jobclass, cron);
* System.err.println("修改任务"+oldJobName);
*/
TriggerKey triggerKey = TriggerKey.triggerKey(oldJobName, GloabalConstant.QZ_TRIGGER_GROUP_NAME);
JobKey jobKey = JobKey.jobKey(oldJobName, GloabalConstant.QZ_JOB_GROUP_NAME);
try {
Trigger trigger = (Trigger) scheduler.getTrigger(triggerKey);
if (trigger == null) {
return;
}
scheduler.pauseTrigger(triggerKey);// 停止触发器
scheduler.unscheduleJob(triggerKey);// 移除触发器
scheduler.deleteJob(jobKey);// 删除任务
System.err.println("移除任务:" + oldJobName);
JobDetail job = JobBuilder.newJob(jobclass).withIdentity(jobName,
GloabalConstant.QZ_JOB_GROUP_NAME)
.build();
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
// 按新的cronExpression表达式构建一个新的trigger
Trigger newTrigger = TriggerBuilder.newTrigger().withIdentity(jobName,
GloabalConstant.QZ_TRIGGER_GROUP_NAME)
.withSchedule(scheduleBuilder).build();
// 交给scheduler去调度
scheduler.scheduleJob(job, newTrigger);
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
System.err.println("添加新任务:" + jobName);
}
System.err.println("修改任务【" + oldJobName + "】为:" + jobName);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description: 修改一个任务的触发时间
* @param triggerName
* @param triggerGroupName
* @param cron
*/
public void modifyJobTime(String triggerName, String triggerGroupName, String cron) {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
try {
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (trigger == null) {
return;
}
String oldTime = trigger.getCronExpression();
if (!oldTime.equalsIgnoreCase(cron)) {
// trigger已存在,则更新相应的定时设置
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
// 按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
// 按新的trigger重新设置job执行
scheduler.resumeTrigger(triggerKey);
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description 移除一个任务(使用默认的任务组名,触发器名,触发器组名)
* @param jobName
*/
public void removeJob(String jobName) {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, GloabalConstant.QZ_TRIGGER_GROUP_NAME);
JobKey jobKey = JobKey.jobKey(jobName, GloabalConstant.QZ_JOB_GROUP_NAME);
try {
Trigger trigger = (Trigger) scheduler.getTrigger(triggerKey);
if (trigger == null) {
return;
}
scheduler.pauseTrigger(triggerKey);// 停止触发器
scheduler.unscheduleJob(triggerKey);// 移除触发器
scheduler.deleteJob(jobKey);// 删除任务
System.err.println("移除任务:" + jobName);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description: 移除一个任务
* @param jobName
* @param jobGroupName
* @param triggerName
* @param triggerGroupName
*/
public void removeJob(String jobName, String jobGroupName, String triggerName, String triggerGroupName) {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, triggerGroupName);
JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
try {
scheduler.pauseTrigger(triggerKey);// 停止触发器
scheduler.unscheduleJob(triggerKey);// 移除触发器
scheduler.deleteJob(jobKey);// 删除任务
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description:暂停一个任务(使用默认组名)
* @param jobName
*/
public void pauseJob(String jobName) {
JobKey jobKey = JobKey.jobKey(jobName, GloabalConstant.QZ_JOB_GROUP_NAME);
try {
scheduler.pauseJob(jobKey);
}
catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* @Description:暂停一个任务
* @param jobName
* @param jobGroupName
*/
public void pauseJob(String jobName, String jobGroupName) {
JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
try {
scheduler.pauseJob(jobKey);
}
catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* @Description:恢复一个任务(使用默认组名)
* @param jobName
*/
public void resumeJob(String jobName) {
JobKey jobKey = JobKey.jobKey(jobName, GloabalConstant.QZ_JOB_GROUP_NAME);
try {
scheduler.resumeJob(jobKey);
}
catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* @Description:恢复一个任务
* @param jobName
* @param jobGroupName
*/
public void resumeJob(String jobName, String jobGroupName) {
JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
try {
scheduler.resumeJob(jobKey);
}
catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* @Description:启动所有定时任务
*/
public void startJobs() {
try {
scheduler.start();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description 关闭所有定时任务
*/
public void shutdownJobs() {
try {
if (!scheduler.isShutdown()) {
scheduler.shutdown();
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @Description: 立即运行任务,这里的立即运行,只会运行一次,方便测试时用。
* @param jobName
*/
public void triggerJob(String jobName) {
JobKey jobKey = JobKey.jobKey(jobName, GloabalConstant.QZ_JOB_GROUP_NAME);
try {
scheduler.triggerJob(jobKey);
}
catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* @Description: 立即运行任务,这里的立即运行,只会运行一次,方便测试时用。
* @param jobName
* @param jobGroupName
*/
public void triggerJob(String jobName, String jobGroupName) {
JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
try {
scheduler.triggerJob(jobKey);
}
catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* @Description: 获取任务状态
* @param jobName
* 触发器名
*/
public String getTriggerState(String jobName) {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, GloabalConstant.QZ_TRIGGER_GROUP_NAME);
String name = null;
try {
Trigger.TriggerState triggerState = scheduler.getTriggerState(triggerKey);
name = triggerState.name();
}
catch (SchedulerException e) {
e.printStackTrace();
}
return name;
}
/**
* @Description:获取最近5次执行时间
* @param cron
*/
public List<String> getRecentTriggerTime(String cron) {
List<String> list = new ArrayList<String>();
try {
CronTriggerImpl cronTriggerImpl = new CronTriggerImpl();
cronTriggerImpl.setCronExpression(cron);
List<Date> dates = TriggerUtils.computeFireTimes(cronTriggerImpl, null, 5);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
for (Date date : dates) {
list.add(dateFormat.format(date));
}
}
catch (ParseException e) {
log.error("GetRecentTriggerTime error, cron = {}", cron, e);
}
return list;
}
}
qzComp包
该包中的类为quartz中job调用时实际业务处理类
package com.example.demo.qzComp;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import lombok.extern.slf4j.Slf4j;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: quartz job 业务处理类
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020
*/
@DisallowConcurrentExecution
@Slf4j
public class TaskJobDetail extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
log.info("begin delwith batch task >>>>>>>>>>>>>>>>>>>>>>>");
String batchId = context.getJobDetail().getKey().getName();
log.info("执行的任务id为:[{}]", batchId);
}
}
注意:
- 该类为实际业务处理类,定时任务调起时,所有的业务都在此处写(该类不能使用Spring的注解,需要通过上下文来引入需要的bean)
- 我们可以看到,在该类中有一行代码:String batchId = context.getJobDetail().getKey().getName();该行代码会获取job的名称,我们可以在添加job的时候,将要处理的数据的唯一标识(比如id)设置为jobName,然后在这个业务处理类中就知道到底要处理哪个数据了。
controller包
该包中模拟的是前端请求添加、暂停、修改、停止、删除job的接口,在这个类中,同样有封装job的代码
package com.example.demo.controller;
import java.util.Calendar;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.common.Result;
import com.example.demo.constant.GloabalConstant;
import com.example.demo.entity.QuartzJobModule;
import com.example.demo.qzComp.TaskJobDetail;
import com.example.demo.utils.CronUtil;
import com.example.demo.utils.QuartzJobComponent;
/**
* @project: 郭鹏飞的博客(wwww.pengfeiguo.com)
* @description: quartz controller 。
* @version 1.0.0
* @errorcode
* 错误码: 错误描述
* @author
* <li>2020-09-04 825338623@qq.com Create 1.0
* @copyright ©2019-2020
*/
@RestController
@RequestMapping("/job")
public class JobController {
private final static Logger LOGGER = LoggerFactory.getLogger(JobController.class);
@Autowired
private QuartzJobComponent quartzJobComponent;
@PostMapping("/add")
@ResponseBody
public Result save() {
LOGGER.info("新增任务");
try {
QuartzJobModule quartzJobModule = new QuartzJobModule();
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 2020);
cal.set(Calendar.MONTH, 8);
cal.set(Calendar.DATE, 9);
cal.set(Calendar.HOUR_OF_DAY, 12);
cal.set(Calendar.MINUTE, 30);
cal.set(Calendar.SECOND, 00);
Date startDate = cal.getTime();// 任务开始日期为2020年9月9日12点30分
Calendar endCal = Calendar.getInstance();
endCal.set(Calendar.YEAR, 2020);
endCal.set(Calendar.MONTH, 8);
endCal.set(Calendar.DATE, 12);
endCal.set(Calendar.HOUR_OF_DAY, 12);
endCal.set(Calendar.MINUTE, 30);
endCal.set(Calendar.SECOND, 00);
Date endDate = endCal.getTime();// 任务结束日期为2020年9月12日12点30分
quartzJobModule.setStartTime(CronUtil.getStartDate(startDate));
quartzJobModule.setEndTime(CronUtil.getEndDate(endDate));
// 注意:在后面的任务中需要通过这个JobName来获取你要处理的数据,因此您可以讲这个设置为你要处理的数据的主键,比如id
quartzJobModule.setJobName("testJobId");
quartzJobModule.setTriggerName("tesTriggerNmae");
quartzJobModule.setJobGroupName(GloabalConstant.QZ_JOB_GROUP_NAME);
quartzJobModule.setTriggerGroupName(GloabalConstant.QZ_TRIGGER_GROUP_NAME);
String weeks = "1,2,3,5";// 该处模拟每周1,2,3,5执行任务
String cronExpression = CronUtil
.convertCronExpression(startDate,
endDate, weeks.split(","));
quartzJobModule.setCron(cronExpression);
quartzJobModule.setJobClass(TaskJobDetail.class);
quartzJobComponent.addJob(quartzJobModule);
}
catch (Exception e) {
e.printStackTrace();
return Result.ok();
}
return Result.ok();
}
@PostMapping("/edit")
@ResponseBody
public Result edit() {
LOGGER.info("编辑任务");
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 2020);
cal.set(Calendar.MONTH, 8);
cal.set(Calendar.DATE, 12);
cal.set(Calendar.HOUR_OF_DAY, 12);
cal.set(Calendar.MINUTE, 30);
cal.set(Calendar.SECOND, 00);
Date startDate = cal.getTime();// 任务开始日期为2020年9月12日12点30分
Calendar endCal = Calendar.getInstance();
endCal.set(Calendar.YEAR, 2020);
endCal.set(Calendar.MONTH, 8);
endCal.set(Calendar.DATE, 24);
endCal.set(Calendar.HOUR_OF_DAY, 12);
endCal.set(Calendar.MINUTE, 30);
endCal.set(Calendar.SECOND, 00);
Date endDate = endCal.getTime();// 任务结束日期为2020年9月24日12点30分
// "testJobId"为add方法添加的job的name
quartzJobComponent.modifyJobTime("testJobId", "/10 * * ? * *", startDate, endDate);
return Result.ok();
}
@PostMapping("/pause")
@ResponseBody
public Result pause(String jobName, String jobGroup) {
LOGGER.info("停止任务");
quartzJobComponent.pauseJob("testJobId");
return Result.ok();
}
@PostMapping("/resume")
@ResponseBody
public Result resume(String jobName, String jobGroup) {
LOGGER.info("恢复任务");
quartzJobComponent.removeJob("testJobId");
return Result.ok();
}
@PostMapping("/remove")
@ResponseBody
public Result remove(String jobName, String jobGroup) {
LOGGER.info("移除任务");
quartzJobComponent.removeJob("testJobId");
return Result.ok();
}
}
注意:
- 夲示例在添加job时,jobGroupName和triggerGroupName都是用的是上面常量类中的常量,您在实际使用中,必须保证添加时的jobGroupName和triggerGroupName和后面要进行暂停、停止等操作时的值完全一致,否则不会起作用
- 我们知道,cron表达式还是有一些鸡肋的,比如无法设置9:50-10:10分的定时任务,因此我们在设置代码时尽量规避这类任务(CronUtil.convertCronExpression的作用就是生成一个cron表达式),如果您认为和您的业务有差距,请换成您自己的逻辑即可
至此,所有的配置和代码都完成了,我们需要调用以下进行验证是否能够成功动态添加job,并且成功调起job
验证是否成功
访问http://localhost:8080/quartz/job/add 接口,此时数据库中的表中将会新增进入数据
同时我们稍等几秒,控制台上就会打印出如下内容:
2020-09-10 11:31:46.272 INFO 15308 --- [nio-8080-exec-1] c.example.demo.controller.JobController : 新增任务
2020-09-10 11:31:46.339 INFO 15308 --- [nio-8080-exec-1] org.quartz.core.QuartzScheduler : Scheduler quartzScheduler_$_NON_CLUSTERED started.
2020-09-10 11:32:26.086 INFO 15308 --- [_MisfireHandler] o.s.s.quartz.LocalDataSourceJobStore : Handling 1 trigger(s) that missed their scheduled fire-time.
2020-09-10 11:32:26.151 INFO 15308 --- [eduler_Worker-1] com.example.demo.qzComp.TaskJobDetail : begin delwith batch task >>>>>>>>>>>>>>>>>>>>>>>
2020-09-10 11:32:26.151 INFO 15308 --- [eduler_Worker-1] com.example.demo.qzComp.TaskJobDetail : 执行的任务id为:[testJobId]
2020-09-10 11:32:30.010 INFO 15308 --- [eduler_Worker-2] com.example.demo.qzComp.TaskJobDetail : begin delwith batch task >>>>>>>>>>>>>>>>>>>>>>>
2020-09-10 11:32:30.011 INFO 15308 --- [eduler_Worker-2] com.example.demo.qzComp.TaskJobDetail : 执行的任务id为:[testJobId]
2020-09-10 11:32:35.023 INFO 15308 --- [eduler_Worker-3] com.example.demo.qzComp.TaskJobDetail : begin delwith batch task >>>>>>>>>>>>>>>>>>>>>>>
2020-09-10 11:32:35.023 INFO 15308 --- [eduler_Worker-3] com.example.demo.qzComp.TaskJobDetail : 执行的任务id为:[testJobId]
说明程序调用成功,并且在任务处理类中成功获取到了jobName,可以继续进行后面的业务处理
其他操作不在验证,大家可以自己调用进行测试,现在把代码放出来:
github :https://github.com/wangmingweikong/spring-boot/tree/master/spring-boot-quartz