一、背景
在平常的開發過程中,大家對定時任務肯定都不陌生,比如每天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秒一次的頻率執行。
QuartzController
的updateCronScheduleJob
方法,將定時任務修改為每30秒一次
傳送請求後,檢視控制檯列印資訊,修改成功,定時任務變為每30秒執行一次!
OK,到了這裡大功告成了!
當然Quartz功能不止如此,我們還可以動態的建立、停止定時任務等等,留給大家去探索。