五、Quartz中Trigger理解和使用

(一)、Trigger触发器

  Job中包含了任务执行的逻辑,Scheduler负责扫描需要执行的Job任务,那么Scheduler如何知道何时执行这个Job任务呢?接下来就需要触发器上场了。

  触发器(org.quartz.Trigger)抽象类的几个主要属性和JobDetail差不多,这里就不说明了,主要注意的是下面表格中的属性和misfireInstruction这个属性,misfireInstruction这个属性的是触发器错失执行(misfire)后的一个错失触发机制标识。当线程池中没有可用的线程执行任务时,就会错过触发时间,Trigger抽象类中默认错失触发机制是常量0(聪明的策略)。派生类有它们自己的错失触发机制。

下面对触发器的子类分别进行讲述,我们主要看Trigger的4个可用的派生类(注:共5个,UICronTrigger1.5版本后已废弃),分别是:

  • org.quartz.SimpleTrigger
  • org.quartz.CronTrigger (常用)
  • org.quartz.DateIntervalTrigger
  • org.quartz.NthIncludedDayTrigger

以上派生类的几个主要属性:

属性名 详细介绍
startTime 首次触发时间
endTime 截止时间,就算指定的执行次数没有执行完也立即结束
nextFireTime 下一次触发时间
previousFireTime 上一次触发时间
repeatCount (不提供get/set)重复执行次数(不包含首次执行)
repeatInterval (不提供get/set) 重复执行间隔,单位为毫秒
timesTriggered (不提供get/set) 总触发次数

了解了触发器的几个关键属性,接下来通过代码来看看几个触发器具体的使用。
下面这个Job打印了触发器的几种时间,示例代码中的几种触发器都使用到了这个Job。


package com.xk.quartz.test;

import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Trigger;

/**
 * 演示job
 */
public class DemoJob implements Job{

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取触发器
        Trigger trigger = context.getTrigger();
        
        // 获取几种时间
        Date startTime = trigger.getStartTime();
        Date endTime = trigger.getEndTime();
        Date previousFireTime = trigger.getPreviousFireTime();
        Date nextFireTime = trigger.getNextFireTime();
        
        System.out.println("首次执行时间:" + startTime.toLocaleString());
        System.out.println("最后截止时间:" + (endTime != null ? endTime.toLocaleString() : null));
        System.out.println("上一次执行时间:" + previousFireTime.toLocaleString());
        System.out.println("下一次执行时间:" + (nextFireTime != null ? nextFireTime.toLocaleString() : null));
        System.out.println("*******************************");
    }
}

接下来对每个触发器进行分别演示。

(1)、org.quartz.SimpleTrigger

  SimpleTrigger是一种设置和使用简单的触发器,它是在指定日期/时间且可能需要重复执行n次的时机下使用的。这种触发器不适合每天定时执行任务这种场景,举个栗子,假设指定任务首次触发时间是中午12点整,间隔时间为1天,那么理论上来讲以后的每一天中午12点整都会准时执行,但是,只要出现了一次misfire,假设错失执行后等待可用线程的时间为1分钟,即此任务会在12点01分被恢复触发,那么触发器记录的此次执行时间就是12点01分钟,又因为间隔时间设定的是1天,所以下一次执行时间也会被推算到第二天的12点01分。

为了方便演示,下面使用的Quartz框架没有持久化配置,仅使用RAMJobStore(存放在内存中),这样的好处是可以使用默认配置并且直接main方法启动。

package com.xk.quartz.test;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;

/**
 * SimpleTrigger演示
 */
public class SimpleTriggerTest {

    public static void main(String[] args) throws SchedulerException {
        // 获取默认调度器  内部加载顺序在调度器章节提到过
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        // 启动调度器
        scheduler.start();
        
        // 创建job详情
        JobDetail jobDetail = new JobDetail("demoJob演示", DemoJob.class);
        
        // 以下是触发器的不同构造
        // 立即执行
        SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger演示");
        
        // 立即执行
        // SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger演示", new Date());
        
        // 当前立即执行,指定重复次数3次,间隔500毫秒
        // 总执行次数为4次(首次执行+3次重复执行)
        // SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger演示", 3, 500);
        
        // 当前时间2秒后执行,当前时间5秒后结束,指定重复次数20次,间隔1000毫秒
        // 这里虽然指定执行次数为20次,但是实际只能执行3次(首次执行+2次重复执行),因为有"当前时间5秒后结束"这个因素在
        // SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger演示", new Date(System.currentTimeMillis() + 2000), new Date(System.currentTimeMillis() + 5000), 20, 1000);
        
        // TODO
        // 只要明白上面几种创建方式   其他没有写出的够着就很容易明白
        
        // 注册job和触发器
        scheduler.scheduleJob(jobDetail, simpleTrigger);
    }
}

(2)、org.quartz.CronTrigger

  CronTrigger触发器比SimpleTrigger触发器强大很多,上面的代码可以看出,使用SimpleTrigger触发器时,需要设置不同的属性来支撑,代码编写相对较多,而且不是很灵活。而接下来CronTrigger触发器,可以设定复杂的触发时间表,设置方式也很简单,唯一的难点就是需要理解时间表达式如何编写。
  为了更深刻的了解CronTrigger,我们需要了解一下Cron到底是什么?
  Cron概念是来自UNIX(Linux也是基于UNIX的)操作系统的,它就是一个执行计划任务的服务器或程序,你只需要将你的计划和执行时间交给它,它就会根据指定时间来调度工作任务。这里不要混淆UNIX的Cron和Quartz的Cron,因为他们之间有一些很明显的差别,UNIX cron 计划仅支持至分钟级,而Quartz支持到秒级的计划。说这些就是让大家了解下Quartz Cron的历史。

下面先看一下几个示例,会感觉也没多难,很好理解:

package com.xk.quartz.test;

import java.text.ParseException;

import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;

/**
 * CronTrigger演示
 */
public class CronTriggerTest {

    public static void main(String[] args) throws SchedulerException {
        // 获取默认调度器  内部加载顺序在调度器章节提到过
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        // 启动调度器
        scheduler.start();
        
        // 创建job详情
        JobDetail jobDetail = new JobDetail("demoJob演示", DemoJob.class);
        
        // 创建触发器  下面只演示一种触发器构造   其他的构造也很好理解
        CronTrigger cronTrigger = null;
        try {
            // 每秒执行一次两种写法
            cronTrigger = new CronTrigger("cronTrigger演示", "DEFAULT", "* * * * * ?");
            // cronTrigger = new CronTrigger("cronTrigger演示", "DEFAULT", "0/1 * * * * ?");
            
            // 每分钟的秒钟等于30时执行
            // cronTrigger = new CronTrigger("cronTrigger演示", "DEFAULT", "30 * * * * ?");
            
            // 每5分钟的整分时执行
            // cronTrigger = new CronTrigger("cronTrigger演示", "DEFAULT", "0 0/5 * * * ?");
            
            // 每天的9点和18点整执行  (通知下班啦☺)
            // cronTrigger = new CronTrigger("cronTrigger演示", "DEFAULT", "0 0 9,18 * * ?");
            
            // 每个月最后一天的23点59分59秒执行  (做月统计时使用)
            // cronTrigger = new CronTrigger("cronTrigger演示", "DEFAULT", "59 59 23 L * ?");
            
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
        // 注册job和触发器
        scheduler.scheduleJob(jobDetail, cronTrigger);
    }
}

下面列出Quartz Cron表达式格式的7个域:

名称 是否必须 允许值 特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W C
1-12或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L C #
空 或 1970-2099 , - * /

域之间通过空格进行分隔,年份不是必须项,月份和星期的名称不区分大小写,比如SUN和sun是一样的。

  • 星号( * )表示该域上所有合法的值都会被触发,如果使用在秒域上,就表示 每秒钟
  • 问号( ? )表示不为该域指定值。?号只能用在日和周两个域上,但不能两个域同时使用,只要记住一点,在这两个域的其中一个指定了值,那另外一个值就放一个?问号,这样不会出现混乱
  • 逗号( , )表示该域上指定一个值列表,例如秒的域上使用值0,20,40 表示秒钟等于0、20或40时触发
  • 斜杠( / )表示时间表递增,上面逗号举例20秒触发一次,也可以在秒域上写成 0/20
  • 中划线( - )表示取值范围,例如时域上指定9-18,那么表示上午9点到下午6点每小时触发一次
  • 字母( L )表示合法范围的最后一个值,只能在日和周两个域上使用,用在日上时表示指定月份的最后一天;用在周上时表示周的最后一天,注意是数字7或者星期六(国外周日是星期的第一天),用在周上时还可以用一个数字与 L 连起来表示月份的最后一个星期 X。例如,表达式 0 0 0 ? * 1L表示在每个月的最后一个星期日触发
  • 字母( W )表示指定日的最近的一个工作日(周一到周五),并且只能用在日域上。假如日字段指定的是15W,那么就表示该月15号最相近的工作日执行。如果15号是周六,那么就是14号周五触发;如果15号是周日,那么就会在16号周一触发;如果15号是周三,那就当天触发
  • 井号( # )表示指定月份中第几周的第几天,只能用在周域上。例如值为1#2,表示某月第二周的星期日触发,2表示第二周,1表示星期日。如果把2改为大于4的数字,比如5或6,那么就不会被触发,因为一个月中不会超过4周
  • 字母( C )表示计算需要依赖日历(org.quartz.impl.calendar.CronCalendar)类的计算结果,只能用在日和周两个域上。

下面是一些例子,可以照葫芦画瓢进行延伸扩展:

表达式 含义
0 0 12 * * ? 每天中午12点整触发
0 0 12 ? * * 每天中午12点整触发
0 0 12 * * ? 2018 2018年的每天中午12点整触发
0 0/5 12 * * ? 每天12点开始到12点55分结束每5分钟触发
0 0-5 12 * * ? 每天12:00到12:05每分钟触发
0 0 12,13,14 * * ? 每天12点、13点、14点整点时各触发
0 0 12 ? 6 WED 6月份的每个周三中午12点整触发
0 0 12 L * ? 每个月最后一天12点整触发
0 0 12 15W * ? 每个月15号最近的工作日(周一到周五)触发一次
0 0 12 ? * 2#3 每个月第三周的星期一触发
(3)、org.quartz.DateIntervalTrigger

  这是quartz1.7版本以后加入的触发器,该触发器适用于每小时,每几周,每几月重复执行的任务。
  举个例子,假如任务需求是需要每5个月间隔时触发一次,如果用Cron来实现,那表达式就是“0 0 12 * 0/5 ?“,启动时会报一个解析异常ParseException: Month values must be between 1 and 12。虽然我们也可以使用SimpleTrigger触发器,但是上面提到,SimpleTrigger当出现misfire时,下一次执行的时间也会向后延迟。而DateIntervalTrigger就不会有这些问题。使用DateIntervalTrigger时只要注意一个问题,如果使用的Unit单位为月,开始执行时间是1月31日,那么第二次执行将会是2月28日(可能29日),第三次执行会是3月28日,以后都是28日,而不会出现31日了,这是需要注意点。如果想要实现每个月最后一天触发,那就使用CronTrigger触发器去实现(示例代码中有提到)。

上代码:


package com.xk.quartz.test;

import org.quartz.DateIntervalTrigger;
import org.quartz.DateIntervalTrigger.IntervalUnit;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;

/**
 * DateIntervalTrigger演示
 */
public class DateIntervalTriggerTest {

    public static void main(String[] args) throws SchedulerException {
        // 获取默认调度器  内部加载顺序在调度器章节提到过
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        // 启动调度器
        scheduler.start();
        
        // 创建job详情
        JobDetail jobDetail = new JobDetail("demoJob演示", DemoJob.class);
        
        // 创建触发器   只演示一种构造方法  其他构造属性和以上触发器属性类似
        // 每一个月触发一次   IntervalUnit.MONTH是个枚举常量  1表示重复间隔
        DateIntervalTrigger dateIntervalTrigger = new DateIntervalTrigger("dateIntervalTrigger演示", IntervalUnit.MONTH, 1);
        
        // TODO
        // 下面就不一一列举了 IntervalUnit的其他常量很容易理解,每个常量对应了一个日期域

        // 注册job和触发器
        scheduler.scheduleJob(jobDetail, dateIntervalTrigger);
    }
}

(4)、org.quartz.NthIncludedDayTrigger

NthIncludedDayTrigger适用于在每一个间隔类型(月或年等)的第N天触发。直接上代码:

package com.xk.quartz.test;

import org.quartz.Calendar;
import org.quartz.JobDetail;
import org.quartz.NthIncludedDayTrigger;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;

/**
 * NthIncludedDayTrigger演示
 */
public class NthIncludedDayTriggerTest {

    public static void main(String[] args) throws SchedulerException {
        // 获取默认调度器  内部加载顺序在调度器章节提到过
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        // 启动调度器
        scheduler.start();
        
        // 创建job详情
        JobDetail jobDetail = new JobDetail("demoJob演示", DemoJob.class);
        
        // 创建触发器  
        // 下面触发器表示每个月的第10天中午12点整触发
        NthIncludedDayTrigger nthIncludedDayTrigger = new NthIncludedDayTrigger("nthIncludedDayTrigger演示");
        nthIncludedDayTrigger.setN(10);
        nthIncludedDayTrigger.setIntervalType(NthIncludedDayTrigger.INTERVAL_TYPE_MONTHLY);
        nthIncludedDayTrigger.setFireAtTime("12:00:00");
        
        // 注册job和触发器
        scheduler.scheduleJob(jobDetail, nthIncludedDayTrigger);
    }
}

  四种触发器的创建方式都演示了,大家平时需要考虑的就是哪些情况下使用哪种触发器。这需要大家在开发的过程中自己进行研究和调试。大家使用最多的是CronTrigger,通过Cron表达式几乎可以完成工作业务中的绝大多数任务。
(二)、使用TriggerUtils工具类创建触发器

  下面介绍一个常用的触发器工具类(org.quartz.TriggerUtils)。可以很方便创建一些简单的触发器。上代码:

package com.xk.quartz.test;

import org.quartz.Trigger;
import org.quartz.TriggerUtils;

/**
 * TriggerUtils演示
 */
public class TriggerUtilsTest {

    public static void main(String[] args) {
        
        // 构造函数有很多  下面只列举常用的几种
        
        // 创建一个每2秒执行一次 重复执行5次的触发器
        Trigger trigger = TriggerUtils.makeSecondlyTrigger("trigger", 2, 5);
        
        // 创建一个每5分钟执行一次的触发器
        Trigger trigger2 = TriggerUtils.makeMinutelyTrigger(5);
        trigger2.setName("trigger2");
        
        // 创建一个每2小时执行一次 重复执行5次的触发器
        Trigger trigger3 = TriggerUtils.makeHourlyTrigger(2, 5);
        trigger3.setName("trigger3");
        
        // 创建一个每天中午12点10分执行的触发器
        Trigger trigger4 = TriggerUtils.makeDailyTrigger("trigger4", 12, 10);
        
        // 创建一个每个月5号中午12点10执行的触发器
        Trigger trigger5 = TriggerUtils.makeMonthlyTrigger("trigger5", 5, 12, 10);
        
        // 创建一个每周三中午12点整执行的触发器
        Trigger trigger6 = TriggerUtils.makeWeeklyTrigger("trigger6", 4, 12, 0);
                
        // 创建一个立即触发的触发器
        Trigger trigger7 = TriggerUtils.makeImmediateTrigger("trigger7", 0, 0);
    }
}

下面是工具类中的其他方法,了解一下含义:

  • computeFireTimes(Trigger trigg, org.quartz.Calendar cal, int numTimes)
    返回下次执行的日期列表
  • computeFireTimesBetween(Trigger trigg, org.quartz.Calendar cal, Date from, Date to)
    返回指定时间段内下次执行的日期列表
  • computeEndTimeToAllowParticularNumberOfFirings(Trigger trigg, org.quartz.Calendar cal, int numTimes)
    返回第n次触发后的第1秒时间
  • getDateOf(int second, int minute, int hour)
    返回指定时分秒的一个今天的日期对象,此方法有包含日,月,年参数重载方法
  • getEvenHourDate(Date date)
    返回一个舍入到下一个小时以上的日期。比如:入参 09:35:12 返回10:00:00
    此方法也有多个重载。
到此,Quartz中Trigger理解和使用说完了。如果大家有什么问题,请在下方留言,谢谢!
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容