目录
一、前言
二、quartz介绍
2.1 quartz概述
2.2 quartz优缺点
2.3 quartz核心概念
2.3.1 Scheduler
2.3.2 Trigger
2.3.3 Job
2.3.4 JobDetail
2.4 Quartz作业存储类型
2.5 适用场景
三、Cron表达式
3.1 Cron表达式语法
3.2 Cron表达式各元素说明
3.3 Cron表达式字符说明
四、环境准备
4.1 搭建步骤
4.1.1 拉取镜像
4.1.2 创建相关的数据目录
4.1.3 启动容器
五、springboot整合quartz
5.1 导入maven依赖
5.2 初始化数据表
5.3 增加配置文件
5.4 核心业务类
5.4.1 前端控制器类
5.4.2 业务实现类
5.4.3 mybatis-plus配置类
5.4.4 job执行任务类
5.5 效果测试
5.5.1 添加任务
5.5.2 暂停任务
5.6 补充说明
六、写在结尾
在项目开发中,经常需要定时任务来处理一些业务,可以说定时任务的场景无处不在,比如定时同步数据,定时清理数据等,在使用SpringBoot框架中,简单的场景,可以考虑使用内置的注解就可以使用,但是如果需要调度的场景比较复杂,或者说需要更灵活的配置调度,就需要考虑采用其他的方案了,比如Quartz , xxl-job,elastic job等。
Quartz 是 OpenSymphony 开源组织在 Job Scheduling 领域又一个开源项目,是完全由 Java 开发的一个开源任务日程管理系统,“任务进度管理器”就是一个在预先确定(被纳入日程)的时间到达时,负责执行(或者通知)其他软件组件的系统。Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中,它提供了巨大的灵活性而不牺牲简单性。
官网地址:Quartz Enterprise Job Scheduler
quartz具备如下优点:
Quartz 还支持开源,是一个功能丰富的开源作业调度库,可以集成到几乎任何 Java 应用程序中;
缺点:
在真正开始学习quartz之前,有必要对quartz中的几个核心业务概念做全面的了解。下面几个核心组件的关系如下:
Quartz 中的任务调度器,通过 Trigger 和 JobDetail 可以用来调度、暂停和删除任务,提供调度任务的主要API;
调度器就相当于一个容器,装载着任务和触发器,该类是一个接口,代表一个 Quartz 的独立运行容器,Trigger 和 JobDetail 可以注册到 Scheduler 中,两者在 Scheduler 中拥有各自的组及名称,组及名称是 Scheduler 查找定位容器中某一对象的依据,Trigger 的组及名称必须唯一,JobDetail 的组和名称也必须唯一(但可以和 Trigger 的组和名称相同,因为它们是不同类型的);
定义调度执行计划的组件,即什么时候触发执行,Trigger是Quartz 中的触发器,是一个类,描述触发 Job 执行的时间触发规则,主要有 SimpleTrigger 和 CronTrigger 这两个子类。
1)当且仅当需调度一次或者以固定时间间隔周期执行调度,SimpleTrigger 是最适合的选择;
2)CronTrigger 则可以通过 Cron 表达式定义出各种复杂时间规则的调度方案:如工作日周一到周五的 16:00 ~16:10 执行调度等;
Quartz 中具体的任务,包含了执行任务的具体方法,是一个接口,只定义一个方法 execute() 方法,在实现接口的 execute() 方法中编写所需要定时执行的 Job。
Quartz 中需要执行的任务详情,包括了任务的唯一标识和具体要执行的任务,可以通过 JobDataMap 往任务中传递数据。
RAMJobStore
RAM 也就是内存,默认情况下 Quartz 会将任务调度存储在内存中,这种方式性能是最好的,因为内存的速度是最快的。不好的地方就是数据缺乏持久性,但程序崩溃或者重新发布的时候,所有运行信息都会丢失
JDBC 作业存储
存到数据库之后,可以做单点也可以做集群,当任务多了之后,可以统一进行管理,随时停止、暂停、修改任务。关闭或者重启服务器,运行的信息都不会丢失。缺点就是运行速度快慢取决于连接数据库的快慢
在下面的场景中你可以考虑适用quartz
定时任务
Quartz最常见的应用场景是执行定时任务,比如每天凌晨生成报表、每小时执行数据备份等。它可以根据配置的时间表触发任务,并在预定的时间间隔内重复执行。
计划任务
除了定时任务,Quartz还支持基于日历的计划任务。例如,在特定的日期或周几执行某个任务,或者排除特定的日期和时间段。
分布式任务调度
Quartz可以与分布式系统集成,实现分布式任务调度。它提供了可靠的任务调度机制,能够确保在分布式环境中准确地调度和执行任务。
监控和管理
Quartz提供了监控和管理任务的功能。它可以获取任务的执行状态、日志和性能指标,并提供了对任务的管理接口,如启动、停止、暂停和恢复任务等。
Cron 表达式是任务调度的关键要素,简单来说,Cron 表达式是一个字符串,包括6~7个时间元素,每个元素都有特定的含义,在 Quartz中可以用于指定任务的执行时间。比如:0/10 * * * * ?
cron表达式中各个位置的元素代表的含义如下
Seconds | Minutes | Hours | DayofMonth | Month | DayofWeek |
---|---|---|---|---|---|
秒 | 分钟 | 小时 | 日期天/日 | 日期月份 | 星期 |
cron表达式中各个位置的元素的用法参考如下
时间元素 | 可出现的字符 | 有效数值范围 |
---|---|---|
Seconds | , - * / | 0-59 |
Minutes | , - * / | 0-59 |
Hours | , - * / | 0-23 |
DayofMonth | , - * / ? L W | 0-31 |
Month | , - * / | 1-12 |
DayofWeek | , - * / ? L # | 1-7或SUN-SAT |
字符 | 作用 | 举例 |
---|---|---|
, | 列出枚举值 | 在Minutes域使用5,10,表示在5分和10分各触发一次 |
- | 表示触发范围 | 在Minutes域使用5-10,表示从5分到10分钟每分钟触发一次 |
* | 匹配任意值 | 在Minutes域使用*, 表示每分钟都会触发一次 |
/ | 起始时间开始触发,每隔固定时间触发 一次 |
在Minutes域使用5/10,表示5分时触发一次,每10分钟再触发一次 |
? | 在DayofMonth和DayofWeek中,用于匹 配任意值 |
在DayofMonth域使用?,表示每天都触发一次 |
# | 在DayofMonth中,确定第几个星期几 | 1#3表示第三个星期日 |
L | 表示最后 | 在DayofWeek中使用5L,表示在最后一个星期四触发 |
W | 表示有效工作日(周一到周五) | 在DayofMonth使用5W,如果5日是星期六,则将在最近的工作日4日触发一次 |
在线 Cron 表达式生成器,其实 Cron 表达式无需多记,需要使用的时候直接使用在线生成器就可以了
地址1: 在线Cron表达式生成器
地址2: 在线Cron表达式生成器
在实际使用quartz时,任务调度相关的配置数据肯定是要入库的,即Quartz作业存储类型使用jdbc作业存储,所以需要准备mysql环境,下面使用docker快速搭建mysql环境。
docker pull mysql:5.7
mkdir -p /usr/local/mysql/data
mkdir -p /usr/local/mysql/logs
mkdir -p /usr/local/mysql/conf
docker run --name mysql -p 3306:3306 \
-v /usr/local/mysql/data:/var/lib/mysql \
-v /usr/local/mysql/logs:/logs \
-v /usr/local/mysql/conf:/etc/mysql/conf.d \
-e MYSQL_ROOT_PASSWORD=密码 -d mysql:5.7
网上关于quartz与springboot的整合方式有很多,但很多样例无法跑起来,请参选接下来的操作步骤,完成一个完整的整合。完整的工程目录如下:
以下为本例必须的依赖,可以根据自身的情况酌情添加
org.springframework.boot
spring-boot-starter-parent
2.5.2
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-quartz
org.projectlombok
lombok
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
3.4.1
cn.hutool
hutool-all
5.1.4
com.alibaba
fastjson
1.2.76
上面导入完成依赖jar包之后,在依赖的jar包中,找到与调度任务初始化的一个sql
拷贝上面的sql文件中的初始化sql,在mysql中创建一个数据库,这里命名为:quartz_db,然后执行即可,执行成功后,可以看到与任务调度相关的数据表就初始化了
在此基础上,再额外增加一张表,与我们可能有业务关联的信息整合
DROP TABLE IF EXISTS `sys_quartz_job`;
CREATE TABLE `sys_quartz_job` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`create_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`del_flag` int(1) NULL DEFAULT NULL COMMENT '删除状态',
`update_by` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`job_class_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '任务类名',
`cron_expression` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'cron表达式',
`parameter` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '参数',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`status` int(1) NULL DEFAULT NULL COMMENT '状态 0正常 -1停止',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;
配置application.yml文件
server:
port: 8088
spring:
## quartz定时任务,采用数据库方式
quartz:
job-store-type: jdbc
#json 时间戳统一转换
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
datasource:
url: jdbc:mysql://IP:3306/quartz_db?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&useSSL=false
username: root
password: 密码
driver-class-name: com.mysql.jdbc.Driver
jta:
atomikos:
properties:
recovery:
forget-orphaned-log-entries-delay:
#mybatis plus 设置
mybatis-plus:
mapper-locations: classpath:resources/mapper/*
global-config:
# 关闭MP3.0自带的banner
banner: false
db-config:
#主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
table-underline: true
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
#log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 返回类型为Map,显示null对应的字段
call-setters-on-nulls: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
下面列举出工程中核心的类文件,编写的顺序为从上至下,即从接口开始
该类主要提供与交互操作相关的API接口,后续可以通过界面的配置方式操作任务
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.congge.common.CommonConstant;
import com.congge.common.Result;
import com.congge.entity.QuartzJob;
import com.congge.exception.BizException;
import com.congge.service.QuartzJobService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/job")
public class QuartzController {
@Autowired
private QuartzJobService quartzJobService;
@Autowired
private Scheduler scheduler;
/**
* 分页列表查询
*
* @return
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Result> queryPageList(QuartzJob quartzJob, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
Page page = new Page(pageNo, pageSize);
IPage pageList = quartzJobService.page(page);
return Result.ok(pageList);
}
/**
* 添加定时任务
*
* @param quartzJob
* @return
*/
@PostMapping("/add")
public Result> add(@RequestBody QuartzJob quartzJob) {
List list = quartzJobService.list(new QueryWrapper().eq("job_class_name", quartzJob.getJobClassName()));
if (list != null && list.size() > 0) {
return Result.error("该定时任务类名已存在");
}
quartzJobService.saveAndScheduleJob(quartzJob);
return Result.ok("创建定时任务成功");
}
/**
* 更新定时任务
*
* @param quartzJob
* @return
*/
@PostMapping("/edit")
public Result> eidt(@RequestBody QuartzJob quartzJob) {
try {
quartzJobService.editAndScheduleJob(quartzJob);
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
return Result.error("更新定时任务失败!");
}
return Result.ok("更新定时任务成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@GetMapping("/delete")
public Result> delete(@RequestParam(name = "id", required = true) String id) {
QuartzJob quartzJob = quartzJobService.getById(id);
if (quartzJob == null) {
return Result.error("未找到对应实体");
}
quartzJobService.deleteAndStopJob(quartzJob);
return Result.ok("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE)
public Result> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
if (ids == null || "".equals(ids.trim())) {
return Result.error("参数不识别!");
}
for (String id : Arrays.asList(ids.split(","))) {
QuartzJob job = quartzJobService.getById(id);
quartzJobService.deleteAndStopJob(job);
}
return Result.ok("删除定时任务成功!");
}
/**
* 暂停定时任务
*
* @param jobClassName
* @return
*/
@GetMapping(value = "/pause")
public Result
接口控制器的业务逻辑实现
import com.congge.common.CommonConstant;
import com.congge.entity.QuartzJob;
import com.congge.exception.BizException;
import com.congge.mapper.QuartzJobMapper;
import com.congge.service.QuartzJobService;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class QuartzJobServiceImpl extends ServiceImpl implements QuartzJobService {
// @Autowired
// private QuartzJobMapper quartzJobMapper;
@Autowired
private Scheduler scheduler;
/**
* 保存&启动定时任务
*/
@Override
public boolean saveAndScheduleJob(QuartzJob quartzJob) {
if (CommonConstant.STATUS_NORMAL.equals(quartzJob.getStatus())) {
// 定时器添加
this.schedulerAdd(quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter());
}
// DB设置修改
return this.save(quartzJob);
}
/**
* 恢复定时任务
*/
@Override
public boolean resumeJob(QuartzJob quartzJob) {
schedulerDelete(quartzJob.getJobClassName().trim());
schedulerAdd(quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter());
quartzJob.setStatus(CommonConstant.STATUS_NORMAL);
return this.updateById(quartzJob);
}
@Override
public void test(String param) {
System.out.println("param====>"+param);
}
/**
* 编辑&启停定时任务
* @throws SchedulerException
*/
@Override
public boolean editAndScheduleJob(QuartzJob quartzJob) throws SchedulerException {
if (CommonConstant.STATUS_NORMAL.equals(quartzJob.getStatus())) {
schedulerDelete(quartzJob.getJobClassName().trim());
schedulerAdd(quartzJob.getJobClassName().trim(), quartzJob.getCronExpression().trim(), quartzJob.getParameter());
}else{
scheduler.pauseJob(JobKey.jobKey(quartzJob.getJobClassName().trim()));
}
return this.updateById(quartzJob);
}
/**
* 删除&停止删除定时任务
*/
@Override
public boolean deleteAndStopJob(QuartzJob job) {
schedulerDelete(job.getJobClassName().trim());
return this.removeById(job.getId());
}
/**
* 添加定时任务
*
* @param jobClassName
* @param cronExpression
* @param parameter
*/
private void schedulerAdd(String jobClassName, String cronExpression, String parameter) {
try {
// 启动调度器
scheduler.start();
// 构建job信息
JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(jobClassName).usingJobData("parameter", parameter).build();
// 表达式调度构建器(即任务执行的时间)
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName).withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
throw new BizException("创建定时任务失败", e);
} catch (RuntimeException e) {
throw new BizException(e.getMessage(), e);
}catch (Exception e) {
throw new BizException("后台找不到该类名:" + jobClassName, e);
}
}
/**
* 删除定时任务
*
* @param jobClassName
*/
private void schedulerDelete(String jobClassName) {
try {
/*使用给定的键暂停Trigger 。*/
scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName));
/*从调度程序中删除指示的Trigger */
scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName));
/*从 Scheduler 中删除已识别的Job - 以及任何关联的Trigger */
scheduler.deleteJob(JobKey.jobKey(jobClassName));
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new BizException("删除定时任务失败");
}
}
private static Job getClass(String classname) throws Exception {
Class> class1 = Class.forName(classname);
return (Job) class1.newInstance();
}
}
MybatisPlusConfig,配置分页信息
@Configuration
public class MybatisPlusConfig {
/**
* 分页
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 注册乐观锁 插件
return mybatisPlusInterceptor;
}
}
MyMetaObjectHandler,统一处理时间日期等字段信息
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
在实际开发中,具体执行各类业务时,需要根据需求场景编写任务执行类,下面提供两个测试使用的任务类
不带参数的任务
@Slf4j
public class SampleJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info(String.format("执行普通定时任务 SampleJob ! 当前时间:" + new Date()));
}
}
带任务参数
import com.congge.service.QuartzJobService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@Slf4j
public class SampleParamJob implements Job {
@Autowired
private QuartzJobService quartzJobService;
/**
* 若参数变量名修改 QuartzJobController中也需对应修改
*/
private String parameter;
public void setParameter(String parameter) {
this.parameter = parameter;
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info(String.format("准备执行带参数定时任务 SampleParamJob ! 时间:" + LocalDateTime.now(), this.parameter));
quartzJobService.test(this.parameter);
}
}
使用postman调用添加任务的接口
执行成功后,从控制台可以看到,任务已经在执行了
同时数据库也增加了一条数据
浏览器调用接口:localhost:8088/job/pause?jobClassName=com.congge.job.SampleJob
调用成功后,可以从控制台看到任务已经停止执行了
其他的测试用例可以参照上面的方式继续测试即可。
基于上面的整合和效果验证,下面留待几个问题请进一步思考和探究
问题1:
如何让任务添加成功后延迟一段时间执行?
问题2:
如何解决任务的并发执行?
问题3:
两次执行任务的时间间隔太短怎么办?
相对其他的分布式任务调度框架,quartz整体来说学习成本不算高,而且适用的场景也比较多,在项目中合理的引用,可以灵活的解决很多问题。