前言
>>>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表达式
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L C # |
月份 | 1-12 或者 JAN-DEC | , - * / |
星期 | 1-7 或者 SUN-SAT | , - * ? / L C # |
年 | (可选)留空 1970-2099 | , - * / |
每个符号的意义
* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值
/ 符号前表示开始时间,符号后表示每次递增的值;
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的第一天。
示例
表达式 | 含义 |
---|---|
*/5 * * * * ? | 每隔5秒执行一次 |
0 */1 * * * ? | 每隔1分钟执行一次 |
0 0 5-15 * * ? | 每天5-15点整点触发 |
0 0/3 * * * ? | 每三分钟触发一次 |
0 0-5 14 * * ? | 在每天下午2点到下午2:05期间的每1分钟触发 |
0 0/5 14 * * ? | 在每天下午2点到下午2:55期间的每5分钟触发 |
0 0/5 14,18 * * ? | 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 |
0 0/30 9-17 * * ? | 朝九晚五工作时间内每半小时 |
0 0 10,14,16 * * ? | 每天上午10点,下午2点,4点 |
0 0 12 ? * WED | 表示每个星期三中午12点 |
0 0 17 ? * TUES,THUR,SAT | 每周二、四、六下午五点 |
0 10,44 14 ? 3 WED | 每年三月的星期三的下午2:10和2:44触发 |
0 15 10 ? * MON-FRI | 周一至周五的上午10:15触发 |
0 0 23 L * ? | 每月最后一天23点执行一次 |
0 15 10 L * ? | 每月最后一日的上午10:15触发 |
0 15 10 ? * 6L | 每月的最后一个星期五上午10:15触发 |
0 15 10 * * ? 2005 | 2005年的每天上午10:15触发 |
0 15 10 ? * 6L 2002-2005 | 2002年至2005年的每月的最后一个星期五上午10:15触发 |
0 15 10 ? * 6#3 | 每月的第三个星期五上午10:15触发 |
30 * * * * ? | 每半分钟触发任务 |
30 10 * * * ? | 每小时的10分30秒触发任务 |
30 10 1 * * ? | 每天1点10分30秒触发任务 |
30 10 1 20 * ? | 每月20号1点10分30秒触发任务 |
30 10 1 20 10 ? * | 每年10月20号1点10分30秒触发任务 |
30 10 1 20 10 ? 2011 | 2011年10月20号1点10分30秒触发任务 |
30 10 1 ? 10 * 2011 | 2011年10月每天1点10分30秒触发任务 |
30 10 1 ? 10 SUN 2011 | 2011年10月每周日1点10分30秒触发任务 |
15,30,45 * * * * ? | 每15秒,30秒,45秒时触发任务 |
15-45 * * * * ? | 15到45秒内,每秒都触发任务 |
15/5 * * * * ? | 每分钟的每15秒开始触发,每隔5秒触发一次 |
15-30/5 * * * * ? | 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次 |
0 0/3 * * * ? | 每小时的第0分0秒开始,每三分钟触发一次 |
0 15 10 ? * MON-FRI | 星期一到星期五的10点15分0秒触发任务 |
0 15 10 L * ? | 每个月最后一天的10点15分0秒触发任务 |
0 15 10 LW * ? | 每个月最后一个工作日的10点15分0秒触发任务 |
0 15 10 ? * 5L | 每个月最后一个星期四的10点15分0秒触发任务 |
0 15 10 ? * 5#3 | 每个月第三周的星期四的10点15分0秒触发任务 |