一、背景
在平常的开发过程中,大家对定时任务肯定都不陌生,比如每天0点自动导出下用户数据,每天早上8点自动发送一封系统的邮件等等。简单的定时任务使用spring自带的 @Scheduled实现即可。但是对于一些特殊的场景下,比如我们想在不重启项目的情况下,动态的修改定时器的运行间隔,将原来每30分钟执行一次的定时任务,动态改为5分钟执行一次,这时候Spring自带的定时器就不太方便了。
二、引入Quartz
“工欲善其事,必先利其器”——Quartz就是我们解决这个问题的利器, Quartz是Apache下开源的一款功能强大的定时任务框架。Quartz官网 对它的描述:
Quartz是一个功能丰富的开源作业调度库,可以集成到几乎任何Java应用程序中——从最小的独立应用程序到最大的电子商务系统。Quartz可用于创建简单或复杂的调度,以执行数万、数百甚至数万个作业;任务被定义为标准Java组件的作业,这些组件可以执行几乎任何您可以编程让它们执行的任务。Quartz调度器包含许多企业级特性,比如对JTA事务和集群的支持。
在使用Quartz之前,我们要先了解下Quartz的中核心组成:
-
Job 任务,要执行的具体内容,我们自定义的任务类都要实现此Job接口,Job接口中只有一个方法,我们的业务逻辑就在此方法编写,Job的源码如下:
-
JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略
-
Trigger 触发器,用于定义Job任务何时被触发,以何种方式触发。Trigger接口的关系图如下,最常用的是CronTrigger
-
Scheduler 调度容器,Scheduler负责将Job和Trigger关联起来,统一进行任务调度,一个Scheduler中可以注册多个 Job 和 Trigger。
三、SpringBoot集成Quartz
接下来我们使用SpringBoot 2.2.0.RELEASE,集成Quartz 2.3.0版本,来创建一个简单的demo。
1.添加maven依赖
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
2.创建Job任务
此处创建了PrintTimeJob 类并实现Job接口,任务很简单——打印开始时间,然后休眠5s,打印结束时间。
package com.example.demo.quartz;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PrintTimeJob implements Job {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
String format = sdf.format(new Date());
System.out.println(Thread.currentThread().getName()+" 任务开始>>>>>>>>>>>>>>现在时间是:"+format);
//模拟任务执行耗时5s
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 任务结束!现在时间是:"+sdf.format(new Date()));
}
}
3.创建CronTrigger与scheduler
在定义过Job类之后,我们就可以通过创建CronTrigger与scheduler来执行定时任务了,为了简单,代码都写在了main方法里
package com.example.demo.quartz;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.spi.MutableTrigger;
import java.util.Date;
public class QuartzTest {
public static void main(String[] args) throws SchedulerException {
//1.创建一个jobDetail,并与PrintTimeJob绑定,Job任务的name为printJob
JobDetail jobDetail = JobBuilder.newJob(PrintTimeJob.class).withIdentity("printJob").build();
//2.使用cron表达式,构建CronScheduleBuilder
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
//使用TriggerBuilder构建cronTrigger,并指定了Trigger的name和group
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("job1", "group1")
.withSchedule(cronScheduleBuilder).build();
//3.创建scheduler
StdSchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();
//4.将job和cronTrigger加入scheduler进行管理
scheduler.scheduleJob(jobDetail,cronTrigger);
//5.开启调度
scheduler.start();
}
}
执行main方法,可以看到控制台输出,如下图:
四、如何动态修改定时任务间隔
通过上面的代码我们知道了如何通过Quartz实现定时任务,那么关键的问题来了——如何动态的修改任务间隔?这个问题其实可以分解为三小个问题:
-
如何将scheduler变为spring容器管理?
在SpringBoot中很容易实现,使用 @Bean或者 @Autowired注入Scheduler 即可。 -
如何重新设置Job任务的Trigger?
既然scheduler是任务的调度器,那么我们自然想到scheduler中是否有相关API,果然发现了scheduler中的rescheduleJob(TriggerKey key, Trigger trigger)
方法,从方法名很明显看出,这是在重新设置任务的触发器,我们修改任务的时间间隔,就是在重新设置的触发器。而TriggerKey
是目标任务的标识。包含任务名字和分组名,这个在上面demo中我们设置过。 -
如何在项目启动后,就开始任务调度?
这个问题等同于 “如何监听springboot应用启动成功事件?”,翻阅资料发现,自Spring框架3.0版本后,自带了ApplicationListener
接口,允许我们通过实现此接口监听spring框架中的的ApplicationEvent
,ApplicationListener接口的源码如下:
ApplicationListener
使用了观察者模式,实现该接口的类,会作为观察者,当特定的ApplicationEvent
被触发时,spring框架反射动调用onApplicationEvent
方法,更多的说明详见官网说明ApplicationListener
而ApplicationEvent
就是要监听的事件,查看源码发现其有很多实现类,而其中的SpringApplicationEvent
下的ApplicationReadyEvent
就是我们想要的监听的事件。
关于ApplicationEvent 更多说明见官网链接ApplicationEvent
到了这里,三个问题都解决了,思路清晰了,编起代码来,就很快了。
五、动态修改定时任务间隔
1.创建QuartzUtil工具类
package com.example.demo.util;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
@Component
public class QuartzUtil {
/**
* 注入Scheduler
*/
@Autowired
private Scheduler scheduler;
/**
* 创建CronTrigger
* @param triggerName 触发器名
* @param triggerGroupName 触发器组名
* @param cronExpression 定时任务表达式
*/
public CronTrigger createCronTrigger(String triggerName,String triggerGroupName ,String cronExpression){
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerName, triggerGroupName)
.withSchedule(cronScheduleBuilder).build();
return cronTrigger;
}
/**
* 创建JobDetail
* @param jobDetailName 任务名称
* @param jobClass 任务类,实现Job类接口
*/
public JobDetail createCronScheduleJob(String jobDetailName,Class<? extends Job> jobClass){
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobDetailName).build();
return jobDetail;
}
/**
* 修改cron任务的执行间隔
* @param triggerName 旧触发器名
* @param triggerGroupName 旧触发器组名
* @param newCronTime
* @throws SchedulerException
*/
public boolean updateCronScheduleJob(String triggerName,String triggerGroupName,String newCronTime) throws SchedulerException {
Date date;
log.info("updateCronScheduleJob 入参name={},triggerGroupName={},newCronTime={}",triggerName,triggerGroupName,newCronTime);
TriggerKey triggerKey = new TriggerKey(triggerName, triggerGroupName);
CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if(ObjectUtils.isEmpty(cronTrigger)){
log.error("获取到的cronTrigger为null!");
return false;
}
String oldTime = cronTrigger.getCronExpression();
log.info("oldTimeCron={},newCronTime={}",oldTime,newCronTime);
if (!oldTime.equalsIgnoreCase(newCronTime)) {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(newCronTime);
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerName, triggerGroupName)
.withSchedule(cronScheduleBuilder).build();
date = scheduler.rescheduleJob(triggerKey, trigger);
log.info("修改执行成功,下次任务开始time={}",new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
return true;
}else{
log.error("oldTimeCron与newCronTime相等,修改结束");
return false;
}
}
}
2.使用ApplicationListener监听
package com.example.demo.config;
import cn.hutool.core.date.DateUtil;
import com.example.demo.job.PrintTimeJob;
import com.example.demo.util.QuartzUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobDetail;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Properties;
/**
* 监听器,启动定时任务
*
*/
@Slf4j
@Component
public class QuartzConfig implements ApplicationListener<ApplicationReadyEvent> {
/**
* 注入QuartzUtil
*/
@Autowired
private QuartzUtil quartzUtil;
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
try {
log.info("监听程序启动完成");
String jobName = "printTimeJob";
String cronTriggerName = "printTimeCronTrigger";
String cronTriggerGroupName = "printTimeCronTriggerGroup";
//创建定时任务PrintTimeJob,每10秒描述执行一次
Date cronScheduleJob = quartzUtil.createCronScheduleJob(jobName, PrintTimeJob.class, cronTriggerName, cronTriggerGroupName, "0/10 * * * * ?");
log.info("定时任务jobName={},cronTriggerName={},cronTriggerGroupName={},date={}",jobName, cronTriggerName,cronTriggerGroupName,DateUtil.format(cronScheduleJob,"yyyy-MM-dd HH:mm:ss"));
quartzUtil.scheduleJob();
} catch (SchedulerException e) {
e.printStackTrace();
log.error("监听程序启动失败");
}
}
}
3.创建Controller,模拟动态修改定时任务间隔
package com.example.demo.controller;
import com.example.demo.config.CompanyWeChatConfig;
import com.example.demo.util.ApiRes;
import com.example.demo.util.QuartzUtil;
import com.example.demo.util.ResultEnum;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @Author: wgq
* @CreateDate: 2019-11-8 13:43:21
* @Description: quartz测试
* @Version: 1.0
*/
@Slf4j
@RestController
@RequestMapping(value = "/qz")
public class QuartzController {
/*
*注入QuartzUtil
**/
@Autowired
QuartzUtil quartzUtil;
/**
* 修改定时任务间隔
* @param triggerName 触发器名称
* @param groupName 触发器组名
* @param newCronTime
* @return
*/
@PostMapping(value = "updateCronScheduleJob",produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "修改cron任务执行间隔")
public boolean sendMsg(@RequestParam String triggerName, @RequestParam String groupName,@RequestParam String newCronTime){
try {
return quartzUtil.updateCronScheduleJob(triggerName, groupName, newCronTime);
}catch (Exception e){
e.printStackTrace();
}
return false;
}
}
4.模拟动态修改定时任务间隔
1.现在我们启动项目,会发现在项目启动完成后,我们的自定义监听类QuartzConfig
里面的onApplicationEvent
方法被触发,我们的PrintTimeJob开始按照每10秒一次的频率执行。
2.现在我们使用postman调用
QuartzController
的updateCronScheduleJob
方法,将定时任务修改为每30秒一次
发送请求后,查看控制台打印信息,修改成功,定时任务变为每30秒执行一次!
OK,到了这里大功告成了!
当然Quartz功能不止如此,我们还可以动态的创建、停止定时任务等等,留给大家去探索。