1、场景
??1.1 发布消息、问卷等信息时,发布者可以指定星期、月份的具体时间进行定时发布(cron 触发器)
??1.2 设置当天或指定日期的时间范围内,指定时间间隔执行任务。(目前该功能未研究,可参考如下说明即可实现。)
??1.3 其他定时功能可根据不同的任务触发器进行实现。(未研究)
2、定时任务框架(Quartz )
2.1 选型
??简单地使用注解方式的定时任务已无法满足上述场景需求。网上也找了很多定时框架,如 cn.hutool(Java 工具类库)、XXL-JOB,hutool 研究一下感觉无法满足,且代码写起来不够简洁,就放弃了。XXL-JOB 看着介绍起来烦琐就不研究了,最后看了一下 Quartz 介绍和相关博客,要用上很多 Quartz 相关的表,但我不需要这些表,一开始打算放弃的,然后查看了Springboot 整合Quartz 后,灵感突然来了。嗯~不需要关联的表,自己创建业务表即可。嗯!感觉可以。
2.2 Quartz 介绍
?? Quartz 定时任务据我了解可分为Trigger(触发器)、Job(任务)和Scheduler(调度器),定时任务的逻辑大体为:创建触发器和任务,并将其加入到调度器中,如下图所示:
??Trigger 有五种触发器:
- SimpleTrigger 触发器:需要在特定的日期/时间启动,且以指定的间隔时间(单位毫秒)重复执行 n 次任务,如 :在 9:00 开始,每隔1小时,每隔几分钟,每隔几秒钟执行一次 。没办法指定每隔一个月执行一次(每月的时间间隔不是固定值)。
- CalendarIntervalTrigger 触发器:指定从某一个时间开始,以一定的时间间隔(单位有秒,分钟,小时,天,月,年,星期)执行的任务。
- DailyTimeIntervalTrigger 触发器:指定每天的某个时间段内,以一定的时间间隔执行任务。并且支持指定星期。如:指定每天 9:00 至 18:00 ,每隔 70 秒执行一次,并且只要周一至周五执行。
- CronTrigger 触发器:基于日历的任务调度器,即指定星期、日期的某时间执行任务。
- NthIncludedDayTrigger 触发器:不同时间间隔的第 n 天执行任务。比如,在每个月的第 15 日处理财务发票记帐,同样设定双休日或者假期。
说明:2 中介绍只了解皮毛,研究了怎么用,如何实现想要功能而已,有不同意见可评论,勿喷。
3、实现
?? 基于场景 1.1,使用 CronTrigger 触发器最合适。我想的实现逻辑如下:创建一张定时任务的表来存储动态生成的 cron 表达式,接着创建触发器、任务,再启动调度器。程序在重启时,读取定时任务表,将之前生成的任务重新调度起来。以定时发布问卷为例。
3.1 定时任务表
CREATE TABLE `que_task_info` (
`task_id` varchar(60) NOT NULL COMMENT '定时任务ID(触发器名)',
`type` int(1) NOT NULL COMMENT '作业类型: 1/每天; 2/每周; 3/每月',
`minute` int(2) DEFAULT 0 COMMENT '指定分钟',
`hour` int(2) DEFAULT 0 COMMENT '指定小时',
`last_day_of_month` int(1) DEFAULT NULL COMMENT '指定一个月的最后一天:0/不指定;1/指定',
`week_days` varchar(20) DEFAULT NULL COMMENT '指定一周哪几天,用英文‘,’隔开:1/星期天; 2/...3/.. ; 7/星期六',
`month_days` varchar(100) DEFAULT NULL COMMENT '指定一个月的哪几天,用英文‘,’隔开',
`cron` varchar(255) NOT NULL COMMENT '定时任务表达式',
`start_time` datetime NOT NULL COMMENT '定时任务开始时间',
`end_time` datetime NOT NULL COMMENT '定时任务结束时间',
`trigger_group` varchar(60) NOT NULL COMMENT '定时任务触发器所在组名(问卷ID)',
`job_name` varchar(60) NOT NULL COMMENT '定时任务名称',
`status` int(1) NOT NULL DEFAULT '1' COMMENT '状态:0/过期或未执行;1/执行中',
PRIMARY KEY (`task_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='问卷定时任务表'
3.2 pom.xml
??添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
3.3 任务调度器类
??封装一个自己的任务调度器类,含有创建触发器、更新触发器、移除触发器、创建任务、启动调度器功能方法。
import cn.hutool.core.date.DateUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
@AllArgsConstructor
public class TaskScheduler {
private final Scheduler scheduler;
private final static String CORN_RESPONSE_TIME_ERR = "will never fire";
private final static String DEFAULT_TIME_ERR_MSG = "截止时间必须大于其中最小的有效指定定期时间";
private final static String ERR_MSG_OF_LAST_MONTH_DAY = "截止时间不可小于月末时间";
public void startScheduler(JobDetail jobDetail, Trigger trigger, String cron) {
try {
String name = jobDetail.getKey().getName();
String group = jobDetail.getKey().getGroup();
JobDetail schedulerJobDetail = scheduler.getJobDetail(new JobKey(name, group));
scheduler.scheduleJob(jobDetail, trigger);
if(schedulerJobDetail == null) {
scheduler.start();
}
log.info("启动调度器成功");
}
catch (Exception e) {
log.error(e.toString());
String message = e.getMessage();
if(StringUtils.isNotBlank(message) && message.contains(CORN_RESPONSE_TIME_ERR)) {
if(cron.contains("L")) {
throw new ServiceException(ERR_MSG_OF_LAST_MONTH_DAY);
}
throw new ServiceException(DEFAULT_TIME_ERR_MSG);
}
throw new ServiceException("启动调度器失败");
}
}
public <T extends Job> JobDetail getJob(String jobName, String jobGroup, Class<T> cla) {
try {
JobDetail jobDetail = scheduler.getJobDetail(new JobKey(jobName, jobGroup));
if(jobDetail != null) {
return jobDetail;
}
return JobBuilder.newJob(cla).withIdentity(jobName, jobGroup).build();
}
catch (Exception e) {
throw new ServiceException("获取任务失败");
}
}
public Trigger buildCronTrigger(String triggerName, String triggerGroup, Date startTime, Date endTime, String cron) {
return TriggerBuilder.newTrigger()
.withIdentity(triggerName, triggerGroup)
.startAt(DateUtil.parseDateTime(TimeUtil.dateTimeFormat(startTime)))
.endAt(DateUtil.parseDateTime(TimeUtil.dateTimeFormat(endTime)))
.withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing())
.build();
}
public Boolean updatedCronTrigger(String triggerName, String triggerGroup, Date startTime, Date endTime, String cron) {
TriggerKey oldTrigger = new TriggerKey(triggerName, triggerGroup);
Trigger newTrigger = buildCronTrigger(triggerName, triggerGroup, startTime, endTime, cron);
try {
scheduler.rescheduleJob(oldTrigger, newTrigger);
log.info("更新触发器成功");
return true;
}
catch (Exception e) {
log.error(e.toString());
String message = e.getMessage();
if(StringUtils.isNotBlank(message) && message.contains(CORN_RESPONSE_TIME_ERR)) {
if(cron.contains("L")) {
throw new ServiceException(ERR_MSG_OF_LAST_MONTH_DAY);
}
throw new ServiceException(DEFAULT_TIME_ERR_MSG);
}
throw new ServiceException("更新 cron 触发器失败");
}
}
public Boolean removeTrigger(String triggerName, String triggerGroup) {
try {
TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroup);
scheduler.pauseTrigger(triggerKey);
boolean flag = scheduler.unscheduleJob(triggerKey);
log.info("移除触发器成功");
return flag;
}
catch (Exception e) {
throw new ServiceException("移除触发器失败");
}
}
}
3.4 任务类
??定时任务执行的主体,实现 Job 接口
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.transaction.annotation.Transactional;
public class QueReleaseJob implements Job {
@Override
@Transactional(rollbackFor = Exception.class)
public void execute(JobExecutionContext context) {
}
}
3.5 cron 工具类
??封装一个属于场景 1.1 的 cron 表达式生成工具类。
import org.apache.commons.lang3.StringUtils;
import java.util.List;
public class CronUtil {
private static final int DAY_JOB_TYPE = 1;
private static final int WEEK_JOB_TYPE = 2;
private static final int MONTH_JOB_TYPE = 3;
public static String createCronExpression(Integer jobType, Integer minute, Integer hour, Integer lastDayOfMonth, List<Integer> weekDays, List<Integer> monthDays){
StringBuilder cronExp = new StringBuilder();
cronExp.append("0 ");
cronExp.append(minute == null ? "0" : minute).append(" ");
cronExp.append(hour == null ? "0" : hour).append(" ");
if(jobType == DAY_JOB_TYPE){
cronExp.append("* ");
cronExp.append("* ");
cronExp.append("?");
}
else if(lastDayOfMonth != null && lastDayOfMonth == 1) {
cronExp.append("L ");
cronExp.append("* ");
cronExp.append("?");
}
else if(weekDays != null && jobType == WEEK_JOB_TYPE){
cronExp.append("? ");
cronExp.append("* ");
cronExp.append(StringUtils.join(weekDays, ","));
}
else if(monthDays != null && jobType == MONTH_JOB_TYPE){
cronExp.append(StringUtils.join(monthDays, ",")).append(" ");
cronExp.append("* ");
cronExp.append("?");
}
else {
cronExp.append("* ").append("* ").append("?");
}
return cronExp.toString();
}
}
说明:cron 表达式规则可自行查阅
3.6 前端请求的 JSON 格式
3.7 创建触发器、任务,启动调度器
3.8 自定义定时任务配置类
3.8 重启项目时启动定时任务
4、效果
附
持续更新中,急需可留言。
|