什么是定时任务调度呢?
基于给定的时间点,给定的时间间隔或者给定的执行次数自动执行的任务
Java 中最常见的两款定时任务调度工具就是 Timer 和 Quartz,一般来说 Timer 能解决 60% 的需求,解决不了的就交给大哥 Quartz 了,Timer 是 JDK 自带的,不需要其他依赖,而 Quartz 是开源软件。
需要注意的是:能用 Timer 实现的就用 Timer,因为大哥的出场费是很贵的….
Timer
简单定义:
有且只有一个后台线程对多个业务线程进行定时定频率的调度。
构件关系:
Timer (包含有一个队列和一个后台线程)定时调用 TimerTask
我记得在上篇文章中我是写过关于 Timer 的,具体内容可以点击这里回看,这里就简单复习或者补充下:
1 | public class Main { |
这里还是要再次说明:使用 Timer 执行周期性任务时,出现异常后会自动退出(全部任务),因为它是基于单线程的。所以应该尽量使用 ScheduledExecutorService (支持周期任务的线程池)的方式来创建。
1 | ScheduledExecutorService scheduledExecutorService = newScheduledThreadPool(3); |
通过上面,知道 Timer 主要的方法有两个 schedule 和 scheduleAtFixedRate;然后来说说他们最大的区别:
- 首次执行时间早于当前时间
也就是说规定 12:00 执行 task,但是 12:05 的时候才执行到 schedule / scheduleAtFixedRate;
对于这种情况,schedule 会以当前时间为准,然后间隔指定时间重复执行;
对于 scheduleAtFixedRate 它会尽可能的多执行几次以赶上落下的任务,比如说规定没 2 分钟执行一次,那么它会在执行 scheduleAtFixedRate 后连续执行两次 task 来弥补缺失的“工作量” - 执行耗时超过了间隔时间
也就是说规定每隔 3 秒执行一次,但是 task 3 秒还没执行完的情况;
对于 scheduleAtFixedRate,当 task 超时后,第二次会很快被执行,它的间隔计算方式是程序开始执行的时间;
对于 schedule ,它的间隔计算方式是程序执行完后计时的,也就是说规定每隔 3 秒执行一次,task 耗时 5 秒,task 执行完后再等 3 秒才会执行第二次,从运行开始算的话也就是差了 8 秒
他们两个方法的主要区别就集中在这两种情况上了,缺点也可以看出来了,因为是单线程所以在处理并发时效果会非常的不理想,基本不会做到并发执行;还有就是抛出异常所有的任务都会终止了。
所以在对时效性要求较高的多任务并发作业和对复杂任务的调度(可能抛出异常)Timer 是不适合的
Quartz
然后就到了介绍大哥的时候了,它的强大就不用说了,你想实现的它基本都能搞定,听说也是作为 Spring 默认的调度框架,并且它的分布式和集群能力也不错。
Quartz 的核心有三个:调度器、任务、触发器;可以对应为 Scheduler、Job、Trigger。
简单的流程就是:调度器根据触发器来执行任务
1 | public class HelloScheduler { |
上面代码结合下面的解释看最好啦,这也仅仅只是一个简单的小例子
Job
实现业务逻辑的任务接口,没错它是个接口,非常容易实现,只有一个 execute 方法,相当于 TimerTask 的 run 方法;在里面编写逻辑即可,当任务执行失败时会抛出 JobExecutionException 异常。
1 | public class HelloJob implements Job { |
这是一个简单的 Job,只是展示部分常用的,结合下面的说明阅读比较好~
生命周期
每次调度器(Scheduler)调用执行 job 的 execute 方法前会会创建一个新的 job 实例,当调用完成后,关联的 job 实例会被释放,释放的实例就会被 GC 回收
JobExecutionContext
当调度器调用一个 Job ,就会将 JobExecutionContext 传递给 job 的 execute 方法;这样 Job 就可以通过 JobExecutionContext 来访问 Quartz 运行时候的环境以及 Job 本身的明细数据。
从另一方面 JobExecutionContext 就是为了解决不同 Job 需要不同参数的问题。
通过 JobExecutionContext 对象,可以获得 JobDetail 或者 Trigger 设置的自定义参数。
JobDataMap
在进行任务调度时,JobDataMap 存储在 JobExecutionContext 中,非常方便获取。
JobDataMap 可以用来装载任何可序列化的数据对象,当 job 实例执行时,这些参数对象会传递给它。
JobDataMap 实现了 JDK 的 Map 接口,并添加了一些非常方便的方法用来存取基本数据类型。
关于获取,除了使用 JobExecutionContext ,还有另一种方式,那就是在 Job 的实现类里添加相应的成员变量,并且设置 setter 方法,默认的 JobFactory 实现类在初始化 Job 实例的时候会自动调用这些 setter 方法,把 JobDataMap 中相应的值放进去
JobDetail
JobDetail 为 job 实例提供了许多设置属性,以及 JobDataMap 成员变量属性,它用来存储特定的 job 实例的状态信息,调度器需要借助 JobDetail 对象来添加 Job 实例。
简单说就是 JobDetail 是用来绑定 Job 实例的,并携带一些 Job 实例没有携带的状态信息。
下面看看它的一些重要属性:
name 和 group ,这两个是必须的,group 的默认值是 DEFAULT;还有 jobClass 也是必须的,就是绑定的 Job 类;此外还有一个 JobDataMap ,可简单理解为是来传递数据的。
查看可以使用:jd.getKey().getName()/getGroup();
Trigger
Quartz 中的触发器用来告诉调度程序作业什么时候触发。即 Trigger 对象是用来触发执行 Job 的。
触发器的通用属性:
- JobKey
表示 Job 实例的标识,触发器被触发时,该指定的 Job 实例会执行 - StartTime
触发器的时间表首次被触发的时间,类型是 Date,startAt() - EndTime
指定触发器的不再被触发的时间,类型也是 Date,endAt(),它的优先级比设置的重复执行次数高
SimpleTrigger
首先来看它的作用:
在一个指定时间段内执行一次作业任务,或者是在指定的时间间隔内多次执行作业任务。
我们通过 TriggerBuilder.newTrigger()
代码实际上就是生成了一个 SimpleTrigger
CronTrigger
它的作用:
基于日历的作业调度器,它不像 SimpleTrigger 那样精确的控制,但是更加常用。
说到 CronTrigger 就必须得说 Cron 表达式了,熟悉 Linux 的可能接触过,因为 Linux 有一个 crontab 命令来控制计划任务,也是 Cron 表达式。
简单说,Cron 表达式是由 7 个子表达式组成的字符串,描述了时间表的详细信息,格式:[秒] [分] [小时] [日] [月] [周] [年]
*
:表示“每”的意思,放在第一个位置就是每秒。?
:表示不确定,不指定值,就是不关心。,
:表示或的意思。-
:表示至的意思,就是范围啦。/
:表示的也是每的意思,举个例子,如果出现在秒的位置 0/5
就是从 0 秒开始,每隔 5 秒钟。#
:表示第,例如在周的位置有 5#3
意思就是第三周的星期四 。L
:意思是 List,在周位置 3L 就表示最后一周的周二。
需要注意的是,时分秒范围是从 0 开始,日月周就是 1 开始了,年可以省略
在西方,星期天是一个星期的第一天。所以星期天是 1 ,以此类推
掌握了表达式其他的就不是问题了:
月周可以使用英文单词的前三个字母,有在线生成 Cron 表达式的工具哦
Scheduler
调度器 Scheduler 可以说是 Quartz 的发动机,它是通过工厂模式来创建的,常用的两个是 StdSchedulerFactory 和 DirectSchedulerFactory。
常用的还是 StdSchedulerFactory 因为它允许使用声明式的配置,也就是可以配置到 XML/Properties 文件中,而 DirectSchedulerFactory 生成的只允许在代码中配置。
StdSchedulerFactory 默认会加载工程目录下的 quartz.properties ,如果不存在就会去读取自带的配置文件(jar 中),在配置文件中可以配置调度器属性、线程池属性(如线程数)、作业存储位置、插件配置等,可参考 jar 包里的配置。
Quartz与Spring整合
Quartz 与 Spring 能够进行完美的整合,用的也是比较多,下面就赶紧来学习一下,首先,必要的一些依赖:
1 | <dependency> |
然后是在 Spring 的配置文件中配置必要的 Bean 了,没办法,总要写配置文件的,或者使用 SpringBoot ?
在 Spring 中配置使用 Quartz 有两种方式:
MethodInvokingJobDetailFactoryBean 和 JobDetailFactoryBean,它们主要是来确定要执行的任务的,也就是 Job
MethodInvokingJobDetailFactoryBean
使用这种方式,只需要写一个普通的 Bean 即可
1 | <!-- 相当于设置执行的 Job 为 myBean 的 printMessage 方法 --> |
对应执行的 Bean:
1 | "myBean") ( |
这是一种相对简单的方法,然后都猜得出简单的方式定制性不太高,如果逻辑太复杂可能就无能为力了
JobDetailFactoryBean
当需要给作业传递数据,想要更加灵活的话就使用这种方式,配置文件:
1 | <bean id="firstComplexJobDetail" |
因为 firstComplexJobDetail 是通过代码的方式创建的并不是 Spring 容器注入的,启动时可能会报错,提示没有绑定触发器,这里使用的是 Spring 来配置的 trigger,它扫描不到,可以把 Durability 属性设置为 true 表示即使没有绑定触发器也会将其保存在任务容器中。
通过 jobClass 来指定 Job 类,对这个类的要求就是要继承 QuartzJobBean,实现 executeInternal 方法,这个还是比较熟悉的,和最开始我们学习时写法类似:
1 | public class FirstScheduledJob extends QuartzJobBean{ |
这里加了点“难度”,设置自定参数时,我们可以设置对象,通过 value-ref 实现了自动注入,还是挺爽的。
配置Trigger和Scheduler
有了任务 Job,Quartz 还差两大核心,下面就来搞一下,完善 Spring 的配置文件:
1 | <!-- 距离当前时间1秒之后执行,之后每隔两秒钟执行一次 --> |
定义 Trigger 的时候也完全活用了上面学习的两种方式,一个使用 SimpleTrigger ,一个使用 CronTrigger;
这样当程序启动后就会执行配置的两个 Job 了,simpleJobDetail 使用方式一运行,firstComplexJobDetail 使用方式二运行,还是挺和谐的。
上面的也仅仅都是 Quartz 的初级使用,高级的并发、异常处理、持久化等没有涉及,以后有机会再补充吧!
评论框加载失败,无法访问 Disqus
你可能需要魔法上网~~