歡迎您光臨本站 註冊首頁

SpringBoot集成Quartz實現定時任務的方法

←手機掃碼閱讀     sl_ivan @ 2020-05-07 , reply:0

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。
建表以後,可以看到數據庫裡多了11張表。我們完全不需要關心每張表的具體作用,在添加刪除任務、觸發器等的時候,Quartz框架會操作這些表。
4.2 引入依賴
在 pom.xml 裡添加依賴。

org.springframework.bootspring-boot-starter-quartz2.2.6.RELEASE


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


[sl_ivan ] SpringBoot集成Quartz實現定時任務的方法已經有248次圍觀

http://coctec.com/docs/java/show-post-233145.html