3.7 定时任务
在传统节假日,我们会收到各种祝福短信,中国的证券交易系统和基金销售公司每天晚上都要向中国证券登记结算有限责任公司提交每日的股票、基金交易汇总数据,公司的ERP系统每月每季度都要生产各种报表,这类功能都是通过定时任务完成的。定时任务的实现方式有很多种,例如UNIX的Crontab、Java生态的Quartz,本节将以Quartz为基础在Spring体系下实现定时任务。
3.7.1 Quartz
3.7.1.1 Quartz Scheduler简介
开源领域有很多定时任务框架,但是在Java生态中Quartz一定是最优秀的。Quartz提供了各种从简单到复杂的定时任务框架供开发者使用,图3-96展示了Quartz的架构及工作模式。
图3-96 Quartz的架构及工作模式
图3-96中每个组件的描述如下:
• Quartz Scheduler:维护Job(任务)和Trigger(触发器)之间的关系,即Job是被哪些Trigger触发的。
• Quartz Scheduler Thread:该线程从Job Store获得Trigger,并在指定的时间触发Trigger。
• Job:该接口对任务执行的流程做了代码逻辑抽象,可以认为Job是一项可被执行的任务。
• Trigger:Trigger是一种制定任务计划的机制,它用于定义Job应该在什么时候被执行。
• Job Store:用于保存Job和Trigger的相关信息。
• ThreadPool:用于管理执行Job任务的线程。
Quartz有以下七个核心类:
(1)org.Quartz.Scheduler
Quartz Scheduler的主要接口,功能是将Job和Triggers关联起来,并在Trigger被触发时执行Job。
(2)org.Quartz.SchedulerFactory
主要用于创建Scheduler对象实例,具体代码如下:
(3)org.Quartz.Job
Job任务的抽象层接口,所有被Quartz框架执行的任务都必须实现这个接口,具体代码如下:
(4)org.Quartz.JobDetail
Quartz需要通过Job Group和Job Name区分各个不同的Job,但它并不会存储Job实例,而是以JobDetail的方式来存储和标记Job。示例代码如下:
(5)org.Quartz.Trigger
Trigger定义了一个Job在什么时间点会被执行,一个Job可以有多个Trigger,但是一个Trigger只能绑定一个Job。最常见的两个Trigger实例是SimpleTrigger和CronTrigger,下面的例子就通过TriggerBuilder定义了一个每天10:42:00运行的Trigger,具体代码如下:
定义Trigger时还有一个重要的概念就是Misfire Instruction,它是指Trigger在既定的触发时间由于系统故障原因导致没有被正确触发,待系统恢复正常运行后的补救行为。Misfire Instruction主要有以下几个选项:
• MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY(执行错过的任务)。
• MISFIRE_INSTRUCTION_DO_NOTHING(等待下次Cron触发频率到达时执行任务)。
• MISFIRE_INSTRUCTION_FIRE_NOW(立刻触发一次任务)。
(6)org.Quartz.JobStore
主要用于存储Job和Trigger的信息,一般选择RAMJobStore或JDBCJobStore。
(7)org.Quartz.core.QuartzScheduler
定义Trigger和Job之后,需要通过Scheduler将二者关联,QuartzScheduler是Scheduler的一个关键实现,开发者可以通过QuartzScheduler将Job和Trigger进行关联注册,代码如下:
此处的Job并非Job实例,而是JobDetail实例,至此整个定义流程完毕,Job将会被指定于Trigger定义的时间运行,最后只需执行scheduler.start()启动任务即可。
Quartz运行期的逻辑关系如图3-97所示。
图3-97 Quartz运行期的逻辑关系
3.7.1.2 Cron表达式
Cron是一种简单且高效的任务时间表达式,Quartz对基于Cron的触发器做了很好的支持。Quartz的Cron表达式和原生的UNIX下的Cron表达式有所不同,例如Quartz的Cron表达式可以精确到秒,而UNIX只能精确到分钟,Quartz的Cron表达式格式如图3-98所示。
图3-98 Cron表达式
Cron表达式各位置所允许的取值范围如表3-6所示。
表3-6 Cron表达式位置含义及语法
表3-6中包含的特殊字符含义如下:
• 星号(*):可用在所有字段中,表示对应时间域的每一个时刻,例如,*在分钟字段时,表示“每分钟”。
• 问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于占位符。
• *和?的区别:二者都表示不确定的值,但各自表达的含义差异很大。首先,“?”只能用于日期和星期,这是因为在大多数情况下(特别是在不知道年份时)是无法同时定义准确的日期和星期几的。其次,“*”可以出现很多次,但是“?”只能出现一次,表示日期和星期最多一个无意义。
• 减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10点到12点,即10点、11点、12点。
• 逗号(,):表达一个列表值,如在星期字段中使用MON、WED、FRI,则表示星期一、星期三和星期五。
• 斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0、15、30和45s,而5/15在分钟字段中表示5、20、35、50,你也可以使用*/y,它等同于0/y。
• L:该字符只在日期和星期字段中使用,代表Last的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值X,则表示“这个月的最后X天”,例如,6L表示该月最后的星期五。
• W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围。
• LW组合:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日。
• 井号(#):该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,则忽略不触发。
• C:该字符只在日期和星期字段中使用,代表Calendar的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。
注意:以上字符包括月份和星期都不是大小写敏感的。
3.7.2 Spring Batch
3.7.2.1 Spring Batch简介
通过分析Quartz架构,我们可以知道Quartz的重点在于定义何时执行一个计划任务,并保证在正确的时间触发该任务,但是Quartz架构对于如何实现任务的细节并没有定义任何规范。因此,Spring社区创建了Spring Batch来完善任务处理流程,Spring Batch所解决的问题是“如何执行任务”。Spring Batch的主要模块及工作流程如图3-99所示。
图3-99 Spring Batch的主要模块及工作流程
如图3-99所示,图中各组件描述如下:
• Job
在Spring Batch中,Job是一个包含了一系列流程的执行单元。
• JobLauncher
一个启动Job的入口,用户可以直接使用JobLauncher来启动需要启动的Job。
• JobRepository
用于存储执行Job的相关信息
• Step
Job的一个执行单元,一个Job可以包含多个Step,Step支持Chunk或Tasklet模式。
• Item
一条数据源中的记录。
• Tasklet
采用Tasklet模式意味着在一个步骤内只执行一个任务,Tasklet模式的特点是每个步骤都需要将数据源的所有Item全部处理之后才能执行下一个步骤。
• Chunks
在Chunks模式下,每个步骤不需要处理全部Item,只用处理一个恒定数量的Item集合,处理完一个Chunk的数据后就可以执行下一个步骤。
• Item Reader
用于从数据源中读取Item的组件。
• Item Processor
将读取的Item进行一系列处理,比如按照要求过滤结果集或改变返回数据的格式。
• Item Rriter
用于将Item写入数据源的组件。
Spring Batch的运行期流程如图3-100所示。
图3-100 Spring Batch运行期流程图
3.7.2.2 集成Quartz和Spring Batch
Quartz擅长做任务计划调度,而Spring Batch擅长管控任务的处理细节,如果将二者结合在一起,能否产生1+1>2的效果呢?本节,我们就借助Quartz+Spring Batch的组合为coupon-user-service添加任务调度功能。
coupon-user-service项目需要一个新功能,统计各种不同优惠券的使用情况,在每天晚上11点生成报表,利用Spring Boot集成Quartz和Spring Batch从而实现该场景的步骤如下:
第一步,在coupon-user-service项目的pom.xml文件中添加相关依赖,具体代码如下:
第二步,创建一个用于做统计的辅助类,命名为Counter,具体代码如下:
第三步,创建QuartzJobLauncher类,QuartzJobLauncher继承了QuartzJobBean类(QuartzJobBean实现了org.Quartz.Job)。QuartzJobLauncher就像一座桥梁,在Quartz和Spring Batch之间建立起了连接。QuartzJobLauncher具体代码如下:
第四步,创建Scheduler相关配置类,具体代码如下:
第五步,创建一个简单的ItemProcessor实例,完成统计工作,具体代码如下:
最后一步,创建BatchConfiguration类,在这个类中约定任务中每个步骤的执行顺序,具体代码如下:
我们将Quartz和Spring Batch的优势组合在一起,定义了一种全新的定时任务框架执行方式。