Quartz Stateful Job 如果想在 Quartz 定时任务的每次执行过程中,对 JobDetail 的 JobDataMap 中的属性值进行更新,也就是 stateful 的任务,如不使用 @PersistJobDataAfterExecution
注解,稍后会涉及到,最初肯定是想获取上下文的 JobDetail.JobDataMap 引用,调用 put 方法进行覆盖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ScheduleJob implements Job { @Override public void execute (JobExecutionContext jobExecutionContext) throws JobExecutionException { JobDataMap dataMap = jobExecutionContext.getJobDetail().getJobDataMap(); String name = dataMap.getString("name" ); System.out.println("dataMap:" + dataMap.hashCode()); System.out.println("executionContext:" + jobExecutionContext.hashCode()); System.out.println(LocalDateTime.now() + " This is hi from " + name); dataMap.put("name" , name + " la" ); System.out.println(dataMap.getString("name" )); } }
结果是,每次执行时,获取到的 name 属性值还是 Jason,为此,对获取到的 dataMap
进行了 hashCode 值的打印,结果每次输出都是一样的,自认为每次 Job 执行时获取到的 JobDataMap 应用都指向同一个对象,为什么每次 put 不成功?
这个问题的答案在于,获取到的 JobDataMap 不是同一个对象,但返回的 hashCode 一样,是因为 JobDataMap 内部封装了一个 HashMap,HashMap hashCode 方法是对其中包含的每一个 Node(Entry) 中的 <K,V> 计算 HashCode 的值的加和,所以如果不同 HashMap 对象,存储相同的 K,V, 这两个 HashMap 的 hashCode 是相等,但实际上是两个不同的对象。
1 2 3 4 5 6 7 8 public int hashCode () { int h = 0 ; Iterator<Entry<K,V>> i = entrySet().iterator(); while (i.hasNext()) h += i.next().hashCode(); return h; }
再回到如何更新 JobDetail 中 JobDataMap 中的值,通过 debug , Job 每次执行的流程是,由 QuartzSchedulerThread
线程创建 JobRunShell
时,由于未使用持久化,使用默认的 RAMJobStore
,每次执行时,从 JobStore 中获取执行时的上下文数据,并将其封装在 TriggerFiredBundle
对象中,并且再初始化该对象时,返回 JobDetail 对象的一个克隆对象,对应的 JobDataMap 也是原始 JobDataMap 的一个克隆对象(浅拷贝 shallow copy ),最后将这些数据封装在 JobExecutionContext
中, JobShell(Runnanle 对象)
由工作线程执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public List<TriggerFiredResult> triggersFired (List<OperableTrigger> firedTriggers) { TriggerFiredBundle bndle = new TriggerFiredBundle(retrieveJob( tw.jobKey), trigger, cal, false , new Date(), trigger.getPreviousFireTime(), prevFireTime, trigger.getNextFireTime()); } public JobDetail retrieveJob (JobKey jobKey) { synchronized (lock) { JobWrapper jw = jobsByKey.get(jobKey); return (jw != null ) ? (JobDetail)jw.jobDetail.clone() : null ; } } @Override public Object clone () { JobDetailImpl copy; try { copy = (JobDetailImpl) super .clone(); if (jobDataMap != null ) { copy.jobDataMap = (JobDataMap) jobDataMap.clone(); } } catch (CloneNotSupportedException ex) { throw new IncompatibleClassChangeError("Not Cloneable." ); } return copy; }
上面的代码解释了为什么每次在 Job 执行时,对 JobDetail.JobDataMap 的字符串属性进行更新时,不起效,因为每次获取都是不同的 JobDataMap 对象,对克隆对象做的更新不能反映到原始的对象中,但是对引用类型的属性进行更新,可以生效,因为原始的克隆是浅拷贝(比如,在 JobDataMap 中使用一个 ArrayList, 并在每次任务执行时添加元素到这个 List 中)。
那么要如何实现对 JobDetail 中的 JobDataMap 进行更新,使得每次都可以更新一个状态,保存在里面呢?答案是使用 @PersistJobDataAfterExecution
注解,在 Job 执行结束时,会触发 JobStore 的 triggeredJobComplete
方法,做一些收尾工作, 其中会判断如果 JobClass 类上使用了该注解,会更新 JobStore 中保存的 JobDetial 和其 JobDataMap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void triggeredJobComplete (OperableTrigger trigger, JobDetail jobDetail, CompletedExecutionInstruction triggerInstCode) { if (jd.isPersistJobDataAfterExecution()) { JobDataMap newData = jobDetail.getJobDataMap(); if (newData != null ) { newData = (JobDataMap)newData.clone(); newData.clearDirtyFlag(); } jd = jd.getJobBuilder().setJobData(newData).build(); jw.jobDetail = jd; } }