1. 程式人生 > >Quartz中Job相關類的理解和使用

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