四、Quartz中Job相關類的理解和使用

(一)、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的實現類時,發現已經找不到此實現類了。啟動直接報錯!所以上面的實現方式可能是相對方便好用一點的方法了。

到此,Job相關類的理解和使用說完了。如果有什么問題,請在下方留言,謝謝!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容