SpringBoot集成Quartz實現定時任務

1 需求

在個人先後端分離的實驗室管理項目中,有一個功能是學生狀態統計。個人設計是按天統計每種狀態的比例。爲了便於計算,在天天0點,系統須要將學生的狀態重置,並插入一條數據做爲一天的開始狀態。另外,考慮到學生的請假需求,請假的申請每每是提早作好,等系統時間走到實際請假時間的時候,系統要將學生的狀態修改成請假。html

顯然,這兩個子需求均可以經過定時任務實現。在網上略作搜索之後,我選擇了比較流行的定時任務框架Quartz。java

2 Quartz

Quartz是一個定時任務框架,其餘介紹網上也很詳盡。這裏要介紹一下Quartz裏的幾個很是核心的接口。git

2.1 Scheduler接口

Scheduler翻譯成調度器,Quartz經過調度器來註冊、暫停、刪除Trigger和JobDetail。Scheduler還擁有一個SchedulerContext,顧名思義就是上下文,經過SchedulerContext咱們能夠獲取到觸發器和任務的一些信息。github

2.2 Trigger接口

Trigger能夠翻譯成觸發器,經過cron表達式或是SimpleScheduleBuilder等類,指定任務執行的週期。系統時間走到觸發器指定的時間的時候,觸發器就會觸發任務的執行。spring

2.3 JobDetail接口

Job接口是真正須要執行的任務。JobDetail接口至關於將Job接口包裝了一下,Trigger和Scheduler實際用到的都是JobDetail。sql

3 SpringBoot官方文檔解讀

SpringBoot官方寫了spring-boot-starter-quartz。使用過SpringBoot的同窗都知道這是一個官方提供的啓動器,有了這個啓動器,集成的操做就會被大大簡化。數據庫

如今咱們來看一看SpingBoot2.2.6官方文檔,其中第4.20小節Quartz Scheduler就談到了Quartz,但很惋惜一共只有兩頁不到的內容,先來看看這麼精華的文檔裏能學到些什麼。後端

Spring Boot offers several conveniences for working with the Quartz scheduler, including the
spring-boot-starter-quartz 「Starter」. If Quartz is available, a Scheduler is auto-configured (through the SchedulerFactoryBean abstraction).
Beans of the following types are automatically picked up and associated with the Scheduler:
• JobDetail: defines a particular Job. JobDetail instances can be built with the JobBuilder API.
• Calendar.
• Trigger: defines when a particular job is triggered.

翻譯一下:app

SpringBoot提供了一些便捷的方法來和Quartz協同工做,這些方法裏面包括`spring-boot-starter-quartz`這個啓動器。若是Quartz可用,Scheduler會經過SchedulerFactoryBean這個工廠bean自動配置到SpringBoot裏。
JobDetail、Calendar、Trigger這些類型的bean會被自動採集並關聯到Scheduler上。
Jobs can define setters to inject data map properties. Regular beans can also be injected in a similar manner.

翻譯一下:框架

Job能夠定義setter(也就是set方法)來注入配置信息。也能夠用一樣的方法注入普通的bean。

下面是文檔裏給的示例代碼,我直接徹底照着寫,拿到的倒是null。不知道是否是個人使用方式有誤。後來仔細一想,文檔的意思應該是在建立Job對象以後,調用set方法將依賴注入進去。但後面咱們是經過框架反射生成的Job對象,這樣作反而會搞得更加複雜。最後仍是決定採用給Job類加@Component註解的方法。

文檔的其餘篇幅就介紹了一些配置,可是介紹得也不全面,看了幫助也並非很大。詳細的配置能夠參考w3school的Quartz配置

4 SpringBoot集成Quartz

4.1 建表

我選擇將定時任務的信息保存在數據庫中,優勢是顯而易見的,定時任務不會由於系統的崩潰而丟失。

建表的sql語句在Quartz的github中能夠找到,裏面有針對每一種經常使用數據庫的sql語句,具體地址是:Quartz數據庫建表sql

quartz表

建表之後,能夠看到數據庫裏多了11張表。咱們徹底不須要關心每張表的具體做用,在添加刪除任務、觸發器等的時候,Quartz框架會操做這些表。

4.2 引入依賴

pom.xml裏添加依賴。

<!-- quartz 定時任務 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

4.3 配置quartz

application.yml中配置quartz。相關配置的做用已經寫在註解上。

# spring的datasource等配置未貼出
spring:
  quartz:
      # 將任務等保存化到數據庫
      job-store-type: jdbc
      # 程序結束時會等待quartz相關的內容結束
      wait-for-jobs-to-complete-on-shutdown: true
      # QuartzScheduler啓動時更新己存在的Job,這樣就不用每次修改targetObject後刪除qrtz_job_details表對應記錄
      overwrite-existing-jobs: true
      # 這裏竟然是個map,搞得智能提示都沒有,佛了
      properties:
        org:
          quartz:
              # scheduler相關
            scheduler:
              # scheduler的實例名
              instanceName: scheduler
              instanceId: AUTO
            # 持久化相關
            jobStore:
              class: org.quartz.impl.jdbcjobstore.JobStoreTX
              driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
              # 表示數據庫中相關表是QRTZ_開頭的
              tablePrefix: QRTZ_
              useProperties: false
            # 線程池相關
            threadPool:
              class: org.quartz.simpl.SimpleThreadPool
              # 線程數
              threadCount: 10
              # 線程優先級
              threadPriority: 5
              threadsInheritContextClassLoaderOfInitializingThread: true

4.4 註冊週期性的定時任務

第1節中提到的第一個子需求是在天天0點執行的,是一個週期性的任務,任務內容也是肯定的,因此直接在代碼裏註冊JobDetail和Trigger的bean就能夠了。固然,這些JobDetail和Trigger也是會被持久化到數據庫裏。

/**
 * Quartz的相關配置,註冊JobDetail和Trigger
 * 注意JobDetail和Trigger是org.quartz包下的,不是spring包下的,不要導入錯誤
 */
@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail jobDetail() {
        JobDetail jobDetail = JobBuilder.newJob(StartOfDayJob.class)
                .withIdentity("start_of_day", "start_of_day")
                .storeDurably()
                .build();
        return jobDetail;
    }

    @Bean
    public Trigger trigger() {
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail())
                .withIdentity("start_of_day", "start_of_day")
                .startNow()
                // 天天0點執行
                .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?"))
                .build();
        return trigger;
    }
}

builder類建立了一個JobDetail和一個Trigger並註冊成爲Spring bean。從第3節中摘錄的官方文檔中,咱們已經知道這些bean會自動關聯到調度器上。須要注意的是JobDetail和Trigger須要設置組名和本身的名字,用來做爲惟一標識。固然,JobDetail和Trigger的惟一標識能夠相同,由於他們是不一樣的類。

Trigger經過cron表達式指定了任務執行的週期。對cron表達式不熟悉的同窗能夠百度學習一下。

JobDetail裏有一個StartOfDayJob類,這個類就是Job接口的一個實現類,裏面定義了任務的具體內容,看一下代碼:

@Component
public class StartOfDayJob extends QuartzJobBean {
    private StudentService studentService;

    @Autowired
    public StartOfDayJob(StudentService studentService) {
        this.studentService = studentService;
    }

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext)
            throws JobExecutionException {
        // 任務的具體邏輯
    }
}
這裏面有一個小問題,上面用builder建立JobDetail時,傳入了StartOfDayJob.class,按常理推測,應該是Quartz框架經過反射建立StartOfDayJob對象,再調用executeInternal()執行任務。這樣依賴,這個Job是Quartz經過反射建立的,即便加了註解@Component,這個StartOfDayJob對象也不會被註冊到ioc容器中,更不可能實現依賴的自動裝配。

網上不少博客也是這麼介紹的。可是根據個人實際測試,這樣寫能夠完成依賴注入,但我還不知道它的實現原理。

編寫定時任務

依賴注入成功

4.5 註冊無週期性的定時任務

第1節中提到的第二個子需求是學生請假,顯然請假是不定時的,一次性的,並且不具備週期性。

4.5節與4.4節大致相同,可是有兩點區別:

  • Job類須要獲取到一些數據用於任務的執行;
  • 任務執行完成後刪除Job和Trigger。

業務邏輯是在老師批准學生的請假申請時,向調度器添加Trigger和JobDetail。

實體類:

public class LeaveApplication {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Long proposerUsername;
    @JsonFormat( pattern = "yyyy-MM-dd HH:mm",timezone="GMT+8")
    private LocalDateTime startTime;
    @JsonFormat( pattern = "yyyy-MM-dd HH:mm",timezone="GMT+8")
    private LocalDateTime endTime;
    private String reason;
    private String state;
    private String disapprovedReason;
    private Long checkerUsername;
    private LocalDateTime checkTime;

    // 省略getter、setter
}

Service層邏輯,重要的地方已在註釋中說明。

@Service
public class LeaveApplicationServiceImpl implements LeaveApplicationService {
    @Autowired
    private Scheduler scheduler;
    
    // 省略其餘方法與其餘依賴

    /**
     * 添加job和trigger到scheduler
     */
    private void addJobAndTrigger(LeaveApplication leaveApplication) {
        Long proposerUsername = leaveApplication.getProposerUsername();
        // 建立請假開始Job
        LocalDateTime startTime = leaveApplication.getStartTime();
        JobDetail startJobDetail = JobBuilder.newJob(LeaveStartJob.class)
                // 指定任務組名和任務名
                .withIdentity(leaveApplication.getStartTime().toString(),
                        proposerUsername + "_start")
                // 添加一些參數,執行的時候用
                .usingJobData("username", proposerUsername)
                .usingJobData("time", startTime.toString())
                .build();
        // 建立請假開始任務的觸發器
        // 建立cron表達式指定任務執行的時間,因爲請假時間是肯定的,因此年月日時分秒都是肯定的,這也符合任務只執行一次的要求。
        String startCron = String.format("%d %d %d %d %d ? %d",
                startTime.getSecond(),
                startTime.getMinute(),
                startTime.getHour(),
                startTime.getDayOfMonth(),
                startTime.getMonth().getValue(),
                startTime.getYear());
        CronTrigger startCronTrigger = TriggerBuilder.newTrigger()
                // 指定觸發器組名和觸發器名
                .withIdentity(leaveApplication.getStartTime().toString(),
                        proposerUsername + "_start")
                .withSchedule(CronScheduleBuilder.cronSchedule(startCron))
                .build();

        // 將job和trigger添加到scheduler裏
        try {
            scheduler.scheduleJob(startJobDetail, startCronTrigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
            throw new CustomizedException("添加請假任務失敗");
        }
    }
}

Job類邏輯,重要的地方已在註釋中說明。

@Component
public class LeaveStartJob extends QuartzJobBean {
    private Scheduler scheduler;
    private SystemUserMapperPlus systemUserMapperPlus;

    @Autowired
    public LeaveStartJob(Scheduler scheduler,
                         SystemUserMapperPlus systemUserMapperPlus) {
        this.scheduler = scheduler;
        this.systemUserMapperPlus = systemUserMapperPlus;
    }

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext)
            throws JobExecutionException {
        Trigger trigger = jobExecutionContext.getTrigger();
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        // 將添加任務的時候存進去的數據拿出來
        long username = jobDataMap.getLongValue("username");
        LocalDateTime time = LocalDateTime.parse(jobDataMap.getString("time"));

        // 編寫任務的邏輯

        // 執行以後刪除任務
        try {
            // 暫停觸發器的計時
            scheduler.pauseTrigger(trigger.getKey());
            // 移除觸發器中的任務
            scheduler.unscheduleJob(trigger.getKey());
            // 刪除任務
            scheduler.deleteJob(jobDetail.getKey());
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

5 總結

上文所述的內容應該能夠知足絕大部分定時任務的需求。我在查閱網上的博客以後,發現大部分博客裏介紹的Quartz使用仍是停留在Spring階段,配置也都是經過xml,所以我在實現了功能之後,將整個過程總結了一下,留給須要的人以及之後的本身作參考。

整體上來講,Quartz實現定時任務仍是很是方便的,與SpringBoot整合以後配置也很是簡單,是實現定時任務的不錯的選擇。

5.2 小坑1

在IDEA2020.1版本里使用SpringBoot與Quartz時,報錯找不到org.quartz程序包,可是依賴裏面明明有org.quartz,類裏的import也沒有報錯,還能夠經過Ctrl+鼠標左鍵直接跳轉到相應的類裏。後面我用了IDEA2019.3.4就再也不有這個錯誤。那麼就是新版IDEA的BUG了。

報錯

本文由博客羣發一文多發等運營工具平臺 OpenWrite 發佈
相關文章
相關標籤/搜索