1. 程式人生 > 程式設計 >SpringBoot整合Quartz實現定時任務的方法

SpringBoot整合Quartz實現定時任務的方法

1 需求

在我的前後端分離的實驗室管理專案中,有一個功能是學生狀態統計。我的設計是按天統計每種狀態的比例。為了便於計算,在每天0點,系統需要將學生的狀態重置,並插入一條資料作為一天的開始狀態。另外,考慮到學生的請假需求,請假的申請往往是提前做好,等系統時間走到實際請假時間的時候,系統要將學生的狀態修改為請假。

顯然,這兩個子需求都可以通過定時任務實現。在網上略做搜尋以後,我選擇了比較流行的定時任務框架Quartz。

2 Quartz

Quartz是一個定時任務框架,其他介紹網上也很詳盡。這裡要介紹一下Quartz裡的幾個非常核心的介面。

2.1 Scheduler介面

Scheduler翻譯成排程器,Quartz通過排程器來註冊、暫停、刪除Trigger和JobDetail。Scheduler還擁有一個SchedulerContext,顧名思義就是上下文,通過SchedulerContext我們可以獲取到觸發器和任務的一些資訊。

2.2 Trigger介面

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

2.3 JobDetail介面

Job介面是真正需要執行的任務。JobDetail介面相當於將Job介面包裝了一下,Trigger和Scheduler實際用到的都是JobDetail。

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.

翻譯一下:

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。

SpringBoot整合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容器中,更不可能實現依賴的自動裝配。

網上很多部落格也是這麼介紹的。但是根據我的實際測試,這樣寫可以完成依賴注入,但我還不知道它的實現原理。

SpringBoot整合Quartz實現定時任務的方法

SpringBoot整合Quartz實現定時任務的方法

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了。

SpringBoot整合Quartz實現定時任務的方法

到此這篇關於SpringBoot整合Quartz實現定時任務的文章就介紹到這了,更多相關SpringBoot整合Quartz實現定時任務內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!