前言
前段时间接到任务,处理Quartz多节点Job并发执行的问题。在此之前,对于Quartz了解甚微,为了完成开发任务,利用了两个周末的时间研究了Quartz,写下了笔记,记录下Quartz学习过程。
一、什么是Quartz?
Quartz Job Scheduling是基于Java实现的成熟的企业级作业调度组件,可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。
是完全由java开发的一个开源的任务日程管理系统,“任务进度管理器”就是一个在预先确定(被纳入日程)的时间到达时,负责执行(或者通知)其他软件组件的系统。Quartz是目前最为成熟,使用最广泛的Java任务调度框架。
二、使用场景
定时任务处理,大部分公司都会有涉及到这一块业务。比较常见的如订单超时未支付。当用户生成一条订单,迟迟没有支付,业务判定这一订单是无效订单,然后进行恢复库存等业务处理。在技术层面,我们需要有一个“任务”,不断的刷新超过30分钟(就拿设置超时时间为30分钟为例)没有支付成功的订单。这里就可以使用Quartz定时任务调度框架。
三、代码实现
这一篇笔记主要记录入门的简单使用,后续会分享集群分布式部署处理。大部分名词解释写在代码注释中,方便阅读。
第一步导入maven依赖
<dependency> <groupId>org.quartz-schedulergroupId> <artifactId>quartzartifactId> <version>2.3.0version>dependency><dependency> <groupId>org.quartz-schedulergroupId> <artifactId>quartz-jobsartifactId> <version>2.3.0version>dependency><dependency> <groupId>javax.transactiongroupId> <artifactId>jtaartifactId> <version>1.1version>dependency>
我们定义一个简单的Job类
import org.quartz.*;/*** @description: 定义job,输出打印,测试功能** Job(作业)是指执行一些作业的特定的Java类* Job必须实现 org.quartz.Job接口* 这个接口要求在Job中实现execute()方法* 当 Quartz 调用 execute() 方法,* 会传递一个 JobExecutionContext 上下文变量,* 里面封装有 Quartz 的运行时环境和当前正执行的 Job。* JobExecutionContext可以被用来访问 JobDetail 类,* JobDetail 类持有Job的详细信息,包括为Job实例指定的名称,Job 所属组,Job 是否被持久化(易失性)。* JobDetail又持有一个指向JobDataMap的引用。JobDataMap中包含Job配置的自定义属性。** @author: Lishuzhen* @create: 2019-12-13 12:35*/public class HelloJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { JobDetail jobDetail = jobExecutionContext.getJobDetail(); // JobDataMap JobDataMap dataMap = jobDetail.getJobDataMap(); String content = dataMap.getString("HELLO"); System.out.println(content); }}
创建一个调度器,带动HelloJob运行
import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.quartz.*;import org.quartz.impl.StdSchedulerFactory;import static org.quartz.JobBuilder.newJob;import static org.quartz.SimpleScheduleBuilder.simpleSchedule;import static org.quartz.TriggerBuilder.newTrigger;/*** @description: job调度器* @author: Lishuzhen* @create: 2019-12-13 12:54*/public class HelloQuartz { private static Log logger = LogFactory.getLog(HelloQuartz.class); public static void main(String[] args) { try { /** * 从Scheduler工厂获取一个Scheduler的实例 * * Scheduler(调度器)是Quartz框架的心脏。 * Scheduler的生命周期始于通过SchedulerFactory工厂类创建实例 终于调用shutdown() 方法 * Scheduler不仅可以用于新增、移除、列举Jobs和Triggers * 还可以执行调度相关操作 * 比如暂停Trigger、恢复Trigger等 * 需要注意的是 * 直到调用start()方法时,Scheduler才正式开始执行job和trigger。 */ Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); /** * 注册jobDetail1,打印"Hello Quartz!",第5秒钟执行一次 * JobDetail表示一个具体的可执行的调度程序 * Job 是这个可执行程调度程序所要执行的内容 * 另外 JobDetail 还包含了这个任务调度的方案和策略 */ JobDetail jobDetail = newJob(HelloJob.class).withIdentity("job", "hello").build(); jobDetail.getJobDataMap().put("HELLO", "Hello Quartz!"); /** * Trigger(触发器)用于触发Job的执行。最常用的类型包括 SimpleTrigger和CronTrigger。 */ Trigger trigger = newTrigger().withIdentity("trigger", "hello").startNow() .withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(10)).build(); /** * 组合Job和trigger */ scheduler.scheduleJob(jobDetail, trigger); /** * 调度器启动 */ scheduler.start(); /** * 调度器关闭 */ //scheduler.shutdown(true); } catch (SchedulerException e) { logger.error(e); } }}
此时,运行main,即可得到以下结果,看到我们的HelloJob已经在定时执行了
上面的代码简单演示Job执行,quartz通过调用器来根据Trigger(触发器)用于触发Job的执行。我们最常用的Trigger类型有 SimpleTrigger和CronTrigger。我们分别写两个Demo来演示使用过程。
SimpleTrigger 触发 job,定义一个 HelloSimpleTrigger触发器测试类
import org.quartz.JobDetail;import org.quartz.Scheduler;import org.quartz.SchedulerException;import org.quartz.SimpleTrigger;import org.quartz.impl.StdSchedulerFactory;import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;import static org.quartz.DateBuilder.dateOf;import static org.quartz.DateBuilder.evenHourDate;import static org.quartz.JobBuilder.newJob;import static org.quartz.SimpleScheduleBuilder.simpleSchedule;import static org.quartz.TriggerBuilder.newTrigger;/*** @description: 使用 SimpleTrigger 部署job* SimpleTrigger对于设置和使用是一种最简单的一种Quartz Trigger。* 它是为需要在特定日期/时间启动,且以一个可能间隔的时间执行n次的Job设计的。* @author: Lishuzhen* @create: 2019-12-13 14:42*/public class HelloSimpleTrigger { public static void main(String[] args) { try { // 以 StdScheduleFactory的方式获取一个 Schedule 实例 Scheduler scheduled = StdSchedulerFactory.getDefaultScheduler(); JobDetail jobDetail = newJob(HelloJob.class).build(); jobDetail.getJobDataMap().put("simpleTrigger", "Hello SimpleTrigger..."); // 创建指定时间 DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 开始时间 Date startTime = dateFormat.parse("2019-12-13 15:00:00"); // 结束时间 Date endTime = dateFormat.parse("2019-12-13 22:00:00"); // 创建 SimpleTrigger 触发器 SimpleTrigger trigger = createSimpleTrigger1(startTime,endTime);// SimpleTrigger trigger = createSimpleTrigger2(5);// SimpleTrigger trigger = createSimpleTrigger3(16,23, 0); scheduled.scheduleJob(jobDetail, trigger); scheduled.start(); } catch (SchedulerException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } } /** * 创建一个指定时间开始,指定时间结束的触发器 * @param startTime * @param endTime * @return */ private static SimpleTrigger createSimpleTrigger1(Date startTime,Date endTime){ return newTrigger() /** * group 是用于分类的,相当于一个命名空间。 * 作用 : * 比如 判断两个 trigger 或者 job 是否相同 * 在 trigger 中,使用 equal 方法就是在父类 key 中的 equal 方法,这里就用到 group */ .withIdentity("simpleTrigger","hello") // 指定开始时间 .startAt(startTime) // 执行策略 .withSchedule(simpleSchedule() // 间隔 5 分钟// .withIntervalInMinutes(5) // 间隔 5 秒 .withIntervalInSeconds(5) // 重复执行次数,默认 1 次 .withRepeatCount(100) ) // 结束时间 .endAt(endTime) .build(); } /** * 创建下一个整点开始执行,X 小时执行一次,永久执行的触发器 * @param hour * @return */ private static SimpleTrigger createSimpleTrigger2(Integer hour){ return newTrigger() /** * group 是用于分类的,相当于一个命名空间。 * 作用 : * 比如 判断两个 trigger 或者 job 是否相同 * 在 trigger 中,使用 equal 方法就是在父类 key 中的 equal 方法,这里就用到 group */ .withIdentity("simpleTrigger","hello") // 指定开始时间,下一个整点开始执行 .startAt(evenHourDate(null)) // 执行策略 .withSchedule(simpleSchedule() // 间隔 x 小时 .withIntervalInHours(hour) // 间隔 5 分钟// .withIntervalInMinutes(5) // 间隔 5 秒// .withIntervalInSeconds(5) // 重复执行次数,默认 1 次// .withRepeatCount(100) // 永远重复执行 .repeatForever() ) .build(); } /** * 创建现在开始执行,指定时间结束的触发器 * @param hour * @param minute * @param second * @return */ private static SimpleTrigger createSimpleTrigger3(Integer hour, Integer minute ,Integer second){ return newTrigger() /** * group 是用于分类的,相当于一个命名空间。 * 作用 : * 比如 判断两个 trigger 或者 job 是否相同 * 在 trigger 中,使用 equal 方法就是在父类 key 中的 equal 方法,这里就用到 group */ .withIdentity("simpleTrigger","hello") // 指定开始时间,默认 当前时间开始执行// .startAt(evenHourDate(null)) // 执行策略 .withSchedule(simpleSchedule() // 间隔 x 小时// .withIntervalInHours(hour) // 间隔 5 分钟// .withIntervalInMinutes(5) // 间隔 5 秒 .withIntervalInSeconds(5) // 重复执行次数,默认 1 次// .withRepeatCount(100) // 永远重复执行 .repeatForever() ) // 指定时间结束 .endAt(dateOf(hour, minute, second)) .build(); }}
运行 main ,得到运行结果
CronTrigger 触发job , 新建一个 HelloCronTrigger 测试类
import org.quartz.*;import org.quartz.impl.StdSchedulerFactory;import static org.quartz.JobBuilder.newJob;import static org.quartz.TriggerBuilder.newTrigger;/*** @description: 使用 CronTrigger 触发器部署Job** CronTrigger 是基于 Unix 类似于 cron 的表达式,允许设定非常复杂的触发时间表。* Cron表达式由七个子表达式组成的字符串,它描述了不同的调度细节。* 这些子表达式是用空格分隔的,并表示:秒、分钟、小时、天、月、星期几、年(可选项)** “,” :表示指定多个值* “-”:表示指定一个范围的值* “*”:表示整个时间段* “/”:表示指定一个值的增加幅度。n/m表示从n开始,每次增加m* “?”:表示不确定的值* “L”:用在日表示一个月中的最后一天,用在周表示该月最后一个星期X* “W”:指定离给定日期最近的工作日(周一到周五)。LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。* “#”:用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。** @author: Lishuzhen* @create: 2019-12-13 16:25*/public class HelloCronTrigger { public static void main(String[] args) { try { // 调度器 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // jobDetail 可执行程序 JobDetail jobDetail = newJob(HelloJob.class).build(); jobDetail.getJobDataMap().put("cronTrigger","Hello CronTrigger..."); // 触发器 CronTrigger trigger = newTrigger() .withIdentity("cronTrigger","hello") .startNow() .withSchedule( // 每天 01:00:00 执行 // CronScheduleBuilder.cronSchedule("0 0 1 * * ?") // 每隔 5 秒执行一次 CronScheduleBuilder.cronSchedule("*/5 * * * * ?") ) .build(); scheduler.scheduleJob(jobDetail, trigger); scheduler.start(); } catch (SchedulerException e) { e.printStackTrace(); } }
运行结果
CronTrigger触发模式使用cron表达式,可以设置非常复杂的触发时间,对于cron表达式很多同学可能不熟悉,这里附上常用的cron表达式并且对此进行解释
(1)0 0 2 1 * ? * 表示在每月的1日的凌晨2点调整任务 (2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 (3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 (4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 (5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 (6)0 0 12 ? * WED 表示每个星期三中午12点 (7)0 0 12 * * ? 每天中午12点触发 (8)0 15 10 ? * * 每天上午10:15触发 (9)0 15 10 * * ? 每天上午10:15触发 (10)0 15 10 * * ? * 每天上午10:15触发 (11)0 15 10 * * ? 2005 2005年的每天上午10:15触发 (12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 (13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 (14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 (15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 (16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 (17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发 (18)0 15 10 15 * ? 每月15日上午10:15触发 (19)0 15 10 L * ? 每月最后一日的上午10:15触发 (20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 (21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发 (22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发============================================================================================================解释 有些子表达式能包含一些范围或列表 例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”“*”字符代表所有可能的值 因此,“*”在子表达式(月)里表示每个月的含义,“*”在子表达式(天(星期))表示星期的每一天 “/”字符用来指定数值的增量 例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样 “?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值 当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?” “L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写 但是它在两个子表达式里的含义是不同的。 在天(月)子表达式中,“L”表示一个月的最后一天 在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT 如果在“L”前有具体的内容,它就具有其他的含义了 例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五 注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题
Quartz 的 Calender 专门用于屏闭一个时间区间,使 Trigger 在这个区间中不被触发。Quartz包括了多种类型的Calender:
如果是Quartz只在一个节点中运行,可以通过注入springbean,在tomcat启动时,带动job执行
import com.frame.quartz.job.HelloJob;import com.frame.quartz.scheduler.SchedulerInstance;import org.quartz.*;import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;import static org.quartz.JobBuilder.newJob;import static org.quartz.TriggerBuilder.newTrigger;/*** @description: 通过 spring 注入bean初始化的方式带动job启动* @author: Lishuzhen* @create: 2019-12-13 20:25*/@Componentpublic class HelloStartJob { @PostConstruct public void initStart(){ System.out.println("init start running ..."); // 调度器 Scheduler scheduler = SchedulerInstance.getInstance(); // 可执行程序 JobDetail jobDetail = newJob(HelloJob.class).build(); jobDetail.getJobDataMap().put("startRunning", "Job StartRunning..."); // 触发器 CronTrigger CronTrigger trigger = newTrigger() .withIdentity("cronTrigger","hello") .startNow() .withSchedule( // 每隔 5 秒执行一次 CronScheduleBuilder.cronSchedule("*/5 * * * * ?") ) .build(); try { // jobDetail 和 trigger 组合 scheduler.scheduleJob(jobDetail, trigger); // 调度器启动 scheduler.start(); } catch (SchedulerException e) { e.printStackTrace(); } }}
启动日志如下
这一篇笔记记录的Quartz的简单入门,在实际开发过程中,几乎不会这样使用。后续会更新 Quartz在集群中部署使用。