Quartz中Job相關類的理解和使用
原連結地址:https://www.jianshu.com/p/de06cf3c946b
(一)、Job
Job可以理解為就是一個工作任務,程式碼中就是一個實現了org.quartz.Job或org.quartz.StatefulJob介面的java類。當Scheduler決定執行Job時,execute()方法就會被執行。
具體可以幹啥:
1、每天定時傳送系統郵件
2、在指定的時刻傳送一條簡訊給使用者
3、執行完A任務後希望B任務在10秒後執行
4、。。。
總結就是任何java能做的任務都可以成為一個job。
注意:在 @Quartz整合-專案使用中已經講到,org.quartz.Job介面是一個無狀態的任務介面,實現此介面的任務每當觸發器觸發時都會準時執行,排程器並不關心此任務是否有其他例項正在執行,如果任務存在阻塞,可能會導致大量的任務例項並行執行。如果業務中存在不允許並行執行的任務,此時就用到有狀態的org.quartz.StatefulJob介面,此介面的任務實現類不能並行執行,必須等上一次觸發器觸發執行完成後 才可以進行下一次觸發執行。
那麼Job和StatefulJob在程式碼上具體有什麼區別呢?
它們沒有區別,只是名字不同而已,StatefulJob介面繼承了Job介面,框架只是通過介面的名字來標識是無狀態任務還是有狀態任務。
(二)、JobDetail
JobDetail(org.quartz.JobDetail)從字面的意思理解就是任務詳情,它包含了任務本身和任務的其他資訊。從 @Quartz整合-專案使用中的程式碼可以看出,是將一個JobDetail例項通過scheduleJob()方法註冊給了排程器,而不是一個Job實現類的例項。註冊在排程器上的每一個Job都只會建立一個JobDetail例項,而當真正執行JobDetail中的Job時,排程器才會對此Job實現類進行例項化,並執行它的execute()方法。這樣的好處就是我們無需擔心執行緒安全性問題,因為相當於同一時刻僅有一個執行緒去執行Job例項。
JobDetail的屬性介紹:
屬性名 | 詳細介紹 |
---|---|
name | 任務的名稱,框架需要通過此名稱進行關聯,所以需要保證唯一性 |
group | 任務組,預設為“DEFAULT”,取值Scheduler.DEFAULT_GROUP |
jobClass | Job的實現類類物件,注意不能使用代理類,原因下面介紹 |
description | 任務描述資訊,可以用來傳遞字串引數,但是儘量不要這麼幹 |
jobDataMap | 類似Map類,可以通過此類的物件向Job的execute方法中傳遞業務引數 |
volatility | 重啟應用之後是否刪除任務的相關資訊,預設為false |
durability | Job執行完後是否繼續持久化到資料庫,預設為false |
shouldRecover | 伺服器重啟後是否忽略過期的任務,預設為false |
JobDetail類中主要的幾個方法:
構造方法:
public JobDetail(){}
public JobDetail(String name, Class jobClass){} // 常用
public JobDetail(String name, String group, Class jobClass){} // 常用
public JobDetail(String name, String group, Class jobClass, boolean volatility, boolean durability, boolean recover){}
常用方法:
public String getName(){} // 獲取Job的名稱
public JobDataMap getJobDataMap(){} //獲取Job中需要使用到的資料
(三)、JobDataMap
實際開發中的任務業務程式碼不是簡單的列印幾句話而已,經常會需要傳入各種引數才能完成。我們可以通過哪些辦法傳遞引數到Job的execute()方法中呢?
(1)、通過JobDetail中的jobDataMap屬性
JobDataMap最頂層就是實現了java.util.Map介面,所以將它當做Map來使用就可以。但是需要注意的是,org.quartz.jobStore.userProperties預設配置為false,設定為true時表示JobDataMap中的value存放的型別必須是String型別,此配置有利於持久化時不會出現多個類版本。
存放非字串時會丟擲一個異常資訊:org.quartz.JobPersistenceException: Couldn't store job: JobDataMap values must be Strings when the 'useProperties' property is set.
解決辦法:
1、在quartz.properties配置檔案中加入org.quartz.jobStore.userProperties=false或直接刪除此條配置取預設值
2、將物件轉JSON字串後存入JobDataMap。
(2)、在execute方法中根據Job的name屬性(名稱是唯一的)在資料庫中查詢
資料庫表記錄如果生成的是UUID,那麼可以將Job的類名+UUID作為Job的名稱,比如:
TimingSendMessage_4a6c346ea44d4515946b87099275446a,TimingSendMessage是Job實現類的名稱,4a6c346ea44d4515946b87099275446a是業務記錄的唯一標識,然後在execute()方法中通過JobDetail例項的getName()方法獲取到名稱,再通過“_”下劃線切割,這樣就能拿到UUID了,最後通過此UUID在資料庫中查詢需要的資料。此方法的缺點就是程式碼看上去很不優雅而且程式碼理解稍加複雜。
(3)、使用JobDetail的description屬性傳遞簡單的String型別引數
直接呼叫setDescription(String description)方法,然後在execute()方法中通過get方法獲取。雖然這樣做可以達到目的,但是最好不要這樣,因為你寫的程式碼可能最後只有你自己懂或自己都忘了幹了什麼。
(四)、開發中的一些小技巧和注意事項
(1)、Job的execute()方法中需要使用到Spring管理的依賴bean
Spring幫忙管理bean的好處就是自己不用new物件啦,但是前面的文章提到過,Job的例項是在真正排程JobDetail中的Job實現類時進行例項化的,此物件由框架本身進行管理,所以例項化的物件並沒有交給Spring來管理,所以通過@Autowired注入時,會有點麻煩。
- 方法1、通過實現了org.springframework.context.ApplicationContextAware介面的java類來獲取,具體實現程式碼這裡不講述了。獲取示例:
ITaskStoreService taskStoreService = SpringContextHolder.getBean("taskStoreService");
- 通過@Autowired註解注入
package com.quartz.task;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.xk.quartz.entity.TaskStore;
import com.xk.quartz.service.impl.ITaskStoreService;
// 注意加上註解讓Spring掃描到此Job類
@Component
public class DefaultJob implements Job{
// 需要注入的依賴bean
private static ITaskStoreService taskStoreService;
// 定義一個set方法
@Autowired
public void setTaskStoreService(ITaskStoreService taskStoreService) {
DefaultJob.taskStoreService = taskStoreService;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDetail jobDetail = context.getJobDetail();
String name = jobDetail.getName();
// 使用依賴的bean
TaskStore store = taskStoreService.selectById(name);
System.out.println(store);
}
}
(2)、Job的execute()方法不會被Spring進行事物管理,如何保證一致性?
用上面一段程式碼稍作修改做以下示例:
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDetail jobDetail = context.getJobDetail();
String name = jobDetail.getName();
// 查詢記錄
TaskStore store = taskStoreService.selectById(name);
// 更新記錄
store.setUpdateDate(new Date());
taskStoreService.updateById(store);
// 刪除記錄
taskStoreService.deleteById("表記錄的id");
}
從程式碼可以看出,execute()方法既進行了更新和刪除,如果刪除方法因為網路等原因刪除失敗,那麼更新是不會回滾的。在execute()方法加上@Transactional也沒有用,還是上面說的原因,Job每次在需要執行的時候才進行例項化,不被Spring管理。此時我們可以這樣,將更新和刪除兩段程式碼封裝到TaskStoreService服務的同一個方法中,然後在方法上加上@Transactional交給Spring來管理事物,最後在execute()方法中呼叫封裝後的方法。
@Transactional
public void updateAndDelete(TaskStore store, String delId) {
// 更新
store.setUpdateDate(new Date());
taskStoreService.updateById(store);
// 刪除
taskStoreService.deleteById(store);
}
(3)、如何監控所有的Job執行和日誌的列印?
可以通過Quartz提供的監聽器來實現,本章節提供另外一種自定義方式。Quartz提供的監聽器後面的章節中會講到。
首先實現org.quartz.Job介面的一個抽象基類,然後重寫execute(JobExecutionContext context)方法,再在基類中定義一個抽象方法businessWorking(JobExecutionContext context) ,名字可以自己根據情況取,最後在execute方法中呼叫businessWorking方法。下面貼上程式碼:
package com.quartz.task;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
* 基礎Job抽象類
*/
public abstract class AbsQuartzJob implements Job{
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// Job執行前監控
System.out.println("execute before");
// 呼叫業務程式碼
this.businessWorking(context);
// Job執行後監控
System.out.println("execute after");
}
/** 業務程式碼抽象方法 */
public abstract void businessWorking(JobExecutionContext context);
}
開發中的業務Job類直接繼承AbsQuartzJob抽象類,業務程式碼寫在繼承類的businessWorking()方法中。程式碼示例:
package com.quartz.task;
import org.quartz.JobExecutionContext;
/**
* 業務job
*/
public class BusinessJob extends AbsQuartzJob{
@Override
public void businessWorking(JobExecutionContext context) {
// TODO 業務程式碼
}
}
這裡有人可能會問,為什麼不使用JDK或CGLIB動態代理呢?我本人之前也這樣實現過,但是有個致命的問題,那就是JobDetail被持久化之後會儲存到資料庫中,所以Job實現類的包路徑+類名也會存在資料庫中,當專案重啟後,所有動態代理的類都自動被虛擬機器清除了,此時Quartz框架啟動後會載入被持久化的Job,當去訪問Job的實現類時,發現已經找不到此實現類了。啟動直接報錯!所以上面的實現方式可能是相對方便好用一點的方法了。