深入Quartz
Github:https://github.com/ThinkMugz/springboot-demo-major,需要的伙伴儿自取。
本文主要有以下内容:
- Quartz的基本认知和源码初探
- Quartz的基本使用
- Quartz的进阶使用,包括Job中注入Mapper层、Quartz的持久化
在Java领域,有很多定时任务框架,这里简单对比一下目前比较流行的三款:
网络资源:
- Quartz文档:https://www.w3cschool.cn/quartz_doc/
- xxl-job博客:https://www.cnblogs.com/xuxueli/p/5021979.html
1 初识Quartz
如果你的定时任务没有分布式需求,但需要对任务有一定的动态管理,例如任务的启动、暂停、恢复、停止和触发时间修改,那么Quartz非常适合你。
Quartz是Java定时任务领域一个非常优秀的框架,由OpenSymphony(一个开源组织)开发,这个框架进行了优良地解耦设计,整个模块可以分为三大部分:
- Job:顾名思义,指待定时执行的具体工作内容;
- Trigger:触发器,指定运行参数,包括运行次数、运行开始时间和技术时间、运行时长等;
- Scheduler:调度器,将Job和Trigger组装起来,使定时任务被真正执行;
下面这个图简略地描述了三者之间的关系:
- 一个JobDetail(Job的实现类)可以绑定多个Trigger,但一个Trigger只能绑定一个JobDetail;
- 每个JobDetail和Trigger通过group和name来标识唯一性;
- 一个Scheduler可以调度多组JobDetail和Trigger。
为了便于理解和记忆,可以把这套设计机制与工厂车间相关联:
- Job:把Job比作车间要生产的一类产品,例如汽车、电脑等。
- Trigger:trigger可以理解为一条生产线,一条生产线只能生产一类产品,但一类产品可以由多条生产线生产。
- Scheduler:Scheduler则可以理解为车间主任,指挥调度着车间内的生产任务(Scheduler内置线程池,线程池内的工作线程即为车间工人,每个工人承担着一组任务的真正执行)。
2 Quartz基础使用
Quartz提供了丰富的API,下面我们在Springboot中使用Quartz完成一些简单的demo。
2.1 基于时间间隔的定时任务
基于时间间隔和时间长度实现定时任务,借助SimpleTrigger,例如这个场景——每隔2s在控制台输出线程名和当前时间,持续30s。
1.导入依赖:
1 | <dependency> |
2.新建Job,实现我们想要定时执行的任务:
1 | import org.quartz.Job; |
3.创建Scheduler和Trigger,执行定时任务:
1 | import com.quartz.demo.schedule.SimpleJob; |
启动测试方法后,控制台观察现象即可。注意到这么一句日志:Using thread pool ‘org.quartz.simpl.SimpleThreadPool’ - with 10 threads.,这说明Scheduler确实是内置了10个线程的线程池,通过打印线程名也印证了这一点。
另外要尤其注意的是,我们之所以通过TimeUnit.SECONDS.sleep(30);设置休眠,是因为定时任务是交由线程池异步执行的,而测试方法运行结束,主线程随之结束导致定时任务也不再执行了,所以需要设置休眠hold住主线程。在真实项目中,项目的进程是一直存活的,因此不需要设置休眠时间。
这其中的区别可以参考
https://github.com/ThinkMugz/springboot-demo-major。
2.2 基于Cron表达式的定时任务
基于Cron表达式的定时任务demo如下:
1 | import com.quartz.demo.schedule.SimpleJob; |
3 Quartz解读
整个Quartz体系涉及的类及之间的关系如下图所示:
- JobDetail:Job接口的实现类,由JobBuilder将具体定义任务的类包装而成。
- Trigger:触发器,定义定时任务的触发规则,包括执行间隔、时长等,使用TriggerBuilder创建,JobDetail和Trigger可以一对多,反之不可。触发器可以拥有多种状态。
- Scheduler:调度器,将Job和Trigger组装起来,使定时任务被真正执行;是Quartz的核心,提供了大量API。
- JobDataMap:集成Map,通过键值对为JobDetail存储一些额外信息。
- JobStore:用来存储任务和触发器相关的信息,例如任务名称、数量、状态等等。Quartz 中有两种存储任务的方式,一种在在内存(RAMJobStore),一种是在数据库(JDBCJobStore)。
3.1 Job
Job是一个接口,只有一个方法execute(),我们创建具体的任务类时要继承Job并重写execute()方法,使用JobBuilder将具体任务类包装成一个JobDetail(使用了建造者模式)交给Scheduler管理。每个JobDetail由name和group作为其唯一身份标识。
- JobDataMap中可以包含不限量的(序列化的)数据对象,在job实例执行的时候,可以使用其中的数据。
- JobDataMap继承Map,可通过键值对为JobDetail存储一些额外信息。
3.2 Trigger
Trigger有四类实现,分别如下:
- SimpleTrigger:简单触发器,支持定义任务执行的间隔时间,执行次数的规则有两种,一是定义重复次数,二是定义开始时间和结束时间。如果同时设置了结束时间与重复次数,先结束的会覆盖后结束的,以先结束的为准。
- CronTrigger:基于Cron表达式的触发器。
- CalendarIntervalTrigger:基于日历的触发器,比简单触发器更多时间单位,且能智能区分大小月和平闰年。
- DailyTimeIntervalTrigger:基于日期的触发器,如每天的某个时间段。
Trigger是有状态的:NONE, NORMAL, PAUSED, COMPLETE, ERROR, BLOCKED,状态之间转换关系:
COMPLETE状态比较特殊,我在实际操作中发现,当Trigger长时间暂停后(具体时长不太确定)再恢复,状态就会变为COMPLETE,这种状态下无法再次启动该触发器。
3.3 Scheduler
调度器,是 Quartz 的指挥官,由 StdSchedulerFactory 产生,它是单例的。Scheduler中提供了 Quartz 中最重要的 API,默认是实现类是 StdScheduler。
Scheduler中主要的API大概分为三种:
- 操作Scheduler本身:例如start、shutdown等;
- 操作Job:例如:addJob、pauseJob、pauseJobs、resumeJob、resumeJobs、getJobKeys、getJobDetail等
- 操作Trigger:例如pauseTrigger、resumeTrigger等
这些API使用非常简单,源码中也有完善的注释,这里不再赘述。
4 Quartz进阶使用
除了基本使用外,Quartz还有一些较为复杂的应用场景。
4.1 多触发器的定时任务
前文提过,一个JobDetail可以绑定多个触发器,这种场景还是有一些注意点的:
- 首先,要通过storeDurably()方法将JobDetail设置为孤立后保存存储(没有触发器指向该作业的情况);
- Scheduler通过addJob()将给定的作业添加到计划程序中-没有关联的触发器。作业将处于“休眠”状态,直到使用触发器或调度程序对其进行调度;
- 触发器通过forJob(JobDetail jobDetail)指定要绑定的JobDetail,scheduleJob()方法只传入触发器,触发后将自动执行addJob过的绑定JobDetail。
1 | import com.quartz.demo.schedule.SimpleJob; |
4.2 Job中注入Bean
有时候,我们要在定时任务中操作数据库,但Job中无法直接注入数据层,解决这种问题,有两种解决方案。
方案一:借助JobDataMap
在构建JobDetail时,可以将数据放入JobDataMap,基本类型的数据通过usingJobData方法直接放入,mapper这种类型数据手动put进去:
1 |
|
在job的执行过程中,可以从JobDataMap中取出数据,如下示例:
1 | import com.quartz.demo.entity.Person; |
这个方案相对简单,但在持久化中会遇到mapper的序列化问题:
java.io.NotSerializableException: Unable to serialize JobDataMap for insertion into database because the value of property ‘personMapper’ is not serializable: org.mybatis.spring.SqlSessionTemplate
方案二:静态工具类
创建工具类SpringContextJobUtil,实现ApplicationContextAware接口
1 | import org.springframework.beans.BeansException; |
mapper类上打上@Service注解,并赋予其name:
1 |
|
Job中通过SpringContextJobUtil的getBean获取mapper的bean:
1 | public class MajorJob implements Job { |
推荐使用这个方法。
4.3 Quartz的持久化
定时任务的诸多要素,如任务名称、数量、状态、运行频率、运行时间等,是要存储起来的。JobStore,就是用来存储任务和触发器相关的信息的。
Quartz 中有两种存储任务的方式,一种在在内存(RAMJobStore),一种是在数据库(JDBCJobStore)。
Quartz 默认的 JobStore 是 RAMJobstore,也就是把任务和触发器信息运行的信息存储在内存中,用到了 HashMap、TreeSet、HashSet 等等数据结构,如果程序崩溃或重启,所有存储在内存中的数据都会丢失。所以我们需要把这些数据持久化到磁盘。
实现Quartz的持久化并不困难,按下列步骤操作即可:
1.添加相关依赖:
1 | <!--Quartz 使用的连接池 --> |
2.编写配置:
1 | import org.quartz.Scheduler; |
3.创建quartz.properties配置文件
1 | # 实例化ThreadPool时,使用的线程类为SimpleThreadPool |
4.创建Quartz持久化数据的表
数据表初始化sql放置在External Libraries的org/quartz/impl/jdbcjobstore中,直接用其初始化相关表即可。要注意的是,用来放置这些表的库要与quartz.properties的库一致。
1 | # |