Quartz定时任务框架(一):概述和Job、JobDetail详解
详解quartz定时任务框架,死磕
Quartz定时任务框架
核心概念
- Job
- JobDetail
- Trigger
- Scheduler
Job:
既然是定时任务,任务任务对吧,job就是你要定时干的事。比如就在控制台打印Hello World
JobDetail:
Job只是定义你要干什么,可以理解为模板不能直接用。需要创建一个Job实例,这个实例就是JobDetail。
Trigger:
触发器,规定怎么执行,5s一次还是10s一次。
Scheduler:
调度器、执行器。它可以根据触发器Trigger去执行Job
Hello World 入门
我们用结果驱动学习,先来一个Hello World
玩玩。
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
从上面的概念可以理解到:我们要输出Hello World
,这不就是一个任务(Job)吗?来:
Job是一个接口,我们写一个类继承即可:
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("Hello World!");
}
}
我滴任务完成啦~~~!!
任务就是这么简单。
任务有了,接下来就是触发器Trigger、调度器Scheduler。
写一个main方法就能搞定了:
public class TestScheduler {
public static void main(String[] args) throws SchedulerException {
// 创建调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 创建任务实例(传入job类)
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("hello", "job")
.build();
// 创建触发器
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger", "trigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
.build();
// 调度器接管任务和触发器
scheduler.scheduleJob(jobDetail, trigger);
// 开始执行
scheduler.start();
}
}
我们现在只需要知道,现在每5s钟,控制台就会打印Hello World
我们现在不要去纠结那些方法是干嘛的(让结果驱动学习)。
可以看见触发器Trigger和任务Job都是有一个xxxBuilder的构建对象!!!!
调度器则是有一个工厂类获取的。
一切就是这么简单。
动手操作一遍,运行main方法:
接下来,我们慢慢深入
深入学习
原理概述
Job为一个接口,只有一个方法:
public interface Job {
void execute(JobExecutionContext var1) throws JobExecutionException;
}
属实很朴素。
job的一个 trigger 被触发后(稍后会讲到),execute() 方法会被 scheduler 的一个工作线程调用;传递给 execute() 方法的 JobExecutionContext 对象中保存着该 job 运行时的一些信息 ,执行 job 的 scheduler 的引用,触发 job 的 trigger 的引用,JobDetail 对象引用,以及一些其它信息。(当job的触发器被触发时,scheduler调度器的一个线程会执行job的execute方法,同时会传递一些参数)
JobDetail 对象是在将 job 加入 scheduler 时,由客户端程序(你的程序)创建的。它包含 job 的各种属性设置,以及用于存储 job 实例状态信息的 JobDataMap
Trigger 用于触发 Job 的执行。当你准备调度一个 job 时,你创建一个 Trigger 的实例,然后设置调度相关的属性。Trigger 也有一个相关联的 JobDataMap,用于给 Job 传递一些触发相关的参数。Quartz 自带了各种不同类型的 Trigger,最常用的主要是 SimpleTrigger 和 CronTrigger。
SimpleTrigger 主要用于一次性执行的 Job(只在某个特定的时间点执行一次),或者 Job 在特定的时间点执行,重复执行 N 次,每次执行间隔T个时间单位。CronTrigger 在基于日历的调度上非常有用,如“每个星期五的正午”,或者“每月的第十天的上午 10:15”等。
为什么既有 Job,又有 Trigger 呢?很多任务调度器并不区分 Job 和 Trigger。有些调度器只是简单地通过一个执行时间和一些 job 标识符来定义一个 Job;其它的一些调度器将 Quartz 的 Job 和 Trigger 对象合二为一。在开发 Quartz 的时候,我们认为将调度和要调度的任务分离是合理的。在我们看来,这可以带来很多好处。
例如,Job 被创建后,可以保存在 Scheduler 中,与 Trigger 是独立的,同一个 Job可以有多个 Trigger;这种松耦合的另一个好处是,当与 Scheduler 中的 Job 关联的 trigger 都过期时,可以配置 Job 稍后被重新调度,而不用重新定义 Job;还有,可以修改或者替换 Trigger,而不用重新定义与之关联的 Job。
将 Job 和 Trigger 注册到 Scheduler 时,可以为它们设置 key,配置其身份属性。
Job 和 Trigger 的 key(JobKey 和 TriggerKey)可以用于将 Job 和 Trigger 放到不同的分组(group)里,然后基于分组进行操作。同一个分组下的 Job 或 Trigger 的名称必须唯一,即一个 Job 或 Trigger 的 key 由名称(name)和分组(group)组成。
Job和JobDetail
当你定义了一个实现Job接口的类,这个类仅仅表明该job需要完成什么任务。
除此之外,Quartz还需要知道该Job实例所包含的属性而这将由JobDetail类来完成。
不妨再看看HelloWorld中的代码:
// 创建任务实例(传入job类)
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("hello", "job")
.build();
// 创建触发器
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger", "trigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
.build();
// 调度器接管任务和触发器
scheduler.scheduleJob(jobDetail, trigger);
因为我们在创建JobDetail对象的时候,把要执行的Job类型传递了进来。
而JobDetail对象又和触发器一起交给了调度器Scheduler。
所以,调度器Scheduler知道将要执行的任务类型。
只不过,调度器每次执行这个任务的时候,每次都会重新创建任务对象。
这个时候我们思考一个问题:如何给Job对象添加属性来追踪job的状态呢?
答案是:否,因为每次执行都会创建一个新的job任务对象,所以无法直接在任务中添加什么属性。
但是,你是否还记得前文原理概述时说过:
public interface Job {
void execute(JobExecutionContext var1) throws JobExecutionException;
}
JobExecutionContext这个参数:Scheduler执行任务execute方法时,会传递很多的参数,包括JobDetail的对象。
都在这个参数中。
不知道此时你有没有恍然大悟。
既然无法直接在任务上动手脚,就用一个外部对象来,每次执行任务的时候,都传给你这个任务。这个外部对象就是JobDetail,而需要增加的属性就放在JobDetail对象的JobDataMap中
我们在execute方法中打个断点,看看JobExecutionContext都有些啥:
JobDataMap
JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据。JobDataMap是Java Map接口的一个实现,额外增加了一些便于存取基本类型的数据的方法
我们就来试试,稍微改动下HelloWorld代码:
// 创建任务实例
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("hello", "job")
.usingJobData("xp","好帅好帅")
.build();
只需要加上.usingJobData("xp","好帅好帅")
然后我们去任务的execute方法中取这个参数:
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
String xp = jobDataMap.getString("xp");
System.out.println("Hello World!" + xp);
}
效果:
在决定JobDataMap中存放什么数据的时候需要小心,因为JobDataMap中存储的对象都会被序列化,因此很可能会导致类的版本不一致的问题;Java的标准类型都很安全,如果你已经有了一个类的序列化后的实例,某个时候,别人修改了该类的定义,此时你需要确保对类的修改没有破坏兼容性
当然,你也可以做一些配置。使得map中仅允许存储基本类型和String类型的数据,这样可以避免后续的序列化问题。
JobExecutionContext中的JobDataMap为我们提供了很多的便利。它是JobDetail中的JobDataMap和Trigger中的JobDataMap的并集,但是如果存在相同的数据,则后者会覆盖前者的值。(打上断点,细心的你可能会发现JobExecutionContext中有好几个jobDataMap)
在execute方法中,可以合并这几个jobDataMap为一个。
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap mergedJobDataMap = jobExecutionContext.getMergedJobDataMap();
String xp = mergedJobDataMap.getString("xp");
System.out.println("Hello World!" + xp);
}
同样也能拿到值,而且这样很方便。不用一会从这个jobDataMap拿数据,一会从另一个拿。
如果你在job类中,为JobDataMap中存储的数据的key增加set方法,那么Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样你就不需要在execute()方法中显式地从map中取数据了。
不明白?上代码,就拿我保存的xp
这个值来说
public class MyJob implements Job {
private String xp;
/** set方法 */
public void setXp(String value) {
this.xp = value;
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("Hello World!" + xp);
}
}
运行效果:
可以看到,只需要在job类中定义这个属性,添加set方法。就直接帮我们赋值了。
直接使用,简直不要太方便。
Job实例
你可以只创建一个job类,然后创建多个与该job关联的JobDetail实例,每一个实例都有自己的属性集和JobDataMap,最后,将所有的实例都加到scheduler中。
比如,你创建了一个实现Job接口的类“SalesReportJob”。该job需要一个参数(通过JobdataMap传入),表示负责该销售报告的销售员的名字。因此,你可以创建该job的多个实例(JobDetail),比如“SalesReportForJoe”、“SalesReportForMike”,将“joe”和“mike”作为JobDataMap的数据传给对应的job实例。
当一个trigger被触发时,与之关联的JobDetail实例会被加载,JobDetail引用的job类通过配置在Scheduler上的JobFactory进行初始化。默认的JobFactory实现,仅仅是调用job类的newInstance()方法,然后尝试调用JobDataMap中的key的setter方法。你也可以创建自己的JobFactory实现,比如让你的IOC或DI容器可以创建/初始化job实例。
一个任务(Job类)可以有多个任务实例(JobDetail),即:
// 创建任务实例
JobDetail jobDetailA = JobBuilder.newJob(MyJob.class)
.withIdentity("helloA", "job")
.usingJobData("xpA","好帅好帅")
.build();
JobDetail jobDetailB = JobBuilder.newJob(MyJob.class)
.withIdentity("helloB", "job")
.usingJobData("xpB","你小子真流弊")
.build();
但是一个任务实例(JobDetail)只能和一个Trigger对应!
Job的数据状态和并发
关于job的状态数据(即JobDataMap)和并发性,还有一些地方需要注意。
@DisallowConcurrentExecution
:将该注解加到job类上,告诉Quartz不要并发地执行同一个job定义(这里指特定的job类)的多个实例
@DisallowConcurrentExecution
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("Hello World!");
}
}
该注解限制是针对JobDetail的,而不是job类
@PersistJobDataAfterExecution
:将该注解加在job类上,告诉Quartz在成功执行了job类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。和 @DisallowConcurrentExecution
注解一样,尽管注解是加在job类上的,但其限制作用是针对job实例的,而不是job类的。
如果你使用了@PersistJobDataAfterExecution
注解,我们强烈建议你同时使用@DisallowConcurrentExecution
注解,因为当同一个job类的两个实例被并发执行时,由于竞争,JobDataMap中存储的数据很可能是不确定的。
Job的其它特性
通过JobDetail对象,可以给job实例配置的其它属性有:
- Durability:如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的存在与否决定的;
- RequestsRecovery:如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。
// 创建任务实例
JobDetail jobDetailA = JobBuilder.newJob(MyJob.class)
.requestRecovery(false) // 是否可恢复
.storeDurably(false) // 是否持久
.withIdentity("helloA", "job")
.usingJobData("xpA","好帅好帅")
.build();
由于篇幅原因,打算分开些。不然一篇得很长了。这里只是说完了Job和JobDetail,后续更精彩。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)