Java 定時任務 Quartz (二)—— 數據傳遞


1 寫在前面

在實際的開發中,我們經常需要向任務傳遞數據參數,在之前的任務創建中,我們只能以 JobBuilder.newJob(DataJob.class) 的形式向建造器傳遞一個 class,實際上 JobDetail 接口規定了一個方法 getJobDataMap(),用於傳遞數據。 

2 初探數據的傳遞

2.1 JobDataMap

通過閱讀 JobDataMap 的源碼,我們發現它是一個使用 String 作為 key 的 Map 的具體實現,同時具有一個 isDirty 字段。它具有 getCharacterFromString() 等方法,方法原理就是從 Map 中獲取 Object,並且強制轉換到 Character 類型,其他方法也類似。請注意其父類的泛型類別:

 

 

 

2.2 JobExecutionContext

如果 Job 在執行任務中需要獲取數據,自然是從唯一的運行方法參數 JobExecutionContext 中獲取,JobExecutionContext 的接口規定了 getJobDetail() 方法以獲取 JobDetail,並且在 JobDetail 中存在一個獲取 JobDataMap 的方法 getJobDataMap(), 按照這個思路,我們自然會想到數據的傳遞很大可能是在 JobBuilder 中完成的(在后邊的分析中,我們發現在 Trigger 中也可以傳遞,當然這些都是后話)

2.3 方法 usingJobData

usingJobData 方法有多種簽名,有 usingJobData(String dataKey, Boolean value),usingJobData(String dataKey, Double value),usingJobData(JobDataMap newJobDataMap) 不等,我們先查看其中一個方法

各種方法中的 dataKey 字段是 String 類型的,對應  DirtyFlagMap<String, Object> 的第一個泛型類型。

 

 

實際上 jobDataMap 字段就是 JobDataMap 類,該方法將數據傳進去 jobDataMap 中,並在 build() 方法中將字段 jobDataMap 的值設置給了 JobDetail 中的 jobDataMap:

2.4 測試

根據上邊的分析,我們可以簡單的寫出下邊的測試類:

 1 /**
 2  * @author pancc
 3  * @version 1.0
 4  */
 5 public class DataJobDemo {
 6 
 7     public static void main(String[] args) throws SchedulerException, InterruptedException {
 8         JobDetail detail = JobBuilder.newJob(DataJob.class)
 9                 .withIdentity("data", "group0")
10                 .usingJobData("data", "hello")
11                 .build();
12 
13         Trigger trigger = TriggerBuilder.newTrigger()
14                 .withIdentity("data_trigger")
15                 .startNow()
16                 .build();
17 
18         Scheduler scheduler = new StdSchedulerFactory().getScheduler();
19 
20         scheduler.start();
21         scheduler.scheduleJob(detail, trigger);
22         /*
23          * 2 秒鍾后關閉
24          */
25         Thread.sleep(2_000);
26         scheduler.shutdown();
27     }
28 
29     public static class DataJob implements Job {
30 
31         @Override
32         public void execute(JobExecutionContext context) {
33             String data = context.getJobDetail().getJobDataMap().getString("data");
34             System.out.printf("get data {%s} from map\n", data);
35 
36         }
37     }
38 }

 

這個程序任務成功的打印了如下語句:

 

3 再探數據的傳遞

根據官方描述,在 JobDataMap 中設置的值,會自動映射到 Job 類的字段中,這里有一個隱性要求,字段與 setter 方法必須遵循 JavaBean 規范。

這次我們不能像上邊一樣通過簡單查看源碼就能理解其中的設計,讓我們通過一個實例,並且打斷點來查看它的自動注入魔法。

3.1 簡單測試

我們規定 Job 有一個 name 字段,並且符合 JavaBean 規范,並且我們向 JobDetail 傳遞一個 dataKey 為 name  的值:

 1 /**
 2  * @author pancc
 3  * @version 1.0
 4  */
 5 public class InjectDataDemo {
 6 
 7     public static void main(String[] args) throws SchedulerException, InterruptedException {
 8         JobDetail detail = JobBuilder.newJob(InjectData.class)
 9                 .withIdentity("inject", "group0")
10                 .usingJobData("name", "Alex")
11                 .build();
12 
13         Trigger trigger = TriggerBuilder.newTrigger()
14                 .withIdentity("inject_trigger")
15                 .startNow()
16                 .build();
17 
18         Scheduler scheduler = new StdSchedulerFactory().getScheduler();
19 
20         scheduler.start();
21         scheduler.scheduleJob(detail, trigger);
22         /*
23          * 2 秒鍾后關閉
24          */
25         Thread.sleep(2_000);
26         scheduler.shutdown();
27     }
28 
29     public static class InjectData implements Job {
30 
31         private String name;
32 
33         public void setName(String name) {
34             this.name = name;
35         }
36 
37         @Override
38         public void execute(JobExecutionContext context) throws JobExecutionException {
39             System.out.printf("hello, my name is %s \n", name);
40         }
41     }
42 
43 }

 

查看控制台,我們成功在 name 屬性獲取到了值:

 

3.2 探索注入原理

 

 現在,在 this.name = name; 前打上斷點,我們通過 debug 的調用棧來查看實際的過程。 

 

分別有四個入口,這是

  • 第一個入口: shell.initialize(qs);  從屬於 QuartzSchedulerThread 線程中 run 方法,是啟動 QuartzScheduler 時調用的。
  • 第二個入口:job = sched.getJobFactory().newJob(firedTriggerBundle, scheduler); 這里實例化了一個任務。
  • 第三個入口:setBeanProps(job, jobDataMap);  從方法名我們可以猜測它使用 jobDataMap 對 Bean 的字段值進行了設置。
  • 第四個入口:setMeth.invoke(obj, new Object[]{ parm }); 從方法名我們可以猜測它通過反射調用了對應的字段的 setter 方法設置了值。

在第個入口,我們通過 debug 的 variables 視圖,可以觀察到此時 它不斷嘗試從 jobDataMap 獲取 key,嘗試使用 datakey  的值在 InjectData 類中查找對應的 setter 方法,並且在實例上調用該 setter 方法設置值:

  

 

4 Trigger 的數據傳遞與數據合並

在 Trigger 的構建中,我們還觀察到了與 JobBuilder 同樣的方法 usingJobData ,如果此時往 Trigger 傳遞進去 重復的值與新的字段,會如何?在編寫測試代碼之前,我們回到上點分析中的 PropertySettingJobFactory 類中,仔細觀察這個方法:

 

 與 DirtyFlagMap 中的 Map 類型:

 

 因此,我們的數據就像在合並兩個 HashMap 一樣,重復的鍵值對會發生覆蓋,新的值覆蓋舊的,不沖突的則保留

4.1 合並驗證

 1 /**
 2  * @author pancc
 3  * @version 1.0
 4  */
 5 public class DuplicatedDataDemo {
 6 
 7     public static void main(String[] args) throws SchedulerException, InterruptedException {
 8         JobDetail detail = JobBuilder.newJob(DuplicatedData.class)
 9                 .withIdentity("inject", "group0")
10                 .usingJobData("name", "Alex")
11                 .build();
12 
13         Trigger trigger = TriggerBuilder.newTrigger()
14                 .withIdentity("inject_trigger")
15                 .usingJobData("name", "Alice")
16                 .usingJobData("age",50)
17                 .startNow()
18                 .build();
19 
20         Scheduler scheduler = new StdSchedulerFactory().getScheduler();
21 
22         scheduler.start();
23         scheduler.scheduleJob(detail, trigger);
24         /*
25          * 2 秒鍾后關閉
26          */
27         Thread.sleep(2_000);
28         scheduler.shutdown();
29     }
30 
31     public static class DuplicatedData implements Job {
32 
33         private String name;
34 
35         private Integer age;
36 
37         public void setName(String name) {
38             this.name = name;
39         }
40 
41         public void setAge(Integer age) {
42             this.age = age;
43         }
44 
45         @Override
46         public void execute(JobExecutionContext context) throws JobExecutionException {
47             System.out.printf("hello, my name is %s , my age is %d \n", name, age);
48         }
49     }
50 }

 

可以觀察到我們在 JonDetail 中設置的 name 被 Trigger 中的替代掉了,新的由 Trigger 持有的 age 值正確的傳遞到了 age 屬性中:

 

4.2 我的 JobExecutionContext 究竟有什么

繼續上邊的代碼,讓我們在 execute 增加以下語句並打上斷點:

 

讓我們步入 org.quartz.core.JobRunShell#initialize 方法,這里根據 Scheduler,從 JobStore(此時是 RAMJobStore)獲取到的 TriggerFiredBundle 實例與 方法內實例化的 Job 實例創建了一個  JobExecutionContext:

 

 

 因此,當我們想要檢測被覆蓋的原有數據,可以用以下語句:

1         @Override
2         public void execute(JobExecutionContext context) throws JobExecutionException {
3             JobDataMap map = context.getJobDetail().getJobDataMap();
4             JobDataMap mapMerged = context.getMergedJobDataMap();
5             List<Map.Entry<String, Object>> duplicates = mapMerged.entrySet().stream().filter(en -> map.getWrappedMap().containsKey(en.getKey())).collect(Collectors.toList());
6             System.out.printf("hello, my name is %s , my age is %d \n", name, age);
7         }

 

 

5 數據傳遞中的坑

類型安全性:在喚起對應字段的 setter 方法時,Quartz 通過類檢查會保證數據的類型安全。

不可序列化錯誤:在喚起對應字段的 setter 方法時,Quartz  還檢查了 setter 對應的參數類型是否為基本類型(Primitive),如果是則會報錯

數據覆蓋:由於 JobDataMap 底層本質上使用 HashMap,所以后來的值會覆蓋原來的值。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM