1. 程式人生 > >springboot整合quartz定時器實現定時任務詳解

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

最近需要 做一個按照時間,定時初始化一些資訊的功能,研究了一下quartz,也簡單瞭解一下TimerTask,廢話不多說。
quartz和TimerTask的區別:
timer是jdk自帶的(可想而知,肯定是不怎麼好用)。
Quartz可以通過cron表示式精確到特定時間執行,而TimerTask不能。Quartz擁有TimerTask所有的功能,而TimerTask則沒有。
學習quartz需要知道的幾個概念

下面的概念來自網上,有點長,沒關係,不願意看可以跳過,下面有我個人理解精簡版

  1. Job:是一個介面,只有一個方法void execute(JobExecutionContext context),開發者實現該介面定義執行任務,JobExecutionContext類提供了排程上下文的各種資訊。Job執行時的資訊儲存在JobDataMap例項中;

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

  3. Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有SimpleTrigger和CronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔週期執行,SimpleTrigger是最適合的選擇;而CronTrigger則可以通過Cron表示式定義出各種複雜時間規則的排程方案:如每早晨9:00執行,週一、週三、週五下午5:00執行等;
  4. 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進行定點排除。
  5. Scheduler:代表一個Quartz的獨立執行容器,Trigger和JobDetail可以註冊到Scheduler中,兩者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查詢定位容器中某一物件的依據,Trigger的組及名稱必須唯一,JobDetail的組和名稱也必須唯一(但可以和Trigger的組和名稱相同,因為它們是不同型別的)。Scheduler定義了多個介面方法,允許外部通過組及名稱訪問和控制容器中Trigger和JobDetail。

幾個概念的理解– 精簡版

  1. job:你就理解成一個工作,是要幹什麼的,比如你是一個洗碗的,你的工作就是用洗潔精把盤子洗乾淨。程式碼裡面就是一個類,這個類就是這個任務要做什麼事情。
  2. jobdetail:就是這個工作的細節,也是一個介面,其中包含了這個工作的job類是什麼,還有就是這個任務的名稱,分組(主要是考慮到任務可能是分組的吧,所以框架是這樣設計的),然後可以將引數放進去。JobDetail jobDetail = JobBuilder.newJob(QuartzInitVopVosFactory.class)
    .withIdentity(job.getJobName(), job.getJobGroup()).build();
    jobDetail.getJobDataMap().put("job", job);
    這兩句新建jobdetail
  3. Trigger:觸發器,這裡面主要放了任務執行的時間(這裡就涉及到cron表示式,這裡不累贅,網上很多,下面會貼一個date轉cron的工具類)trigger = TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup()).withSchedule(scheduleBuilder).build();這句程式碼新建一個觸發器
  4. 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說的很亂,我這裡總結一下,當然肯定還有其他的解決方案。

  1. 資料庫中cron的時間修改以後,每個五秒掃描一次資料庫,然後任務定的是每天某個時間執行一次,但是執行很多次,原因:scheduler.rescheduleJob,如果伺服器當前時間與你的表示式配置的執行時間差在兩小時以內時,動態修改就會出現立即執行的情況。所以這裡設定執行時間從當前時間開始trigger = trigger.getTriggerBuilder().startAt(new Date()).withIdentity(triggerKey),主要startAt
  2. jobExecutionContext.getJobDetail().getJobDataMap()獲取的是一個listmap,應該使用jobExecutionContext.getMergedJobDataMap().getString(“params”);
  3. quartz的job無法注入spring物件,主要原因上面說過了,需要新增一個JobFactory ,並且注入AutowireCapableBeanFactory 。然後在通過JobFactory 新建Scheduler。
  4. 在資料庫中引數修改的時候,發現程式碼中讀取的並沒有同步更新,原因在於Trigger已存在,這裡重新構建scheduler的時候,需要再次執行JobDataMap jobDataMap = trigger.getJobDataMap();//重新獲取JobDataMap,並且更新引數
    jobDataMap.put("job", job);
  5. quartz在springboot專案有時候會執行兩次?這個原因目前不明,網上有說原因的,我這邊偶爾出現,所以暫時不研究。
  6. 大家可能會覺得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("任務名稱 ----------啟動成功");
    }

剩下的自己去研究吧。
如果有什麼寫的不對的地方,歡迎大家指出。