SpringBoot整合Quartz實現動態修改定時任務間隔

飛馳的蝸牛發表於2019-11-24

一、背景

在平常的開發過程中,大家對定時任務肯定都不陌生,比如每天0點自動匯出下使用者資料,每天早上8點自動傳送一封系統的郵件等等。簡單的定時任務使用spring自帶的 @Scheduled實現即可。但是對於一些特殊的場景下,比如我們想在不重啟專案的情況下,動態的修改定時器的執行間隔,將原來每30分鐘執行一次的定時任務,動態改為5分鐘執行一次,這時候Spring自帶的定時器就不太方便了。

二、引入Quartz

“工欲善其事,必先利其器”——Quartz就是我們解決這個問題的利器, Quartz是Apache下開源的一款功能強大的定時任務框架。Quartz官網 對它的描述:

Quartz
Quartz是一個功能豐富的開源作業排程庫,可以整合到幾乎任何Java應用程式中——從最小的獨立應用程式到最大的電子商務系統。Quartz可用於建立簡單或複雜的排程,以執行數萬、數百甚至數萬個作業;任務被定義為標準Java元件的作業,這些元件可以執行幾乎任何您可以程式設計讓它們執行的任務。Quartz排程器包含許多企業級特性,比如對JTA事務和叢集的支援。

在使用Quartz之前,我們要先了解下Quartz的中核心組成:

  • Job 任務,要執行的具體內容,我們自定義的任務類都要實現此Job介面,Job介面中只有一個方法,我們的業務邏輯就在此方法編寫,Job的原始碼如下:

    Job

  • JobDetail 表示一個具體的可執行的排程程式,Job 是這個可執行程排程程式所要執行的內容,另外 JobDetail 還包含了這個任務排程的方案和策略

  • Trigger 觸發器,用於定義Job任務何時被觸發,以何種方式觸發。Trigger介面的關係圖如下,最常用的是CronTrigger

    Trigger

  • 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介面的原始碼如下:

    AppListener

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呼叫QuartzControllerupdateCronScheduleJob方法,將定時任務修改為每30秒一次
在這裡插入圖片描述
傳送請求後,檢視控制檯列印資訊,修改成功,定時任務變為每30秒執行一次!
在這裡插入圖片描述
OK,到了這裡大功告成了! 當然Quartz功能不止如此,我們還可以動態的建立、停止定時任務等等,留給大家去探索。

相關文章