Quartz 定时任务框架详解

1.Quartz 体系结构

Quartz 设计有四个核心类,分别是Scheduler(调度器)、Job(任务) 、Trigger(触发器)、JobDetail(任务详情),他们是使用Quartz的关键。


1.1 Job

Job:定义需要执行的任务,该类是一个接口,只定义了一个方法execute(JobExecutionContext context),在实现类的execute方法中编写所需要定时执行的Job(任务),JobExcutionContext类提供了调度应用的一些信息。Job运行时的信息保存在JobDataMap实例中。

1.2 Trigger

Trigger:负责设置调度策略。该类是一个接口,描述触发job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当且仅当需调度一次或者以固定时间间隔周期执行调度,SimpleTrigger 是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如在周一到周五的15:00 ~ 16:00 执行调度等。

1.3 Scheduler

Scheduler:调度器就相当于一个容器,装载着任务和触发器。该类是一个接口。代表一个Quartz的独立运行容器。Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据。

1.4 JobDetail

JobDetail:描述Job的实现类及其它相关的静态信息,如:Job名字、描述、关联监听器等信息。Quartz每次调度Job时,都重新创建一个Job实例,它接受一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。

1.5 ThreadPool

Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。
Job有一个 StatefulJob 子接口(Quartz2后用@PersistJobDataAfterExecution注解代替),代表有状态的任务,改接口是一个没有方法的标签接口,其目的是让Quartz知道任务的类型,以便采用不同的执行方案。

  • 无状态任务在执行时拥有自己的 JobDataMap 拷贝,对 JobDataMap 的更改不会影响下次的执行。

  • 有状态任务共享同一个 JobDataMap 实例,每次任务执行对 JobDataMap 所做的更改会保存下来,后面的执行可以看到这个更改,也即每次执行任务后都会对后面的执行发生影响。

正因为这个原因,无状态的Job能并发执行,而有状态的StatefulJob不能并发执行。

1.6 Listener

Listener:Quartz拥有完善的事件和监听体系,大部分组件都拥有事件,如:JobListener监听任务执行前事件、任务执行后事件;TriggerListener监听触发前事件,出发后事件;TriggerListener监听调度开始事件,关闭事件等等,可以注册响应的监听器处理感兴趣的事件。


2.cronExpression 表达式

格式:[秒][分][时][每月的第几日][月][每周的第几日][年]

字段名 必填 允许值 允许特殊符号
Seconds YES 0 - 59 , - * /
Minutes YES 0- 59 , - * /
Hours YES 0-23 , - * /
Day of month YES 1-31 , - * ? / L W
Month YES 1-12 or JAN-DEC , - * /
Day of Week YES 1-7 or SUN-SAT , - * ? / L #
Year NO empty, 1970-2099 , - * /

特殊字符说明:

字段 含义
* 用于指定字段中所有值。比如:*在分钟中表示每一分钟
? 用于指定日期中的某一天,或是星期中的某一个星期
- 用于指定范围。比如:10-12在小时中表示 10点、11点、12点
, 用于指定额外的值。比如:MON,WED,FRI在日期中表示星期一,星期三,星期五
/ 用于指定增量。比如:0/15在秒中表示0秒,15秒,30秒,45秒5/15在秒钟表示 5秒,20秒,35秒,50秒
L 在两个字段中拥有不同含义。比如L在日期(Day of month)表示某月的最后一天。在星期(Day of week)只表示7SAT。但是,值L在星期(Day of week)中表示某月的最后一个星期几。比如6L表示某月的最后一个星期五。也可以在日期中(Day of month)中指定一个偏移量(从该月最后一天开始)。比如L-3表示某月的倒数第三天
W 用于指定工作日(星期一到星期五) 比如:15W在日期中表示到15号的最近一个工作日。如果15号是周六,那么触发器的触发在14号星期五。如果15号是周日,触发器的触发在15号周一。如果十五号是星期二,那么它就会在15号周二开始执行。然而 如果指定1W 并且1号是星期六,那么触发器的触发在3号周一,因为他不会“jump”过一个月的日子边界。
LW 可以在日期(day-of-month)合使用,表示月份的最后一个工作日
# 用于指定月份中的第几天。比如:6#3表示月份的第三个星期五(day 6 = Friday and "#3" = the 3rd one in the month)。其它的有,2#1表示月份第一个星期一4#5表示月份第五个星期三。注意:如果只是指定#5,则触发器在月份中不会触发。

注意:字符不区分大小写,MON,mon相同

2.1 cornExpression示例

表达式 含义
0 0 12 * * ? 每天12点
0 15 10 ? * * 每天上午10点 15
0 15 10 * * ? 每天上午10点 15
0 15 10 * * ? * 每天上午10点 15
0 15 10 * * ? 2005 在2005年中每天的上午10点15
0 * 14 * * ? 每天14点-14点59分的每分钟
0 0/5 14 * * ? 每天14点 到14点55 每5分钟执行一次
0 0/5 14,18 * * ? 每天14点到14点55 18点到18点55 每5分钟执行一次
0 0-5 14 * * ? 每天14点到14点5分 的每分钟
0 10,44 14 ? 3 WED 3月每个星期三的 14点10分和44分
0 15 10 ? * MON-FRI 每周一到周五的10点15分
0 15 10 15 * ? 每月15号的10点15分
0 15 10 L * ? 每月的最后一天的10点15分
0 15 10 L-2 * ? 每月最后两天的10点15分(每月的倒数第二天的10点15分???(需验证))
0 15 10 ? * 6L 每月的最后一个周五10点15分
0 15 10 ? * 6L 2002-2005 02-05年 每月最后一个周五10点15
0 15 10 ? * 6#3 每月第三周的周五10点15分
0 0 12 1/5 * ? 每月1号到31号 每隔5天 12点0分
0 11 11 11 11 ? 每年11月11日11点11分

``


3.Listener示例

3.1注册对特定作业的JobListener

scheduler.getListenerManager().addJobListener(
          new MyJobListener(),KeyMatcher.keyEquals(new JobKey("job1","group1")));

3.2注册对特定组的所有作业的JobListener

scheduler.getListenManager().addJobListener(
          new MyJobListener(),GroupMatcher.jobGroupEquals("group1"));

3.3注册对两个特定组的所有作业的JobListener

scheduler.getListenManager().addJobListener(
          new MyJobListener(),OrMatcher.or(
                  GroupMatcher.jobGroupEquals("group1"),
                  GroupMatcher.jobGroupEquals("group2")));

3.4 注册一个对所有作业的JobListener

sched.getListenerManager().addJobListener(
          new MyJobListener(),EverythingMatcher.allJobs());

3.5 JobListener 实现类

public class MyJobListener implements JobListener {

    @Override
    public String getName() {
        return "MyJobListener"; // 一定要设置名称
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {

    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {

    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        if (jobException != null) {
            try {
                // 立即关闭调度器
                context.getScheduler().shutdown();
                System.out.println("Error occurs when executing jobs, shut down the scheduler.");
                // 给管理员发送邮件...
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
        }
    }
}

4.SchedulerListener 示例

SchedulerListener 在调度程序的Scheduler中注册。SchedulerListener 可以运行任何实现 org.quartz.SchedulerListener 接口的对象。

4.1 添加调度器的 SchedulerListener:

scheduler.getListenerManager().addSchedulerListener(mySchedListener);

4.2 删除调度器的 SchedulerListener:

scheduler.getListenerManager().removeSchedulerListener(mySchedListener);

5.多线程并发执行与数据共享

5.1禁止同一个JobDetail中的多个实例并发执行

Quartz定时任务默认都是并发执行的。不会等待上一次任务执行完毕,只要间隔时间到了就会执行,如果定时任务执行太长,会长时间占用资源。导致其它任务堵塞。(quartz是用一个线程池去执行的。线程池有大小。如果同一任务并发执行过多。影响线程池其它任务执行)

禁止并发执行的意思并不是不能同时执行多个Job,而是不能并发执行同一个Job Definition(由JobDetail定义),但是可以同时执行多个不同的JobDetail,举例说明,我们有一个Job类,叫做SayHelloJob,并在这个Job上加了@DisallowConcurrentExecution注解,然后再这个Job上定义了很多JobDetail,如sayHelloTomJobDetail,sayHelloMikeJobDetail,那么当scheduler启动时,不会并发执行多个sayHelloTomJobDetail 或者sayHelloMikeJobDetail 但可以同时执行 sayHelloTomJobDetail跟sayHelloMikeJobDetail。

5.2 同一个JobDetail中多个实例的数据共享

@PersistJobDataAfterExecution 是用在Job实现类上,表示一个有状态的任务,意思是当正常执行完Job后,JobDataMap中的数据会保存 给下一次调用使用。

注意:当使用@persistJobDataAfterExecution注解时,为了避免并发时,存储数据造成混乱,强烈建议吧@DisallolwConcurrentExecution 注解也加上。

示例

假设定时任务的时间间隔为 3 秒,但 job 执行时间是 10 秒。当设置 @DisallowConcurrentExecution 以后程序会等任务执行完毕后再去执行,否则会在 3 秒时再启动新的线程执行。

当设置 @PersistJobDataAfterExecution 时,在执行完 Job 的 execution 方法后保存 JobDataMap 当中固定数据,以便任务在重复执行的时候具有相同的 JobDataMap;在默认情况下也就是没有设置 @PersistJobDataAfterExecution 的时候每个 job 都拥有独立 JobDataMap。


6.Quartz 异常与中断

6.1 作业异常

org.quartz.JobExecutionException会在Scheduler(调度器)运行错误时,由Job(作业)实现类抛出。

在我们捕获异常并解决异常后,可以调用JobExecutionException#setRefireImmediately(true) 立即重新执行作业。
假设我们有一个会抛出异常的job实现类,job实现类的代码片段如下

try {
    // 一个异常例子,假设第一次传入的 denominator 为 0,那么将会抛出异常
    calculation = 4815 / denominator;
} catch (Exception e) {
    JobExecutionException e2 = new JobExecutionException(e);

    // 在第一次异常后,修改 denominator 参数为 1,那么后面的执行就不会出错了
    dataMap.put("denominator", "1");

    // true 表示立即重新执行作业
    e2.setRefireImmediately(true);
    throw e2;
}

上面的作乐会在第一次执行时抛出java.lang.ArithmeticException: / by zero异常后,马上又会执行一次,之后都可以正常执行作业。

注意:为了共享在同一个JobDetail中的JobDataMap,我们需要再上面这个Job实现类上加入@PersistJobDataAfterExecution 和 @DisallowconcurrentExecution注解

6.3 捕获异常,取消所有触发器

在我们捕获异常时,可以调用JobExecutionException#setUnscheduleAllTriggers(true)取消所有与这个作业有关的触发器。
假设我们有一个会爆出一场的job实现类,job实现类代码片段如下:

try {
    // 一个异常例子
    int zero = 0;
    calculation = 4815 / zero;
} catch (Exception e) {
    JobExecutionException e2 = new JobExecutionException(e);
    // true 表示 Quartz 会自动取消所有与这个 job 有关的 trigger,从而避免再次运行 job
    e2.setUnscheduleAllTriggers(true);
    throw e2;
}

上面的作业会在抛出异常后,就不再执行任何有关该作业的触发器了。

6.3 作业中断

org.quartz.InterruptableJob接口提供了一种中断机制,这个接口只有一个方法interrupt() 这个方法会在用户发出中断请求到Scheduler(调度器)时触发(即调用Scheduler#interrupt(JobDetail#getKey())方法时触发)

其中InterruptableJob接口又继承了Job接口,所以当我们使用时,只需要实现InterruptableJob接口,重写其中的executeinterrupt方法即可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • scheduler定时调度系统是大多行业项目都需要的,传统的spring-job模式,个人感觉已经out了,因为存...
    安琪拉_4b7e阅读 2,775评论 4 6
  • 什么是定时任务调度 基于给定的时间点,给定的时间间隔或者给定的执行次数自动完成执行任务 在Java中的定时调度工具...
    Hey_Shaw阅读 2,389评论 2 1
  • Quartz是一个完全由java编写的功能丰富的开源作业调度库,可以集成到几乎任何Java应用程序中,小到独立应用...
    ProteanBear阅读 6,782评论 3 24
  • 入门简介: 基本上任何公司都会用到调度这个功能, 比如我们公司需要定期执行调度生成报表, 或者比如博客什么的定时更...
    水车阅读 5,665评论 0 7
  • 胖头顿时欢乐了,一路上朱脸七都在吹嘘山猫和土耗子都得了他的真传,没想到竟然是这么个情况。胖头明显是唯恐天下不乱的那...
    青柠芝夏阅读 269评论 0 0