SpringBoot 集成Quartz发布、修改、暂停、删除定时任务

631 阅读8分钟

前言

SpringBoot自带schedule

SpringBoot本身支持表达式等多种定时任务,使用起来也很方便,但是如果使用复杂的任务操作时,SpringBoot自带的稍显不足,使用SpringBoot自带的定时任务, 只需要在程序启动的时候加上@EnableScheduling

@Scheduled(cron="0/20 * * * * ?")
public void task(){
 System.out.println("task - 20秒执行一次");
}

使用十分简单,本文不过多陈述

为什么使用Quartz

多任务情况下,quartz更容易管理,可以实现动态配置 ,可随时删除和修改定时任务,方便使用

1、SpringBoot集成Quartz

项目目录:

由于一些quartz集成需要导入quartz自带的一些mysql库,使用起来稍显负复杂,本文采用自己创建任务库来管理简单的定时任务

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <parent>
 <artifactId>learn</artifactId>
 <groupId>com.lss</groupId>
 <version>1.0</version>
 </parent>
 <modelVersion>4.0.0</modelVersion>
 <properties>
 <quertz.version>2.2.1</quertz.version>
 </properties>
 <artifactId>quartz</artifactId>
 <dependencies>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter</artifactId>
 </dependency>
 <dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 </dependency>
 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-jpa</artifactId>
 </dependency>
 <dependency>
 <groupId>org.quartz-scheduler</groupId>
 <artifactId>quartz</artifactId>
 <version>${quertz.version}</version>
 </dependency>
 </dependencies>
 <build>
 <plugins>
 <plugin>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-maven-plugin</artifactId>
 </plugin>
 </plugins>
 </build>
</project>

application.yml

spring:
 datasource:
 url: jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=utf8
 username: root
 password: 12345678
 driverClassName: com.mysql.jdbc.Driver
 jpa:
 database: mysql
 show-sql: true
 hibernate:
 ddl-auto: update
 naming:
 physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
 database-platform: org.hibernate.dialect.MySQL5Dialect
 quartz:
 #相关属性配置
 properties:
 org:
 quartz:
 # dataSource:
 # default:
 # driver: com.mysql.jdbc.Driver
 # URL: jdbc:mysql://localhost:3306/jobconfig?useUnicode=true&characterEncoding=utf8
 # user: root
 # password: 12345678
 # maxConnections: 5
 scheduler:
 instanceName: DefaultQuartzScheduler
 instanceId: AUTO
 jobStore:
 class: org.quartz.impl.jdbcjobstore.JobStoreTX
 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
 tablePrefix: qrtz_
 isClustered: false
 clusterCheckinInterval: 10000
 useProperties: true
 threadPool:
 class: org.quartz.simpl.SimpleThreadPool
 threadCount: 10
 threadPriority: 5
 threadsInheritContextClassLoaderOfInitializingThread: true
 #数据库方式
 job-store-type: JDBC
 #初始化表结构
 jdbc:
 initialize-schema: NEVER

2、项目配置

2.1、新建ScheduleQuartzJob类,实现quartz的Job接口

package com.lss.job;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
@Slf4j
public class ScheduleQuartzJob implements Job {
 @Override
 public void execute(JobExecutionContext context) throws JobExecutionException {
 String group = context.getJobDetail().getJobDataMap().get("group").toString();
 String name = context.getJobDetail().getJobDataMap().get("name").toString();
 log.info("执行了task...group:{}, name:{}", group, name);
 // 可在此执行定时任务的具体业务
 // ...
 }
}

2.2、新建ScheduleJobPo类,对应数据库

package com.lss.entity.po;
import lombok.Data;
import javax.persistence.*;
@Data
@Table(name = "tbl_schedule_job")
@Entity
public class ScheduleJobPo {
 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 private Integer id;
 
 // 任务group名称
 @Column(name = "group_name")
 private String groupName;
 // 任务job名称
 @Column(name = "job_name")
 private String jobName;
 // cron表达式
 private String cron;
 // 0 - 代表正在执行 1 - 已删除 2 - 暂停
 @Column(name = "status")
 private Integer status;
 @Column(name = "create_time")
 private Long createTime;
 @Column(name = "modified_time")
 private Long modifiedTime;
}

2.3、新建ScheduleJobDaoRepository类,调用数据库

package com.lss.dao;
import com.lss.entity.po.ScheduleJobPo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ScheduleJobDaoRepository extends JpaRepository<ScheduleJobPo, Integer>, JpaSpecificationExecutor<ScheduleJobPo> {
 public ScheduleJobPo findByIdAndStatus(Integer id, Integer status);
 public List<ScheduleJobPo> findAllByStatus(Integer status);
 public List<ScheduleJobPo> findByGroupNameAndJobNameAndStatus(String groupName, String jobName, Integer status);
 public List<ScheduleJobPo> findAllByStatusInOrderByCreateTimeDesc(List<Integer> statusList);
}

2.4、新建ScheduleJobService类,业务实现层

package com.lss.service;
import com.lss.dao.ScheduleJobDaoRepository;
import com.lss.entity.model.ScheduleJobModel;
import com.lss.entity.po.ScheduleJobPo;
import com.lss.job.ScheduleQuartzJob;
import com.lss.util.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Set;
@Service
@Slf4j
public class ScheduleJobService {
 // 获取工厂类
 private StdSchedulerFactory sf = new StdSchedulerFactory();
 @Autowired
 private ScheduleJobDaoRepository scheduleJobDaoRepository;
 // 项目重启后,初始化原本已经运行的定时任务
 @PostConstruct
 public void init(){
 List<ScheduleJobPo> poList = scheduleJobDaoRepository.findAllByStatus(0);
 poList.forEach(po -> {
 startScheduleByInit(po);
 });
 }
 /**
 * 初始化时开启定时任务
 */
 private void startScheduleByInit(ScheduleJobPo po){
 try {
 Scheduler scheduler = sf.getScheduler();
 startJob(scheduler, po.getGroupName(), po.getJobName(), po.getCron());
 scheduler.start();
 }catch (Exception e){
 log.error("exception:{}", e);
 }
 }
 /**
 * 开启定时任务
 * @param model
 */
 public void startSchedule(ScheduleJobModel model) {
 if (StringUtils.isEmpty(model.getGroupName()) || StringUtils.isEmpty(model.getJobName()) || StringUtils.isEmpty(model.getCron())){
 throw new RuntimeException("参数不能为空");
 }
 List<ScheduleJobPo> poList = scheduleJobDaoRepository.findByGroupNameAndJobNameAndStatus(model.getGroupName(), model.getJobName(), 0);
 if (!ObjectUtils.isEmpty(poList)){
 throw new RuntimeException("group和job名称已存在");
 }
 try {
 Scheduler scheduler = sf.getScheduler();
 startJob(scheduler, model.getGroupName(), model.getJobName(), model.getCron());
 scheduler.start();
 ScheduleJobPo scheduleJobPo = new ScheduleJobPo();
 scheduleJobPo.setGroupName(model.getGroupName());
 scheduleJobPo.setJobName(model.getJobName());
 scheduleJobPo.setCron(model.getCron());
 scheduleJobPo.setStatus(0);
 scheduleJobPo.setCreateTime(DateUtil.getCurrentTimeStamp());
 scheduleJobPo.setModifiedTime(DateUtil.getCurrentTimeStamp());
 scheduleJobDaoRepository.save(scheduleJobPo);
 }catch (Exception e){
 log.error("exception:{}", e);
 }
 }
 /**
 * 更新定时任务
 * @param model
 */
 public void scheduleUpdateCorn(ScheduleJobModel model) {
 if (ObjectUtils.isEmpty(model.getId()) || ObjectUtils.isEmpty(model.getCron())){
 throw new RuntimeException("定时任务不存在");
 }
 try {
 ScheduleJobPo po = scheduleJobDaoRepository.findByIdAndStatus(model.getId(), 0);
 // 获取调度对象
 Scheduler scheduler = sf.getScheduler();
 // 获取触发器
 TriggerKey triggerKey = new TriggerKey(po.getJobName(), po.getGroupName());
 CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
 String oldTime = cronTrigger.getCronExpression();
 if (!oldTime.equalsIgnoreCase(model.getCron())) {
 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(model.getCron());
 CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(po.getJobName(), po.getGroupName())
 .withSchedule(cronScheduleBuilder).build();
 // 更新定时任务
 scheduler.rescheduleJob(triggerKey, trigger);
 po.setCron(model.getCron());
 // 更新数据库
 scheduleJobDaoRepository.save(po);
 }
 }catch (Exception e){
 log.info("exception:{}", e);
 }
 }
 /**
 * 任务 - 暂停
 */
 public void schedulePause(ScheduleJobModel model) {
 if (ObjectUtils.isEmpty(model.getId())){
 throw new RuntimeException("定时任务不存在");
 }
 ScheduleJobPo po = scheduleJobDaoRepository.findByIdAndStatus(model.getId(), 0);
 if (ObjectUtils.isEmpty(po)){
 throw new RuntimeException("定时任务不存在");
 }
 try {
 Scheduler scheduler = sf.getScheduler();
 JobKey jobKey = new JobKey(po.getJobName(), po.getGroupName());
 JobDetail jobDetail = scheduler.getJobDetail(jobKey);
 if (jobDetail == null)
 return;
 scheduler.pauseJob(jobKey);
 po.setStatus(2);
 scheduleJobDaoRepository.save(po);
 }catch (Exception e){
 log.error("exception:{}", e);
 }
 }
 /**
 * 任务 - 恢复
 */
 public void scheduleResume(ScheduleJobModel model) {
 if (ObjectUtils.isEmpty(model.getId())){
 throw new RuntimeException("定时任务不存在");
 }
 ScheduleJobPo po = scheduleJobDaoRepository.findByIdAndStatus(model.getId(), 2);
 if (ObjectUtils.isEmpty(po)){
 throw new RuntimeException("定时任务不存在");
 }
 try {
 Scheduler scheduler = sf.getScheduler();
 JobKey jobKey = new JobKey(po.getJobName(), po.getGroupName());
 JobDetail jobDetail = scheduler.getJobDetail(jobKey);
 if (jobDetail == null)
 return;
 scheduler.resumeJob(jobKey);
 po.setStatus(0);
 scheduleJobDaoRepository.save(po);
 }catch (Exception e){
 log.error("exception:{}", e);
 }
 }
 /**
 * 任务 - 删除一个定时任务
 */
 public void scheduleDelete(ScheduleJobModel model) {
 if (ObjectUtils.isEmpty(model.getId())){
 throw new RuntimeException("定时任务不存在");
 }
 ScheduleJobPo po = scheduleJobDaoRepository.findByIdAndStatus(model.getId(), 0);
 if (ObjectUtils.isEmpty(po)){
 throw new RuntimeException("定时任务不存在");
 }
 try {
 Scheduler scheduler = sf.getScheduler();
 JobKey jobKey = new JobKey(po.getJobName(), po.getGroupName());
 JobDetail jobDetail = scheduler.getJobDetail(jobKey);
 if (jobDetail == null)
 return;
 scheduler.deleteJob(jobKey);
 po.setStatus(1);
 scheduleJobDaoRepository.save(po);
 }catch (Exception e){
 log.error("exception:{}", e);
 }
 }
 /**
 * 删除所有定时任务
 */
 public void scheduleDeleteAll() {
 try {
 Scheduler scheduler = sf.getScheduler();
 // 获取有所的组
 List<String> jobGroupNameList = scheduler.getJobGroupNames();
 for (String jobGroupName : jobGroupNameList) {
 GroupMatcher<JobKey> jobKeyGroupMatcher = GroupMatcher.jobGroupEquals(jobGroupName);
 Set<JobKey> jobKeySet = scheduler.getJobKeys(jobKeyGroupMatcher);
 for (JobKey jobKey : jobKeySet) {
 String jobName = jobKey.getName();
 JobDetail jobDetail = scheduler.getJobDetail(jobKey);
 if (jobDetail == null)
 return;
 scheduler.deleteJob(jobKey);
 // 更新数据库
 List<ScheduleJobPo> poList = scheduleJobDaoRepository.findByGroupNameAndJobNameAndStatus(jobGroupName, jobName, 0);
 poList.forEach(po -> {
 po.setStatus(1);
 scheduleJobDaoRepository.save(po);
 });
 log.info("group:{}, job:{}", jobGroupName, jobName);
 }
 }
 }catch (Exception e){
 log.error("exception:{}", e);
 }
 }
 // 开启任务
 private void startJob(Scheduler scheduler, String group, String name, String cron) throws SchedulerException {
 // 通过JobBuilder构建JobDetail实例,JobDetail规定只能是实现Job接口的实例
 // 在map中可传入自定义参数,在job中使用
 JobDataMap map = new JobDataMap();
 map.put("group", group);
 map.put("name", name);
 // JobDetail 是具体Job实例
 JobDetail jobDetail = JobBuilder.newJob(ScheduleQuartzJob.class).withIdentity(name, group)
 .usingJobData(map)
 .build();
 // 基于表达式构建触发器
 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
 // CronTrigger表达式触发器 继承于Trigger
 // TriggerBuilder 用于构建触发器实例
 CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(name, group)
 .withSchedule(cronScheduleBuilder).build();
 scheduler.scheduleJob(jobDetail, cronTrigger);
 }
}

2.5、新建ScheduleJobModel类,参数调用

package com.lss.entity.model;
import lombok.Data;
@Data
public class ScheduleJobModel {
 private Integer id;
 private String groupName;
 private String jobName;
 private String cron;
}

2.6、新建TestController类,暴露restful接口,实现接口调用

package com.lss.controller;
import com.lss.entity.model.ScheduleJobModel;
import com.lss.service.ScheduleJobService;
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(value = "/api")
public class TestController {
 @Autowired
 private ScheduleJobService scheduleJobService;
 /**
 * 开启
 * @param model
 * @return
 */
 @PostMapping("start")
 public String startSchedule(@RequestBody ScheduleJobModel model){
 scheduleJobService.startSchedule(model);
 return "ok";
 }
 /**
 * 更新
 * @param model
 * @return
 */
 @PostMapping("update")
 public String scheduleUpdateCorn(@RequestBody ScheduleJobModel model){
 scheduleJobService.scheduleUpdateCorn(model);
 return "ok";
 }
 /**
 * 暂停
 * @param model
 * @return
 */
 @PostMapping("/pause")
 public String schedulePause(@RequestBody ScheduleJobModel model){
 scheduleJobService.schedulePause(model);
 return "ok";
 }
 /**
 * 恢复
 * @param model
 * @return
 */
 @PostMapping("/resume")
 public String scheduleResume(@RequestBody ScheduleJobModel model){
 scheduleJobService.scheduleResume(model);
 return "ok";
 }
 /**
 * 删除一个定时任务
 * @param model
 * @return
 */
 @PostMapping("/delete")
 public String scheduleDelete(@RequestBody ScheduleJobModel model){
 scheduleJobService.scheduleDelete(model);
 return "ok";
 }
 /**
 * 删除所有定时任务
 * @param model
 * @return
 */
 @PostMapping("deleteAll")
 public String scheduleDeleteAll(@RequestBody ScheduleJobModel model){
 scheduleJobService.scheduleDeleteAll();
 return "ok";
 }
}

2.7、新建DateUtil类,获取当前日期

package com.lss.util;
public class DateUtil {
 /**
 * 得到当前时间戳
 * @return
 */
 public static Long getCurrentTimeStamp() {
 long timeMillis = System.currentTimeMillis();
 return timeMillis;
 }
}

以上是该项目的所有配置

3、启动项目

3.1、调用 /api/start 接口

curl -H "Content-Type:application/json" -X POST --data '{"groupName":"group","jobName":"job","cron":"0/5 * * * * ?"}' http://localhost:8080/api/start

看控制台日志,发现每隔5秒调用一次定时服务

3.2、调用 /api/update 接口

curl -H "Content-Type:application/json" -X POST --data '{"id":1, "cron":"0/8 * * * * ?"}' http://localhost:8080/api/update

看控制台日志,发现每隔8秒调用一次定时服务

这个接口实现是先调用的数据库,然后去quartz中更新了该定时任务,也可直接传入groupName和jobName去quartz中查询出来对应的定时任务,更新后再去更新数据库后一种方法更好,实现也很容易实现,学习的朋友们可参照接口 /api/deleteAll 中的一些业务操作实现该业务,本文不在过多陈述

3.3、调用 /api/pause 接口

curl -H "Content-Type:application/json" -X POST --data '{"id":1}' http://localhost:8080/api/pause

看控制台日志,发现定时任务已停止

3.4、调用 /api/resume 接口

curl -H "Content-Type:application/json" -X POST --data '{"id":1}' http://localhost:8080/api/resume

看控制台日志,发现定时任务已恢复

3.5、调用 /api/delete 接口

curl -H "Content-Type:application/json" -X POST --data '{"id":1}' http://localhost:8080/api/delete

发现定时任务已删除

3.6、调用 /api/deleteAll 接口

curl -H "Content-Type:application/json" -X POST --data '{}' http://localhost:8080/api/deleteAll

此时删除所有的定时任务

附录一些cron表达式,方便记忆

cron表达式

每个符号的意义

  • 表示所有值;

? 表示未说明的值,即不关心它为何值;

  • 表示一个指定的范围;

, 表示附加一个可能值

/ 符号前表示开始时间,符号后表示每次递增的值;

L("last") ("last") "L" 用在day-of-month字段意思是 "这个月最后一天";用在 day-of-week字段, 它简单意思是 "7" or "SAT"。 如果在day-of-week字段里和数字联合使用,它的意思就是 "这个月的最后一个星期几" – 例如: "6L" means "这个月的最后一个星期五". 当我们用“L”时,不指明一个列表值或者范围是很重要的,不然的话,我们会得到一些意想不到的结果。

W("weekday") 只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个 月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第 16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在 day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日。

只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。

C 指和calendar联系后计算过的值。例:在day-of-month 字段用“5C”指在这个月第5天或之后包括calendar的第一天;在day-of-week字段用“1C”指在这周日或之后包括calendar的第一天。

示例