springboot整合quartz定時器實現定時任務詳解


quartz和TimerTask的區別:
timer是jdk自帶的(可想而知,肯定是不怎么好用)。
Quartz可以通過cron表達式精確到特定時間執行,而TimerTask不能。Quartz擁有TimerTask所有的功能,而TimerTask則沒有。
學習quartz需要知道的幾個概念

下面的概念來自網上,有點長,沒關系,不願意看可以跳過,下面有我個人理解精簡版
Job:是一個接口,只有一個方法void execute(JobExecutionContext context),開發者實現該接口定義運行任務,JobExecutionContext類提供了調度上下文的各種信息。Job運行時的信息保存在JobDataMap實例中;

JobDetail:Quartz在每次執行Job時,都重新創建一個Job實例,所以它不直接接受一個Job的實例,相反它接收一個Job實現類,以便運行時通過newInstance()的反射機制實例化Job。因此需要通過一個類來描述Job的實現類及其它相關的靜態信息,如Job名字、描述、關聯監聽器等信息,JobDetail承擔了這一角色。

Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有SimpleTrigger和CronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔周期執行,SimpleTrigger是最適合的選擇;而CronTrigger則可以通過Cron表達式定義出各種復雜時間規則的調度方案:如每早晨9:00執行,周一、周三、周五下午5:00執行等;
Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日歷特定時間點的集合(可以簡單地將org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一個日歷時間點,無特殊說明后面的Calendar即指org.quartz.Calendar)。一個Trigger可以和多個Calendar關聯,以便排除或包含某些時間點。假設,我們安排每周星期一早上10:00執行任務,但是如果碰到法定的節日,任務則不執行,這時就需要在Trigger觸發機制的基礎上使用Calendar進行定點排除。
Scheduler:代表一個Quartz的獨立運行容器,Trigger和JobDetail可以注冊到Scheduler中,兩者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查找定位容器中某一對象的依據,Trigger的組及名稱必須唯一,JobDetail的組和名稱也必須唯一(但可以和Trigger的組和名稱相同,因為它們是不同類型的)。Scheduler定義了多個接口方法,允許外部通過組及名稱訪問和控制容器中Trigger和JobDetail。
幾個概念的理解– 精簡版
job:你就理解成一個工作,是要干什么的,比如你是一個洗碗的,你的工作就是用洗潔精把盤子洗干凈。代碼里面就是一個類,這個類就是這個任務要做什么事情。
jobdetail:就是這個工作的細節,也是一個接口,其中包含了這個工作的job類是什么,還有就是這個任務的名稱,分組(主要是考慮到任務可能是分組的吧,所以框架是這樣設計的),然后可以將參數放進去。JobDetail jobDetail = JobBuilder.newJob(QuartzInitVopVosFactory.class)
.withIdentity(job.getJobName(), job.getJobGroup()).build();
jobDetail.getJobDataMap().put("job", job);這兩句新建jobdetail
Trigger:觸發器,這里面主要放了任務執行的時間(這里就涉及到cron表達式,這里不累贅,網上很多,下面會貼一個date轉cron的工具類)trigger = TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup()).withSchedule(scheduleBuilder).build();這句代碼新建一個觸發器
Scheduler:這個就是老大了,相當於一個容易, scheduler.scheduleJob(jobDetail, trigger);這句代碼將任務的參數以及觸發時間等信息放進去,構建成一個任務,到時間了自動執行。
概念大致上了解了,下面直接上代碼:
額,多講幾句,網上的demo都不全,不是差這,就是差那,坑爹啊。
環境說明:springboot+quartz+maven
需要導入兩個jar

<!-- quartz定時器 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency><!-- 該依賴必加,里面有sping對schedule的支持 -->
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>

有關配置:
這里就不用xml配置了,使用java實現配置,兩個文件:

QuartzConfigration

package com.aaa.util;

import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration
public class QuartzConfigration {

@Autowired
private JobFactory jobFactory;

@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobFactory(jobFactory);
// 用於quartz集群,QuartzScheduler 啟動時更新己存在的Job
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setStartupDelay(1);
return schedulerFactoryBean;
}

@Bean
public Scheduler scheduler() {
return schedulerFactoryBean().getScheduler();
}

}

上面這個是主要配置,通過SchedulerFactoryBean,生成Scheduler。

JobFactory

package com.aaa.util;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;

@Component
public class JobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;

@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 調用父類的方法
Object jobInstance = super.createJobInstance(bundle);
// 進行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}

這個配置主要用於第一個配置,在第一個配置中需要注入這個文件,這個文件主要是注入AutowireCapableBeanFactory 這個類,不然你在實現類(就是一個繼承job的類,下面會貼)中是沒辦法注入其他service或者其他東西。

QuartzInitVopVosFactory

package com.aaa.util;

/**
* 初始化運營信息接口調用
* @author wxy
* @date 2018年6月4日 下午5:27:12
*/
@DisallowConcurrentExecution
public class QuartzInitVopVosFactory implements Job{

@Autowired
private RedisTemplate<String, ?> redisTemplate;

public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//這里寫job代碼,就是這個任務,具體要實現什么,就在這里寫
Shift jobBean = (Shift) jobExecutionContext.getMergedJobDataMap().get("job");
//上面這句比較坑,必須用getMergedJobDataMap,不然獲取的是一個list<map>對象。不好解析,
//所有的參數以及其他信息都在JobExecutionContext
//順帶提一句,如果你沒有JobFactory 這個類,在這里是沒辦法注入任何類的。
//shift是實體類,


}
}

package com.aaa.service.impl;

import java.util.Date;
import java.util.List;

/**
* 定時器相關實現
* @author wxy
* @date 2018年6月6日 下午5:37:43
*/
@Service
public class QuartzServiceImpl implements QuartzService {

@Autowired
private Scheduler scheduler;

@Autowired
private ShiftMapper shiftMapper;

@Scheduled(fixedRate = 5000) // 每隔5s查庫,並根據查詢結果決定是否重新設置定時任務
public void initVopVos(){
//這里獲取任務信息數據
List<Shift> jobList = shiftMapper.getTodayJob(); //從數據庫中獲取所以今天需要執行的任務

try {
for (Shift job : jobList) {
TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());

//獲取trigger,即在spring配置文件中定義的 bean id="myTrigger"
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);

//不存在,創建一個
if (null == trigger) {
JobDetail jobDetail = JobBuilder.newJob(QuartzInitVopVosFactory.class)
.withIdentity(job.getJobName(), job.getJobGroup()).build();
jobDetail.getJobDataMap().put("job", job);

//表達式調度構建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(CronDateUtils.getCron(job
.getDatetime()));

//按新的cronExpression表達式構建一個新的trigger
trigger = TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup()).withSchedule(scheduleBuilder).build();

scheduler.scheduleJob(jobDetail, trigger);
} else {
// Trigger已存在,那么更新相應的定時設置
//表達式調度構建器,我這里數據庫中存的執行時間是一個日期,這里講date轉成cron才能執行
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(CronDateUtils.getCron(job
.getDatetime()));

//按新的cronExpression表達式重新構建trigger
trigger = trigger.getTriggerBuilder().startAt(new Date()).withIdentity(triggerKey)
.withSchedule(scheduleBuilder).build();
//scheduler.rescheduleJob如果服務器當前時間與你的表達式配置的執行時間差在兩小時以內時,
//動態修改就會出現立即執行的情況。所以這里設置執行時間從當前時間開始

JobDataMap jobDataMap = trigger.getJobDataMap();//重新獲取JobDataMap,並且更新參數
jobDataMap.put("job", job);

//按新的trigger重新設置job執行
scheduler.rescheduleJob(triggerKey, trigger);
}
}
} catch (SchedulerException e) {
System.out.println("initVopVos Error");
}
}
}

上面這段代碼注釋已經很清楚了,這里不做過多累贅,springboot啟動以后,就會每隔五秒查一次數據庫,如果有新任務就會新建一個scheduleJob,如果有修改,就會執行rescheduleJob。

package com.aaa.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* 該類提供Quartz的cron表達式與Date之間的轉換
* @author wxy
* @date 2018年6月8日 上午10:21:04
*/
public class CronDateUtils{
private static final String CRON_DATE_FORMAT = "ss mm HH dd MM ? yyyy";

/***
*
* @param date 時間
* @return cron類型的日期
*/
public static String getCron(final Date date){
SimpleDateFormat sdf = new SimpleDateFormat(CRON_DATE_FORMAT);
String formatTimeStr = "";
if (date != null) {
formatTimeStr = sdf.format(date);
}
return formatTimeStr;
}

/***
*
* @param cron Quartz cron的類型的日期
* @return Date日期
*/

public static Date getDate(final String cron) {


if(cron == null) {
return null;
}

SimpleDateFormat sdf = new SimpleDateFormat(CRON_DATE_FORMAT);
Date date = null;
try {
date = sdf.parse(cron);
} catch (ParseException e) {
return null;// 此處缺少異常處理,自己根據需要添加
}
return date;
}
}

最后一個工具類,可以將date轉cron表達式,可以將cron表達式轉為date。

上面的代碼可以實現:每隔五秒,查一次數據庫,並將任務加入任務表,到時間執行,並且如果任務參數,或者執行時間有更改,會自動更新到任務中。

最后講一下我遇到的幾個問題(也是網上的demo沒有解決的問題):
網上的demo說的很亂,我這里總結一下,當然肯定還有其他的解決方案。

數據庫中cron的時間修改以后,每個五秒掃描一次數據庫,然后任務定的是每天某個時間執行一次,但是執行很多次,原因:scheduler.rescheduleJob,如果服務器當前時間與你的表達式配置的執行時間差在兩小時以內時,動態修改就會出現立即執行的情況。所以這里設置執行時間從當前時間開始trigger = trigger.getTriggerBuilder().startAt(new Date()).withIdentity(triggerKey),主要startAt
jobExecutionContext.getJobDetail().getJobDataMap()獲取的是一個listmap,應該使用jobExecutionContext.getMergedJobDataMap().getString(“params”);
quartz的job無法注入spring對象,主要原因上面說過了,需要新增一個JobFactory ,並且注入AutowireCapableBeanFactory 。然后在通過JobFactory 新建Scheduler。
在數據庫中參數修改的時候,發現代碼中讀取的並沒有同步更新,原因在於Trigger已存在,這里重新構建scheduler的時候,需要再次執行JobDataMap jobDataMap = trigger.getJobDataMap();//重新獲取JobDataMap,並且更新參數
jobDataMap.put("job", job);
quartz在springboot項目有時候會執行兩次?這個原因目前不明,網上有說原因的,我這邊偶爾出現,所以暫時不研究。
大家可能會覺得QuartzServiceImpl 這里面有一個JobBuilder.newJob(QuartzInitVopVosFactory.class) 這里面有一個QuartzInitVopVosFactory參數,寫的太死了,大家可以利用反射,通過參數來調用這個方法。由於我這里參數太多,所以就沒有用這種方式,給大家一個反射的參考例子:
public static void invokMethod(Operating operating) {
Object object = null;
Class clazz = null;
try {
clazz = Class.forName("com.business.controller.OperatingController");
object = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}

if (object == null) {
//log.error("任務名稱 = [" + scheduleJob.getJobName() + "]---------------未啟動成功,請檢查是否配置正確!!!");
System.out.println("任務名稱 =---------------未啟動成功,請檢查是否配置正確!!!");
return;
}
clazz = object.getClass();
Method method = null;
try {
method = clazz.getMethod("init", Operating.class);
} catch (NoSuchMethodException e) {
//log.error("任務名稱 = [" + scheduleJob.getJobName() + "]---------------未啟動成功,方法名設置錯誤!!!");
System.out.println("任務名稱 =---------------未啟動成功,方法名設置錯誤!!!");
} catch (SecurityException e) {
e.printStackTrace();
}
if (method != null) {
try {
method.invoke(object, operating);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
System.out.println("任務名稱 ----------啟動成功");
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM