提示:本文中部分内容图片节选自互联网,无意冒犯。如有侵权请私信联系作者即刻删除、更改。
Job任务接口
Job接口是自定义任务逻辑的规范,Quartz规定任务类必须实现Job接口。这是因为Schduler会通过此接口的execute方法回调具体的任务逻辑完成任务调度。Job接口中的execute()方法类似TimeTask类中的run()方法,是任务执行的入口。这是二十三种设计模式中策略模式的一种实现,如下例是一个自定义的Job接口实现类:
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("Hello"+simpleDateFormat.format(new Date()));
}
}
如上例中,execute()方法中有一个JobExecutionContext类型的参数context。JobExecutionContext是一个包含了各种上下文信息的对象接口,在执行Job的execute()方法时被传入。指向执行中的JobDetail 实例和执行完成的Trigger实例。
JobExecutionContext中有一个简便的方法可以获取执行上下文中的JobDataMap , 它是JobDetail 中的JobDataMap 和 Trigger 中的JobDataMap 的合并,后者的JobDataMap 会覆盖前者相同名称的值.
注意: 不要期望通过这个返回的JobDataMap 的set方法设置 、某个值持久化到作业自身的JobDataMap ,即使有@PersistJobDataAfterExecution。试图更改此映射的内容通常会导致IllegalStateException 异常。
JobExecutionContext接口
JobExecutionContext接口包含获取触发器、调度器以及MapData与数据的设置的方法规范,如同它名字它包含了任务执行时的一些上下文信息。它的方法较多这里就不一一列举,下边列举几个常用的方法如下:
- public Scheduler getScheduler()
获取执行此任务的Scheduler调度器对象。 - public Trigger getTrigger();
获取此任务绑定的触发器对象 - public Calendar getCalendar();
获取触发器定义的任务执行时间 - public boolean isRecovering();
该任务是否是暂停后,又恢复运行的状态。 - public TriggerKey getRecoveringTriggerKey() throws IllegalStateException;
返回现在被恢复运行的触发器的TriggerKey,TriggerKey中包含了触发器名称、触发器组名称的一些相关信息。
还有获取执行时间、下次执行时间、执行次数等方法,使用时可以查看源码或者相关API。此参数的作用在于获取JobDataMap中的数据以及运行Job时的一些环境信息,通常配合JobDataMap将一些状态传递给Job,具体的示例请看下一章节JobDataMap。
JobDataMap
JobDataMap用于存储可序列化的对象,可以为任务执行时传递信息。它实现了java.util.Map接口,并由Quartz框架添加了一些便于存取数据的方法。向JobDataMap中存入数据的方法有两种:
第一种在JobBuilder中调用相应方法进行存取:
- public JobBuilder setJobData(JobDataMap newJobDataMap)
- public JobBuilder usingJobData(JobDataMap newJobDataMap)
- public JobBuilder usingJobData(String dataKey, Boolean value)
- public JobBuilder usingJobData(String dataKey, Double value)
…
如下例中使用usingJobData方法为JobDataMap添加数据:
final JobDetail detail = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1")
.usingJobData("cout",0)
.usingJobData("couter",0)
.build();
第二种就是在Job的实现类中添加变量,如果想在外部修改变量的值,只需要使用JobDataMap使用相同的key值覆盖即可。
public class HelloJob implements Job {
private Integer cout;
public void setCout(int cout) {
this.cout = cout;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
cout++;
int couter = (int) context.getJobDetail().getJobDataMap().get("couter");
System.out.println(String.format("HelloJob时间:%s,count:%d。counter:%d", simpleDateFormat.format(new Date()), cout,couter++));
context.getJobDetail().getJobDataMap().put("cout", cout);
context.getJobDetail().getJobDataMap().put("couter", couter);
}
}
需要注意如遇到同名的key值Trigger会覆盖Job中的值,尽量不要使用同名的键作为属性标识。
JobDetail
虽说Job是任务逻辑的规范,任务逻辑也是定义在自定义Job接口实现类中,但是Scheduler调度中与之交互的对象却非你定义的Job实现了,而是本章介绍的JobDetail实例。obDetail 的作用是封装 Job信息,它为 Job 添加了许多扩展参数。如:name、group、jobClass、jobDataMap等。
JobDetail定义的是任务数据,而真正执行的任务逻辑是在Job中。这样做的好处在于防止并发任务带来一些不必要的麻烦。因为使用JobDetail方式,Sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样Job实例就会被线程私有,依次规避并发访问的问题。如果Scheduler直接使用Job,就可能会存在对同一个Job实例并发访问的问题。
JobDetail是通过JobBuilder类创建的。它的获取方式如下:
JobDetail detail = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1")
.build();
withIdetity用于定义job的相关信息,如果未指定组的名称将会使用默认的DEFAULT组,需要注意job名称必须唯一。
除此之外JobBuilder中还提供了众多设置job属性相关的方法,如任务描述、设置Data属性的重载等方法,感兴趣的小伙伴可以查看JobBuilder的源码注释了解个方法的使用或者查看官网的JavaDOC。
Job的状态
由上例可知一个Job实例在Quartz中的生命周期只限于一次任务调度。每次调度器执行Job时,会首先创建一个新的Job实例,当调用完成后,关联Job对象的实例就会被释放,进而被垃圾回收器回收。
由此可知,默认Job实例是无状态的。因为在Job调用期间持有的状态信息都是存放在JobDataMap中,而每次调用Job时都会创建一个新的JobDataMap。如果想要保持Job中的属性到下一次任务需要使用@PersistJobDataAfterExecution注解将Job持久化。如下例添加一个cout参数作为定时任务的执行次数:
@PersistJobDataAfterExecution//持久化JobDataMap对象
@DisallowConcurrentExecution//不允许并发执行
public class HelloJob implements Job {
private Integer cout;
public void setCout(int cout) {
this.cout = cout;
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
cout++;
int couter = (int) context.getJobDetail().getJobDataMap().get("couter");
System.out.println(String.format("HelloJob时间:%s,count:%d。counter:%d", simpleDateFormat.format(new Date()), cout,couter++));
context.getJobDetail().getJobDataMap().put("cout", cout);
context.getJobDetail().getJobDataMap().put("couter", couter);
}
}
需要注意到例中使用的@DisallowConcurrentExecution注解,通过对JobDetail讲解可知Quartz正是通过这种无状态的Job避免多线程并发时带来的安全行问题。所以当你决定让一个Job变为有状态时,它就已经不适合多线程了,需要使用@DisallowConcurrentExecution注解以禁用此任务被多线程执行。测试代码如下:
public static void main(String[] args) throws SchedulerException {
final JobDetail detail = JobBuilder.newJob(HelloJob.class)
.withIdentity("job1", "group1")
.usingJobData("cout",0)
.usingJobData("couter",0)
.build();
final SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatForever().withIntervalInSeconds(2))
.startNow()
.build();
final Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(detail,trigger);
scheduler.start();
}
运行结果如下:
Job无状态任务在执行时,每次任务都会新建JobDataMap,所以数据的更改不会影响下次的执行。而有状态任务共享同一个JobDataMap实例,每次任务执行对JobDataMap所做的更改都会保存下来,每次执行任务后都会对后面的执行发生影响。正因为这个原因,无状态的Job可以并发执行,而有状态的Job不能并发执行,这意味着如果前次的Job还没有执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务需要考虑更多的因素,程序往往拥有更高的复杂度,因此除非必要,应该尽量使用无状态的Job。
另外如果Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。
另外还有一种过时的写法就是无状态Job继承Job接口,有状态Job继承StatefulJob,不过官方并不推荐使用。