【一篇文章就够了】作业框架Quartz
资源
- Quartz官方文档
- Maven 地址
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- spring quartz 框架 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.5.RELEASE</version>
<scope>test</scope>
</dependency>
Quartz 的简单事例
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
Object tv1 = context.getTrigger().getJobDataMap().get("t1");
Object tv2 = context.getTrigger().getJobDataMap().get("t2");
Object jv1 = context.getJobDetail().getJobDataMap().get("j1");
Object jv2 = context.getJobDetail().getJobDataMap().get("j2");
Object sv = null;
try {
sv = context.getScheduler().getContext().get("skey");
} catch (SchedulerException e) {
e.printStackTrace();
}
System.out.println(tv1+":"+tv2);
System.out.println(jv1+":"+jv2);
System.out.println(sv);
System.out.println("hello:"+LocalDateTime.now());
}
}
public class Test {
public static void main(String[] args) throws SchedulerException {
//创建一个scheduler
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.getContext().put("skey", "svalue");
//创建一个Trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.usingJobData("t1", "tv1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3)
.repeatForever()).build();
trigger.getJobDataMap().put("t2", "tv2");
//创建一个job
JobDetail job = JobBuilder.newJob(HelloJob.class)
.usingJobData("j1", "jv1")
.withIdentity("myjob", "mygroup").build();
job.getJobDataMap().put("j2", "jv2");
//注册trigger并启动scheduler
scheduler.scheduleJob(job,trigger);
scheduler.start();
}
}
Quartz API
Quartz API的关键接口是:
- Scheduler - 与调度程序交互的主要API。
- Job - 你想要调度器执行的任务组件需要实现的接口
- JobDetail - 用于定义作业的实例。
- Trigger(即触发器) - 定义执行给定作业的计划的组件。
- JobBuilder - 用于定义/构建 JobDetail 实例,用于定义作业的实例。
- TriggerBuilder - 用于定义/构建触发器实例。
- Scheduler 的生命期,从 SchedulerFactory 创建它时开始,到 Scheduler 调用shutdown() 方法时结束;Scheduler 被创建后,可以增加、删除和列举 Job 和 Trigger,以及执行其它与调度相关的操作(如暂停 Trigger)。但是,Scheduler 只有在调用 start() 方法后,才会真正地触发 trigger(即执行 job),见教程一。
Quartz 提供的“builder”类,可以认为是一种领域特定语言(DSL,Domain Specific Language)。教程一中有相关示例,这里是其中的代码片段:(校对注:这种级联的 API 非常方便用户使用,大家以后写对外接口时也可以使用这种方式)
我觉得Quartz有三个非常关键的内容,Job(作业)、Trigger(触发器)、Scheduler(调度表)。作业就是我要要执行的内容,触发器就是对作业触发的条件,例如时间、次数等进行约束,调度表就是将二者联系起来。
Quartz中Job和Trigger是分开定义的,这样的好处就是可以一个Job对应不同Trigger,或者一个Trigger对应不同Job,是一定程度上的解耦合。Job 和 Trigger 注册到 Scheduler 时,可以为它们设置 key,配置其身份属性,key 由名称(name)和分组(group)组成,所以可以分组进行操作。
Job与JobDetail介绍
可以看到,我们传给scheduler一个JobDetail实例,因为我们在创建JobDetail时,将要执行的job的类名传给了JobDetail,所以scheduler就知道了要执行何种类型的job;每次当scheduler执行job时,在调用其execute(…)方法之前会创建该类的一个新的实例;执行完毕,对该实例的引用就被丢弃了,实例会被垃圾回收;这种执行策略带来的一个后果是,job必须有一个无参的构造函数(当使用默认的JobFactory时);另一个后果是,在job类中,不应该定义有状态的数据属性,因为在job的多次执行中,这些属性的值不会保留。
可以看出Quartz框架并不直接操作Job接口,而是使用JobDetail来实例化一个Job。这样做的意义笔者觉得可能和多线程有关,让我们接着往下看吧。但是要注意的是:
- job必须有一个无参的构造函数(当使用默认的JobFactory时)
- 在job类中,不应该定义有状态的数据属性
那么如何给job实例增加属性或配置呢?JobDataMap。
JobDataMap
方法一:在构建JobDetail时,可以将数据放入JobDataMap。
// define the job and tie it to our DumbJob class
JobDetail job = newJob(DumbJob.class)
.withIdentity("myJob", "group1") // name "myJob", group "group1"
.usingJobData("jobSays", "Hello World!")
.usingJobData("myFloatValue", 3.141f)
.build();
在job的执行过程中,可以从JobDataMap中取出数据,如下示例:
public class DumbJob implements Job {
public DumbJob() {
}
public void execute(JobExecutionContext context)
throws JobExecutionException
{
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String jobSays = dataMap.getString("jobSays");
float myFloatValue = dataMap.getFloat("myFloatValue");
System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
}
}
方法二:如果你在job类中,为JobDataMap中存储的数据的key增加set方法(如在上面示例中,增加setJobSays(String val)方法),那么Quartz的默认JobFactory实现在job被实例化的时候会自动调用这些set方法,这样你就不需要在execute()方法中显式地从map中取数据了。
public class DumbJob implements Job {
String jobSays;
float myFloatValue;
ArrayList state;
public DumbJob() {
}
public void execute(JobExecutionContext context)
throws JobExecutionException
{
JobKey key = context.getJobDetail().getKey();
JobDataMap dataMap = context.getMergedJobDataMap(); // Note the difference from the previous example
state.add(new Date());
System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
}
public void setJobSays(String jobSays) {
this.jobSays = jobSays;
}
public void setMyFloatValue(float myFloatValue) {
myFloatValue = myFloatValue;
}
public void setState(ArrayList state) {
state = state;
}
}
这里就是JobDetail的含义了,每个JobDetail实例可以有自己的参数,然后执行。是高内聚,低耦合的体现。
Job状态与并发
@DisallowConcurrentExecution:将该注解加到job类上,告诉Quartz不要并发地执行同一个job定义(这里指特定的job类)的多个实例。请注意这里的用词。
@PersistJobDataAfterExecution:将该注解加在job类上,告诉Quartz在成功执行了job类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该job(即JobDetail)在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据。
Job的其它特性
通过JobDetail对象,可以给job实例配置的其它属性有:
- Durability:如果一个job是非持久的,当没有活跃的trigger与之关联的时候,会被自动地从scheduler中删除。也就是说,非持久的job的生命期是由trigger的存在与否决定的;
- RequestsRecovery:如果一个job是可恢复的,并且在其执行的时候,scheduler发生硬关闭(hard shutdown)(比如运行的进程崩溃了,或者关机了),则当scheduler重新启动的时候,该job会被重新执行。此时,该job的JobExecutionContext.isRecovering() 返回true。
- JobExecutionException:最后,是关于Job.execute(..)方法的一些额外细节。execute方法中仅允许抛出一种类型的异常(包括RuntimeExceptions),即JobExecutionException。因此,你应该将execute方法中的所有内容都放到一个”try-catch”块中。
Quartz中Triggers介绍
最常用的两种trigger——SimpleTrigger和CronTrigger。
Trigger的公共属性
Trigger的公共属性,在构建trigger的时候可以通过TriggerBuilder设置。
- TriggerKey:所有类型的trigger都有TriggerKey这个属性,表示trigger的身份。
- jobKey属性:当trigger触发时被执行的job的身份;
- startTime属性:设置trigger第一次触发的时间;该属性的值是java.util.Date类型,表示某个指定的时间点;有些类型的trigger,会在设置的startTime时立即触发,有些类型的trigger,表示其触发是在startTime之后开始生效。比如,现在是1月份,你设置了一个trigger–“在每个月的第5天执行”,然后你将startTime属性设置为4月1号,则该trigger第一次触发会是在几个月以后了(即4月5号)。
- endTime属性:表示trigger失效的时间点。比如,”每月第5天执行”的trigger,如果其endTime是7月1号,则其最后一次执行时间是6月5号。
优先级(priority)
当Quartz资源发生冲突的时候,priority参数用来确定优先级。默认为5,priority属性的值可以是任意整数,正数、负数都可以。
注意:只有同时触发的trigger之间才会比较优先级。10:59触发的trigger总是在11:00触发的trigger之前执行。
注意:如果trigger是可恢复的,在恢复后再调度时,优先级与原trigger是一样的。
错过触发(misfire Instructions)
如果应为特殊原因,没有触发一些作业,就是错误触发(misfire),不同类型的trigger,有不同的misfire机制。它们默认都使用“智能机制(smart policy)”。这些misfire机制在JavaDoc中有说明。
日历示例(calendar)
Quartz的Calendar对象(不是java.util.Calendar对象)可以在定义和存储trigger的时候与trigger进行关联。Calendar用于从trigger的调度计划中排除时间段。比如,可以创建一个trigger,每个工作日的上午9:30执行,然后增加一个Calendar,排除掉所有的商业节日。
Simple Trigger
SimpleTrigger可以满足的调度需求是:在具体的时间点执行一次,或者在具体的时间点执行,并且以指定的间隔重复执行若干次。比如,你有一个trigger,你可以设置它在2015年1月13日的上午11:23:54准时触发,或者在这个时间点触发,并且每隔2秒触发一次,一共重复5次。
SimpleTrigger的属性包括:开始时间、结束时间、重复次数以及重复的间隔。
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
.startNow()//立即生效
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)//每隔1s执行一次
.repeatForever())//重复执行
.endAt(DateBuilder.dateOf(22, 0, 0))//22时停止
.forJob("job1", "group1")
.build();//一直执行
CronTrigger
和SimpleTrigger一样,CronTrigger有一个startTime,它指定何时生效,以及一个(可选的)endTime,用于指定何时停止计划。CronTrigger通常比Simple Trigger更有用,如果您需要基于日历的概念而不是按照SimpleTrigger的精确指定间隔进行重新启动的作业启动计划。
Cron-Expressions用于配置CronTrigger的实例。Cron Expressions是由七个子表达式组成的字符串,用于描述日程表的各个细节。这些子表达式用空格分隔,并表示:
- Seconds
- Minutes
- Hours
- Day-of-Month
- Month
- Day-of-Week
- Year (optional field)
一个完整的Cron-Expressions的例子是字符串“0 0 12 ?* WED”这意味着“每个星期三下午12:00”。
其中单个子表达式可以使用:
通配符*代表该字段的每个可能的值,例如,“星期几”字段中的“*”显然意味着“每周的每一天”;
/代表可用于指定值的增量,例如,如果在“分钟”字段中输入“0/15”,则表示“每隔15分钟,从零开始”;
-代表前后连接的两个数字组成的区间,例如,“MON-FRI”,表示周一到周五;
连接符,代表不同规则的值的连接,例如,如果在“秒中”字段中输入“0,1,3-10”,意味着“每分钟的0,1和3到10秒开始”;
?字符是允许的日期和星期几字段。用于指定“无特定值”。当您需要在两个字段中的一个字段中指定某个字符而不是另一个字段时,这很有用;
L字符允许用于月日和星期几字段。代表最后,但是在这两个领域的每一个领域都有不同的含义。例如,“月”字段中的“L”表示“月的最后一天” 。如果在本周的某一天使用,它只是意味着“7”或“SAT”。但是如果在星期几的领域中再次使用这个值,就意味着“最后一个月的xxx日”,例如“6L”或“FRIL”都意味着“月的最后一个星期五”。您还可以指定从该月最后一天的偏移量,例如“L-3”,这意味着日历月份的第三个到最后一天。当使用'L'选项时,重要的是不要指定列表或值的范围,因为您会得到混乱/意外的结果;
W代表指定最*给定日期的工作日(星期一至星期五)。例如,如果要将“15W”指定为月日期字段的值,则意思是:“最*的*日到当月15日”;
#用于指定本月的“第n个”XXX工作日。例如,“星期几”字段中的“6#3”或“FRI#3”的值表示“本月的第三个星期五”。
附上cron表达式生成网址,妈妈再也不用担心。
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(10, 42))
.forJob("job1", "group1")
.build();
SpringBoot 整合Quartz
- 第一步,先引入Quartz框架
<!-- quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- spring quartz 框架 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.5.RELEASE</version>
<scope>test</scope>
</dependency>
- 第二步,创建job
public class MyJobOne extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println(this.getClass().getCanonicalName());
}
}
- 第三步,在schedule中创建trigger,并和job简历关联
@Configuration
public class MySchedule {
@Bean
public JobDetail autoMyJobOne() {
return JobBuilder.newJob(MyJobOne.class).withIdentity("MyJobOne").storeDurably().build();
}
@Bean
public Trigger autoMyJobOneTrigger() {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(" 0/1 * * * * ? *");
return TriggerBuilder.newTrigger().forJob(autoMyJobOne()).withSchedule(cronScheduleBuilder).withDescription("MyJobOne").build();
}
}
注意将MySchedule标上注解@Configuration,同时将jobdetail和trigger的生成方法作为bean注入。
轻量级分布式任务调度*台——XXL-JOB
XXL-JOB是一个分布式任务调度*台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。是Quartz的替代品。

浙公网安备 33010602011771号