quartz

简介

Quartz,由Java编写、用于Java程序执行定时任务。可JVM独立运行,也可集成使用。

  • 可以通过 Calendar 执行(排除节假日)
  • 指定某个时间无限循环执行, 比如每五分钟执行一次
  • 固定时间执行,如每周周一上午10点执行,每月2号上午10点执行

相较于java.util.Timer, Quartz增加了很多功能:

  1. 持久性作业 - 保持调度定时的状态;
  2. 作业管理 - 对调度作业进行有效的管理;
Quartz核心概念 中文 描述
Job 任务 具体的作业。what to do
Trigger 触发器 触发Job,指定Job的执行时间、执行间隔、运行次数等。 when to do
Scheduler 调度器 联接Job和Trigger,指定Trigger执行指定Job

一个 Job 可以对应多个 Trigger, 但一个 Trigger 只能对应一个 Job。job(1) <=> Trigger(n)

graph TB
A[Scheduler]
A ==> B[Job]
A ==> C[Trigger]

B -.- B1(JobDetail)
B -.- B2(JobExecutionContext)

C -.- C1(CronTrigger)
C -.- C2(SimpleTrigger)

配置核心概念的方式:

  1. java直接配置并实现。
  2. xml配置Quartz参数,Java只负责业务实现。
  3. 和spring相关框架搭配。

Job

定义需要具体执行的任务。

Job

是个接口,只有一个方法execute(JobExecutionContext context),在实现类的execute中编写业务逻辑。

  • JobExecutionContext:包含了Quartz运行时的环境以及Job本身的详细数据信息,其中Job运行时的信息保存在 JobDataMap 实例中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Job接口源码
package org.quartz;

public interface Job {
void execute(JobExecutionContext var1) throws JobExecutionException;
}

// 在Job接口的实现类中,定义业务逻辑!!!(真正要执行的内容)
public class AutoPrint implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = simpleDateFormat.format(new Date());
Integer random = new Random().nextInt(100);
System.out.println("=================Time:" + time + ", Random:" + random);
}
}

JobDetail

用来绑定指定的Job,为Job实例提供属性:

  • name:Job 名字
  • group
  • jobClass
  • jobDataMap:实现了JDK的Map接口,可以以Key-Value的形式存储数据。JobDetailTrigger都可以使用JobDataMap来设置一些参数信息
  • 不直接接受一个 Job 的实例,相反它接收一个 Job 实现类,以便运行时通过 newInstance() 的反射机制实例化 Job。

每次Scheduler调度执行一个Job的时候,①先根据JobDetail找到对应的Job,②根据JobDetail重新创建一个Job实例,③执行Job实例的execute方法。④任务执行结束后,关联的Job实例会被释放,且被JVM GC清除。

JobDetail定义的是任务配置,真正执行是Job。
这是因为任务有可能【并发执行】,如果Scheduler直接使用Job,会存在对同一Job实例并发访问的问题。
而JobDetail & Job 方式,Scheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。

1
2
3
4
5
6
7
8
9
10
// 绑定AutoPrint这个执行逻辑
JobDetail jobDetail = JobBuilder.newJob(AutoPrint.class)
.withIdentity("jobKey", "groupKey")
.setJobData(jobDataMap)
.build();

// 绑定AutoPrint1执行逻辑
JobDetail jobDetail1 = JobBuilder.newJob(AutoPrint1.class)
.withIdentity("jobKey1", "groupKey1")
.build();

任务状态

  1. 有状态Job:

    • 共享同一个 JobDataMap 实例,每次任务执行对 JobDataMap 所做的更改会保存下来,后面的执行可以看到这个更改,即每次执行任务后都会对后面的执行发生影响。
    • 不能并发执行。上次job未执行完毕,则下次job将阻塞等待,直到上次job执行完毕。
  2. 无状态Job:

    • 在执行时拥有自己的 JobDataMap 拷贝,对 JobDataMap 的更改不会影响下次的执行
    • 并发执行。

监听

JobListener 监听任务执行前事件、任务执行后事件;

Trigger

触发任务。是个接口,描述触发 job 执行的时间触发规则。

  1. new Trigger().startAt():表示触发器首次被触发的时间;
  2. new Trigger().endAt():表示触发器结束触发的时间;

包括两类触发器:

  1. SimpleTrigger:简单,适合次数和间隔
  2. CronTrigger:灵活常用,适合次数、间隔等,也适合精准定时。

也可以使用ScheduleBuilder的子类来触发操作:

  1. SimpleScheduleBuilder
  2. CronScheduleBuilder。

SimpleTrigger

  1. 强调次数,比如每天执行若干次。用于当且仅当需调度一次、或者以固定时间间隔周期执行调度。
  2. 精准指定间隔,如,程序运行5s后开始执行Job,执行Job 5s后结束执行。
  3. 无法设置具体时间,及不适合每天定时执行任务的场景,比如不能设置每天半夜十二点执行任务。
1
2
3
4
5
6
7
8
9
10
11
12
//创建SimpleTrigger startTime end 分别表示触发器的首次触发时间和不再被触发的时间
SimpleTrigger trigger = newTrigger()
.withIdentity("triggerKey", "groupKey")
.startAt(startTime)
.endAt(end)
.withSchedule(
SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInHours(2)
.withRepeatCount(0) // 定义重复执行次数是无限次
)
.build();

CronTrigger

基于Cron表达式定义更灵活的时间规则,是基于日历的作业调度。

1
2
3
4
5
//创建CornTrigger 比如下面的就是设计每天中午十二点整触发
CronTrigger trigger = newTrigger()
.withIdentity("triggerKey", "groupKey")
.withSchedule(cronSchedule("0 0 12 * * ?"))
.build();

Cron表达式

设置定时任务执行的时间

1
2
3
4
<bean class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<!-- 设置调度的时间规则-->
<property name="cronExpression" value="0 */15 * * * ?" />
</bean>

cronExpression是以5或6个空格隔开的字符串。分成6或7个域。每个域代表一个含义。

在线生成Cron表达式:https://cron.qqe2.com/

1
2
3
4
5
6
7
8
9
10
# 7种
[秒] [分] [时] [日] [月] [周] [年]
Seconds Minutes Hours DayofMonth Month DayofWeek Year

# 6种
[秒] [分] [时] [日] [月] [周]
Seconds Minutes Hours DayofMonth Month DayofWeek

# 每月的2日10:00:00,触发调度
0 0 10 2 * ?

字符不区分大小写

含义 可能值 有效范围
Seconds , - * / 0-59的整数
Minutes , - * / 0-59的整数
Hours , - * / 0-23的整数
DayofMonth , - * / ? L W C 0-31的整数
Month , - * / 0-11的整数或JAN-DEc
DayofWeek , - * / ? L C # 1,2,3,4,5,6,7或 SUN,MON,TUE,WED,THU,FRI,SAT两种,1星期天,2星期一
Year , - * / 1970-2099的整数

符号含义

符号 用途 示例
* 匹配该域的任意值 如在Minutes域使用*, 表示每分钟都会触发事件
/ 起始时间开始触发,每隔固定时间再触发 如在Minutes域使用5/20,则意味着5分钟触发一次,以后每隔20分钟再次触发,如25,45等分别触发一次
- 范围 如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
, 列出枚举值 如在Minutes域使用5,20,则意味着在5和20分每分钟触发一次
? 只用在DayofMonth和DayofWeek,匹配该域任意值 因DayofMonth和DayofWeek会冲突,故要在每月20日触发调度,则不管20日到底是星期几,只能使用如下写法: 13 13 15 20 ?。即最后一位只能用?而不能用,如果使用*则表示不管星期几都会触发,实际并非如此
L 只用在DayofMonth和DayofWeek,表示最后 如在DayofWeek域使用5L,表示只在最后的一个星期四触发
W 只用在DayofMonth,在本月中、离指定日期(周一到周五)最近的有效工作日触发 在DayofMonth使用5W,如果5日是星期六,则将在本月中最近的工作日:星期五,即4日触发
LW 连用,只用在DayofMonth,在某个月最后一个工作日 即最后一个星期五
# 只能在DayofMonth域,确定每个月第几个星期几 在4#2,表示某月的第2个星期3

举例:
| 例 | 用途|
| – | – |
| 0 0 2 1 ? | 每月的1日02点 |
| 0 15 10 ? MON-FRI | 周一到周五每日的10:15 |
| 0 15 10 ?
6L 2002-2006 | 2002-2006年,每年最后一个周五的10:15 |
| 0 0 10,14,16 ? | 每天的10点14点16点 |
| 0 0/30 9-17 ? | 9点到17点,从0分开始,每隔30分钟执行一次 |
| 0 0 12 ? WED | 每周三的12点:00 |
| 0 0 12
? | 每天12:00 |
| 0
14 ? | 每天14点到14:59的每一分钟都执行 |
| 0 15 10 ? * 6#3 | 每个月的第3个星期5当天的10:15:00 |

监听

TriggerListener 监听触发器触发前事件、触发后事件

Scheduler

调度器相当于一个容器,装载着任务Job和触发器Trigger。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//创建计划任务抽象工厂
SchedulerFactory schedulerFactory = new StdSchedulerFactory();


//创建计划任务【从工厂生产任务调度】
Scheduler scheduler = schedulerFactory.getScheduler();


// 把job和trigger加入调度
scheduler.scheduleJob(jobDetail, trigger);

//启动scheduler
scheduler.start();

// 6. 任务执行后20秒后休眠
Thread.sleep(startTime.getTime() + 20000L);

//关闭scheduler
if (scheduler.isStarted()) {
scheduler.shutdown(true);
}

Scheduler 是个接口,定义了多个接口方法, 允许外部通过组及名称访问和控制容器中 Trigger 和 JobDetail。

Trigger 和 JobDetail 注册到 Scheduler 中, 两者在 Scheduler 中拥有各自的组及名称, 组及名称是 Scheduler 查找定位容器中某一对象的依据, Trigger 的组及名称必须唯一, JobDetail 的组和名称也必须唯一(但可以和 Trigger 的组和名称相同,因为它们是不同类型的)。

Scheduler 可以将 Trigger 绑定到某一 JobDetail 中, 这样当 Trigger 触发时, 对应的 Job 就被执行。一个 Job 可以对应多个 Trigger, 但一个 Trigger 只能对应一个 Job。

SchedulerFactory

通过 SchedulerFactory 创建一个 SchedulerFactory 实例。

SchedulerContext

Scheduler 拥有一个 SchedulerContext,保存着 Scheduler 上下文信息,Job 和 Trigger 都可以访问 SchedulerContext 内的信息。

SchedulerContext 内部通过一个 Map,以键值对的方式维护这些数据,SchedulerContext 为保存和获取数据提供了多个 put()getXxx() 的方法。可以通过 Scheduler#getContext() 获取对应的 SchedulerContext 实例。

监听

SchedulerListener 听调度器开始事件、关闭事件等等,可以注册相应的监听器处理感兴趣的事件。

ThreadPool

Scheduler 使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

Scheduler调度线程主要有两个:

  1. 执行常规调度的线程:轮询存储的所有trigger,如果有需要触发的trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该trigger关联的任务
  2. 执行misfiredtrigger的线程:扫描所有的trigger,查看是否有misfiredtrigger,如果有的话根据misfire的策略分别处理(fire now OR wait for the next fire)。

org.quartz.simpl.SimpleThreadPool’ - with 10 threads

在Quartz中有两类线程:Scheduler调度线程和任务执行线程。

  • 任务执行线程:Quartz不会在主线程(QuartzSchedulerThread)中处理用户的Job。Quartz把线程管理的职责委托给ThreadPool,一般的设置使用SimpleThreadPool。

  • SimpleThreadPool创建了一定数量的WorkerThread实例来使得Job能够在线程中进行处理。WorkerThread是定义在SimpleThreadPool类中的内部类,它实质上就是一个线程。

1
2
# 默认,且默认10个线程
org.quartz.simpl.SimpleThreadPool

Calendars

用于触发器的触发计划中排除时间块,比如国庆期间不营业。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.quartz.impl.calendar.HolidayCalendar;

HolidayCalendar cal = new HolidayCalendar();
cal.addExcludedDate( someDate );
cal.addExcludedDate( someOtherDate );

sched.addCalendar("myHolidays", cal, false);


Trigger t = newTrigger()
.withIdentity("myTrigger")
.forJob("myJob")
.withSchedule(dailyAtHourAndMinute(9, 30)) // execute job daily at 9:30
.modifiedByCalendar("myHolidays") // 假日除外
.build();
  1. AnnualCalendar 年历
  2. CronCalendar cron 日历
  3. DailyCalendar 每日日历
  4. HolidayCalendar 假日日历
  5. MonthlyCalendar 月历
  6. WeeklyCalendar 周历

Job Stores

Quartz中的trigger和job需要存储下来才能被使用。

主要存储内容是scheduler下的job、trigger、calender等==work data==

Quartz中有两种常用的存储方式:

  1. RAMJobStore:内存存储。存取速度快,停止后数据丢失。
  2. JDBCJobStore: 数据库存储。持久化数据,集群中只能使用持久化存储。

1. RAMJobStore(内存存储 )

【默认配置】scheduler 被关闭或机器故障后,数据就丢了

1
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

2. JDBCJobStore(数据库存储)

支持的数据库有==Oracle, PostgreSQL, MySQL, MS SQLServer, HSQLDB, and DB2==,需要创建quartz单独的数据库表,各类数据库的quartz表结构语句在quartz源代码的==“docs/dbTables”==下。参见集群相关库表。

事务类型:

  1. JobStoreTX(默认,常见):TM 是 transactions-managed 的缩写,该类会处理事务的提交和回滚等逻辑。
  2. JobStoreCMT:CMT 是 container-managed-transactions 的缩写,当前类不处理事务的提交和回滚,全权由应用服务器自身处理。quartz会让应用服务来管理quartz事务
配置quartz 使用JobStoreTX
1
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
配置数据库委托

数据库委托信息在包==org.quartz.impl.jdbcjobstore==下

1
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
quartz数据库表的前缀
1
org.quartz.jobStore.tablePrefix = QRTZ_
配置数据源

可以指定==org.quartz.jobStore.useProperties==为==true==,这样的就是以==String==类型存储==JobDataMaps==信息,而不是==BLOB==

1
org.quartz.jobStore.dataSource = myDS

3. TerracottaJobStore(Terracotta服务器存储)

Quartz被Terracotta收购了。

TerracottaJobStore可以是集群,也可以是单点的,不过都可以为作业数据提供存储介质,因为数据存储在Terracotta服务器中,因此在应用程序重新启动期间是持久存储的。、

它的性能比JDBCJobStore使用数据库要好得多(大约好一个数量级),但是比RAMJobStore慢得多。

配置使用TerracottaJobStore
1
2
org.quartz.jobStore.class = org.terracotta.quartz.TerracottaJobStore
org.quartz.jobStore.tcConfigUrl = localhost:9510

集群

Quartz在某一个时刻,将随机指定集群中的某一个节点,来完成任务。

  1. 一致性:即节点之间在同一时刻是互斥关系,通过锁机制,保证多个节点Scheduler实例对任务的调度唯一。
  2. 高可用性:当一个节点执行失败,可以依靠另一个节点运行。

高可用的两种配置:

  1. 当一个节点Sheduler实例执行某个Job失败时,希望由另一正常工作节点的Scheduler实例接过这个Job重新运行。则,配置给JobDetail对象的Job可恢复属性必须设为true(jobDetail.setRequestsRecovery(true))。

    1
    2
    >    <property name="requestsRecovery" value="true" />
    >
  1. 当一个节点Scheduler实例执行某个Job失败时,希望Job不再重新运行,而是由另一个节点Scheduler实例在下一次触发时间触发。则,可恢复属性应该设置为false(默认为false)。

    1
    2
    >    <property name="requestsRecovery" value="false" />
    >

Scheduler实例出现故障后多快能被侦测到,取决于每个Scheduler检入间隔(org.quartz.jobStore.clusterCheckinInterval)。

集群架构

  1. Quartz集群中,每个节点是一个独立的Quartz应用,必须对每个节点分别启动或停止。

  2. Quartz集群中,独立Quartz节点并不与其他节点通信,而是通过【相同的数据库表】感知另一Quartz应用。

  3. 只有使用【持久化JobStore】存储Job和Trigger才能完成Quartz集群。因此集群依赖于数据库

JobStore的存储方式有两种:

  • RAMJobStore:将scheduler存储在内存中,但是重启服务器信息会丢失
  • JDBCJobStore:将scheduler存储在数据库中。适用于集群。

因为集群依赖于数据库,所以必须采用数据库用来持久化存储scheduler数据。

数据库利用QRTZ_LOCKS表执行“行锁”(数据库悲观锁),来保证某时刻某job只能被一个节点执行。

graph TB

A0(Quartz服务器节点1) --> C(Quartz数据库)
A1(Quartz服务器节点2) --> C
A2(Quartz服务器节点...) --> C

集群相关数据库表

集群依赖于数据库,所以首先创建Quartz数据库表。

Quartz官方提供了生成数据库表的sql语句:

https://github.com/quartz-scheduler/quartz/blob/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql

表名 含义
QRTZ_JOB_DETAILS 记录每个任务JOB的详细信息
QRTZ_TRIGGERS 记录每个触发器Trigger的详细信息
QRTZ_CRON_TRIGGERS 存放cron类型的触发器
QRTZ_SIMPLE_TRIGGERS 存储简单的trigger,包括重复次数,间隔,以及触发次数。
QRTZ_FIRED_TRIGGERS 存储已经触发的trigger相关信息,trigger随着时间的推移状态发生变化,直到最后trigger执行完成,从表中被删除。相同的trigger和job,每触发一次都会创建一个实例;从刚被创建的ACQUIRED状态,到EXECUTING状态,最后执行完从数据库中删除;
QRTZ_PAUSED_TRIGGER_GRPS 存储已暂停的 Trigger 组的信息
QRTZ_SIMPROP_TRIGGERS 存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种类型的触发器
QRTZ_BLOB_TRIGGERS 自定义的triggers使用blog类型进行存储(非自定义的triggers不会存放在此表中,Quartz提供的triggers包括:CronTrigger,CalendarIntervalTrigger,DailyTimeIntervalTrigger以及SimpleTrigger)
QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 信息
QRTZ_SCHEDULER_STATE 存储集群下所有节点的scheduler实例名,会定期检查scheduler是否失效,启动多个scheduler,查询数据库。记录了最后最新的检查时间。
QRTZ_LOCKS 存储程序的悲观锁的信息(假如使用了悲观锁)

QRTZ_SCHEDULER_STATE:

1
<bean name="quartzScheduler"   class="org.springframework.scheduling.quartz.SchedulerFactoryBean">

Quartz提供的锁表,为多个节点调度提供分布式锁,实现分布式调度,默认有2个锁:

  • STATE_ACCESS:主要用在scheduler定期检查是否有效的时候,保证只有一个节点去处理已经失效的scheduler。
  • TRIGGER_ACCESS:主要用在TRIGGER被调度的时候,保证只有一个节点去执行调度。

还有其他锁:

  • CALENDAR_ACCESS
  • JOB_ACCESS
  • MISFIRE_ACCESS
sql附录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;

CREATE TABLE QRTZ_JOB_DETAILS(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(190) NOT NULL,
JOB_GROUP VARCHAR(190) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
JOB_NAME VARCHAR(190) NOT NULL,
JOB_GROUP VARCHAR(190) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(190) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_CRON_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13,4) NULL,
DEC_PROP_2 NUMERIC(13,4) NULL,
BOOL_PROP_1 VARCHAR(1) NULL,
BOOL_PROP_2 VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_BLOB_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_CALENDARS (
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(190) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (SCHED_NAME,CALENDAR_NAME))
ENGINE=InnoDB;

CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_FIRED_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
INSTANCE_NAME VARCHAR(190) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(190) NULL,
JOB_GROUP VARCHAR(190) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SCHEDULER_STATE (
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(190) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME))
ENGINE=InnoDB;

CREATE TABLE QRTZ_LOCKS (
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME))
ENGINE=InnoDB;

CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);

CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);

CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);

commit;

注意问题

1. 时间同步问题

Quartz并不关心是在相同机器、还是不同的机器运行节点。
|水平集群|垂直集群|
|–|–|
|集群放置在不同的机器上|节点跑在同一台机器上|
|存在时间同步问题|存在单点故障的问题,无法真正高可用,一旦机器崩溃,全部终止|

节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。

另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。

需要同步计算机时钟:比如两者都是使用某一个Internet时间服务器(Internet Time Server ITS)

1
2
3
4
5
6
7
yum install -y ntpdate

# 同步时间
ntpdate time.nist.gov

# 查看同步后时间
date
2. 节点争抢Job问题

Quartz官网,当前还不存在一个方法来指派(钉住) 一个 Job 只能由集群中的某个特定节点执行。

Quartz使用一个随机的负载均衡算法,Job以随机的方式由不同的实例执行

quartz.properties

Quartz支持配置文件,好处是比编写代码简单,且修改后不需要重新编译源码

  1. 文件定义了Quartz应用运行时行为,还包含了许多能控制Quartz运转的属性
  2. 文件应放在工程的classpath中
  3. 主要包括scheduler、threadPool、jobStore、plugin等
  4. 所有属性使用固定前缀org.quartz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#

#============================================================================
# Configure Main Scheduler Properties
#============================================================================

# 实例名,集群中所有节点的instanceName必须相同。【集群中每个实例配置文件quartz.properties必须相同】
org.quartz.scheduler.instanceName: QuartzScheduler

# 实例ID,每个节点实例的instanceId必须唯一,可借助AUTO自动生成(主机名称+时间戳)
org.quartz.scheduler.instanceId = AUTO

org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
#============================================================================
# Configure ThreadPool
#============================================================================
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
# 并发线程个数
org.quartz.threadPool.threadCount: 10
# 优先级
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

org.quartz.jobStore.misfireThreshold: 1000
#============================================================================
# Configure JobStore
#============================================================================

# 默认存储到内存
#org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
# 集群需要持久化存储,使用JobStoreTX事务类型
org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
# 数据库委托信息
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties:true

#============================================================================
#havent cluster spring
#============================================================================
# 集群部署(分布式)
org.quartz.jobStore.isClustered = true

# 检入间隔时长
org.quartz.jobStore.clusterCheckinInterval = 20000


org.quartz.jobStore.tablePrefix:QRTZ_
#org.quartz.jobStore.dataSource:qzDS

#============================================================================
# Configure Datasources
#============================================================================
#JDBC
#org.quartz.dataSource.qzDS.driver:com.mysql.jdbc.Driver
#org.quartz.dataSource.qzDS.URL:jdbc:mysql://localhost:3306/quartz_test2
#org.quartz.dataSource.qzDS.user:root
#org.quartz.dataSource.qzDS.password:
#org.quartz.dataSource.qzDS.maxConnection:10

各框架使用

QuartzJobBean

无论SpringMvc还是SpringBoot都可以使用spring帮我们封装的Job抽象QuartzJobBean

QuartzJobBean是对Job的实现类,增加了executeInternal抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.springframework.scheduling.quartz;

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

// 抽象类
public abstract class QuartzJobBean implements Job {

@Override
public final void execute(JobExecutionContext context) throws JobExecutionException {
//具体请参加源代码
}

// 新增了一个抽象方法
protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;

}

QuartzJobBean依赖的jar包——

1
2
3
4
5
6
7
8
9
10
11
<!-- 在SpringMVC 中可以额外借助 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>

<!-- 在SpringBoot 中连同quartz一起 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

SpringMVC

依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<properties>
<quartz.version>2.2.1</quartz.version>
</properties>

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>${quartz.version}</version>
</dependency>

<!-- 基于spring架构,提供QuartzJobBean抽象类 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
xml配置

resources/ microservice-quartz.xml

Job

  • JobDetailFactoryBean
  • MethodInvokingJobDetailFactoryBean

Trigger

  • CronTriggerFactoryBean
  • SimpleTriggerFactoryBean

Scheduler

  • SchedulerFactoryBean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">


<!-- 1. 创建调度器。加入调度工厂SchedulerFactoryBean,引入所有调度触发器 -->
<bean
name="quartzScheduler"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">

<!-- 1.1 装载所有触发器 -->
<property name="triggers">
<list>
<ref bean="autoCompleteTrigger"/>
<ref bean="updateCompleteTimeTrigger"/>
<ref bean="overTimeRemindTrigger"/>
</list>
</property>

<!--可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 -->
<property name="overwriteExistingJobs" value="true" />
<property name="applicationContextSchedulerContextKey" value="applicationContext" />
<property name="configLocation" value="classpath:quartz.properties" />
<property name="dataSource" ref="dataSource"></property>

<!-- 定时任务开关 -->
<property name="autoStartup" value="${scheduleJob.autoStartup:false}"></property>
</bean>


<!-- 2. 创建CronTrigger触发器1,每3分钟执行一次-->
<bean id="autoCompleteTrigger"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<!-- 2.1 关联具体任务Job -->
<property name="jobDetail" ref="autoCompleteJobDetail" />
<!-- 2.2 Cron表达式 -->
<property name="cronExpression" value="0 */3 * * * ?" />
<property name="startDelay" value="500" />
<property name="misfireInstruction" value="2"></property>
</bean>

<!-- 3. 创建任务JobDetail-->
<bean id="autoCompleteJobDetail"
class="org.springframework.scheduling.quartz.JobDetailFactoryBean">

<!-- 3.1 绑定具体的Job实现类 -->
<property name="jobClass">
<value>com.my.services.quartz.AutoCompleteTask</value>
</property>

<!-- 3.1 或者手动指定目标类和方法,手动指定的可以是与Job无关的普通类/方法
<!-- 目标对象 -->
<property name="targetObject" ref="cleanController"/>
<!-- 目标方法 -->
<property name="targetMethod" value="cleanFile"/>
-->

<property name="durability" value="true" />
<property name="requestsRecovery" value="true" />
</bean>

<!-- 2. 创建SimpleTrigger触发器 -->
<!--<bean class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean" id="simpleTrigger">-->
<!-- 引用任务 -->
<!--<property name="jobDetail" ref="jobDetail"/>-->
<!-- 指定循环时间,以秒为单位 -->
<!--<property name="repeatInterval" value="10000"/>-->
<!--</bean>-->

</beans>
xml文件引入

resources/ microservice-quartz.xml

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.annotation.ImportResource;

// 在主应用程序入口,引入配置xml文件
@ImportResource(locations = { "classpath*:/spring-configuration/spring-context*.xml",
"classpath*:/microservice-configuration/microservice-*.xml" })

public class ApnApplication {
public static void main(final String[] args) {
SpringApplication.run(ApnApplication.class, args);
}
}
执行类和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.quartz.SchedulerException;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;


@PersistJobDataAfterExecution
@DisallowConcurrentExecution // 不允许并发执行

// 所有定时任务都需要继承QuartzJobBean
public class AutoCompleteTask extends QuartzJobBean {

// 定时执行的方法。所有定时任务都需要实现Quartz提供的JobExecutionException接口。
@Override
protected void executeInternal(final JobExecutionContext jobExecutionContext) throws JobExecutionException {

final IAutoCompleteTaskService autoCompleteTaskService = getApplicationContext(jobExecutionContext)
.getBean("autoCompleteTaskService", IAutoCompleteTaskService.class);

autoCompleteTaskService.autoCompleteTask();

}

private ApplicationContext getApplicationContext(final JobExecutionContext jobexecutioncontext) {

try {
return (ApplicationContext) jobexecutioncontext.getScheduler().getContext().get("applicationContext");
} catch (final SchedulerException e) {
throw new RuntimeException(e);
}
}

}
springmvc调用@service方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// quartz bean中配置属性
<!-- 将spring上下文以key/value存放在quartz上下文中,可以用applicationContextSchedulerContextKey定义的key得到对应的ring上下文 -->

<property name="applicationContextSchedulerContextKey" value="applicationContext" />


// 通过getApplicationContext方法传入quartz的context即可获取ApplicationContext上下文,进而获取对应的bean
private ApplicationContext getApplicationContext(final JobExecutionContext jobexecutioncontext) {
try {
return (ApplicationContext) jobexecutioncontext.getScheduler().getContext().get("applicationContext");
} catch (final SchedulerException e) {
throw new RuntimeException(e);
}
}

//在QuartzJobBean实现类中使用service
final MyService myService = getApplicationContext(jobExecutionContext)
.getBean("myService", MyService.class);

myService.getList();

Spring boot

依赖
1
2
3
4
5
6
7
8
<!--spring boot集成quartz-->

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
配置类

StoreDurably

默认情况下,当 Job 没有对应的 Trigger 时,Job 不能直接被加入调度器,或者在 Trigger 过期之后, 没有关联 Trigger 的 Job 也会被删除。可以通过 JobBuilder 的 StoreDurably 使 Job 独立存储于调度器中

添加job不带触发器必须写storeDurably()否则报如下异常,durable指明任务就算没有绑定Trigger仍保留在Quartz的JobStore中。

storeDurably的xml写法。

1
<property name="durability" value="true" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class QuartzConfig {
//具体任务:JobDetail实例绑定job
@Bean
public JobDetail uploadTaskDetail() {
return JobBuilder
.newJob(MyJob.class)
.withIdentity("MyJobDetail")
.storeDurably()
.build();
}
//触发器配置:CronTrigger,并绑定JobDetail
@Bean
public Trigger uploadTaskTrigger() {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("*/5 * * * * ?");
return TriggerBuilder.newTrigger().forJob(uploadTaskDetail())
.withIdentity("MyTrigger")
.withSchedule(scheduleBuilder)
.build();
}
}
执行类和方法

使用spring抽象类QuartzJobBean

1
2
3
4
5
6
7
public class MyJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//TODO 这里写定时任务的执行逻辑
System.out.println("简单的定时任务执行时间:"+new Date().toLocaleString());
}
}
spring boot调用@service方法

问题描述:定时任务QuartzJobBean实现类中,@Autowired 注入service 报错NullPointerException,即spring boot spring mvc 在QuartzJobBean实现类中,不能直接 注入service

原因:由于定时任务Quartz的job是在quartz中实例化出来的,优先级高于Spring的自动注入,创建的Service将由Quartz管理,而不是Spring,所以无法注入。不受spring的管理。所以就导致注入不进去了

办法:

  1. 在方法中获取bean的方式。不通过spring的@Autowired 注入service,而是直接获取spring上下文中的service类。 【参见springMvc的解决方式】
  2. 利用@Autowired 注入service,需要做一些配置。
  3. 其他方法注入service。
方法1:获取bean的方式

首先,定义一个ApplicationContextAware的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.demoinit.quartz.dynamic;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;


@Component
public class ApplicationContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext;

public static ApplicationContext getApplicationContext() {
return applicationContext;
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtil.applicationContext = applicationContext;

}

public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
}

而后,在servcie的实现类注解添加名字,以便bean的获取

1
2
3
4
5
6
7
@Service("UserService")
public class UserService {
public List<String> getUserList() {
// 业务逻辑
return Arrays.asList("a", "b", "c", "d");
}
}

最后,使用bean的方式拿到service

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AutoPrint extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {

// 直接使用bean的方式拿到service
UserService userService = (UserService) ApplicationContextUtil.getBean("UserService");

int no = new Random().nextInt(100);
List<String> list = userService.getUserList();
System.out.println("===============no:" + no);
System.out.println(list);
}
}
方法2:@Autowired 注入service

首先,定义一个AdaptableJobFactory的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.demoinit.quartz.dynamic;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;

@Component("MyAdaptableJobFactory")
public class MyAdaptableJobFactory extends AdaptableJobFactory {

@Autowired
private AutowireCapableBeanFactory capableBeanFactory;

@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
// 进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}

而后,在Quartz的配置文件中将job实例化,能够操作进行Spring 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.demoinit.quartz.dynamic;

import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration
public class QuartzConfig {
// 引入组件
@Autowired
private MyAdaptableJobFactory myAdaptableJobFactory;

@Bean
public SchedulerFactoryBean schedulerFactoryBean(){
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

schedulerFactoryBean.setOverwriteExistingJobs(true);

schedulerFactoryBean.setStartupDelay(10);

// 防止Spring依赖注入为null,将job实例化,能够操作进行Spring 注入
schedulerFactoryBean.setJobFactory(myAdaptableJobFactory);

return schedulerFactoryBean;
}

/** 创建schedule **/
/** 方便其他位置调用,如 private Scheduler scheduler = (Scheduler)ApplicationContextUtil.getBean("scheduler"); */
@Bean(name = "scheduler")
public Scheduler scheduler() {
return schedulerFactoryBean().getScheduler();
}
}

最后,调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.demoinit.quartz.dynamic;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.List;
import java.util.Random;

public class AutoPrint extends QuartzJobBean {

// 可以直接注入service并使用
@Autowired
private UserService userService;

@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
int no = new Random().nextInt(100);
List<String> list = userService.getUserList();
System.out.println("===============no:" + no);
System.out.println(list);
}
}
动态案例
调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.demoinit.quartz.dynamic;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
* 实现项目启动后自运行
*/
@Component
public class JobSchedule implements CommandLineRunner {

@Override
public void run(String... strings) throws Exception {
System.out.println("任务调度开始==============任务调度开始");
QuartzBean quartzBean = new QuartzBean();
quartzBean.setJobId("autoPrintJob");
quartzBean.setJobClassName("com.example.demoinit.quartz.dynamic.AutoPrint");
quartzBean.setCronExpression("*/4 * * * * ? *");
QuartzUtils.createScheduler(quartzBean);

QuartzUtils.pauseJob ("autoPrintJob");
QuartzUtils.resumeScheduleJob ("autoPrintJob");
QuartzUtils.runOnce ("autoPrintJob");

quartzBean.setCronExpression("*/2 * * * * ? *");
QuartzUtils.updateScheduleJob (quartzBean);
QuartzUtils.deleteScheduleJob ("autoPrintJob");
System.out.println("任务调度结束==============任务调度结束");
}
}
增删改操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package com.example.demoinit.quartz.dynamic;

import org.quartz.*;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;

@Component
public class QuartzUtils {
private static Scheduler scheduler = (Scheduler)ApplicationContextUtil.getBean("scheduler");

public static void createScheduler(QuartzBean quartzBean) {
try {
// JobDetail
Class<? extends QuartzJobBean> jobClass = (Class<? extends QuartzJobBean>) Class.forName(quartzBean.getJobClassName());
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(quartzBean.getJobId()).build();

// Trigger
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExpression());
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(quartzBean.getJobId()).withSchedule(
cronScheduleBuilder
).build();

// 装载
scheduler.scheduleJob(jobDetail, trigger);
// scheduler.start(); // 此处不需要进行start了,springboot默认会启动调度
} catch (SchedulerException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

/**
* 根据jobId暂停定时任务
* @param jobId
*/
public static void pauseJob(String jobId){
JobKey jobKey = JobKey.jobKey(jobId);
try {
scheduler.pauseJob(jobKey);
} catch (SchedulerException e) {
System.out.println("暂停定时任务出错:"+e.getMessage());
}
}

/**
* 根据任务名称恢复定时任务
* @param jobId 定时任务名称
* @throws SchedulerException
*/
public static void resumeScheduleJob(String jobId) {
JobKey jobKey = JobKey.jobKey(jobId);
try {
scheduler.resumeJob(jobKey);
} catch (SchedulerException e) {
System.out.println("启动定时任务出错:"+e.getMessage());
}
}
/**
* 根据任务名称立即运行一次定时任务
* @param jobId 定时任务名称
* @throws SchedulerException
*/
public static void runOnce(String jobId){
JobKey jobKey = JobKey.jobKey(jobId);
try {
scheduler.triggerJob(jobKey);
} catch (SchedulerException e) {
System.out.println("运行定时任务出错:"+e.getMessage());
}
}

/**
* 更新定时任务
* @param quartzBean 定时任务信息类
* @throws SchedulerException
*/
public static void updateScheduleJob(QuartzBean quartzBean) {
try {
//获取到对应任务的触发器
TriggerKey triggerKey = TriggerKey.triggerKey(quartzBean.getJobId());
//设置定时任务执行方式
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExpression());
//重新构建任务的触发器trigger
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//重置对应的job
scheduler.rescheduleJob(triggerKey, trigger);
} catch (SchedulerException e) {
System.out.println("更新定时任务出错:"+e.getMessage());
}
}

/**
* 根据定时任务名称从调度器当中删除定时任务
* @param jobId 定时任务名称
* @throws SchedulerException
*/
public static void deleteScheduleJob(String jobId) {
JobKey jobKey = JobKey.jobKey(jobId);
try {
scheduler.deleteJob(jobKey);
} catch (SchedulerException e) {
System.out.println("删除定时任务出错:"+e.getMessage());
}
}
}
参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demoinit.quartz.dynamic;

public class QuartzBean {
private String jobId;
private String jobClassName;
private String cronExpression;

public String getJobId() {
return jobId;
}

public void setJobId(String jobId) {
this.jobId = jobId;
}

public String getJobClassName() {
return jobClassName;
}

public void setJobClassName(String jobClassName) {
this.jobClassName = jobClassName;
}

public String getCronExpression() {
return cronExpression;
}

public void setCronExpression(String cronExpression) {
this.cronExpression = cronExpression;
}
}

参数Job实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demoinit.quartz.dynamic;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.List;
import java.util.Random;

public class AutoPrint extends QuartzJobBean {
@Autowired
private UserService userService;

@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
int no = new Random().nextInt(100);
List<String> list = userService.getUserList();
System.out.println("===============no:" + no);
System.out.println(list);
}
}

参数job调用的service层

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demoinit.quartz.dynamic;

import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
public class UserService {
public List<String> getUserList() {
return Arrays.asList("a", "b", "c", "d");
}
}
环境配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.demoinit.quartz.dynamic;

import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration
public class QuartzConfig {
@Autowired
private MyAdaptableJobFactory myAdaptableJobFactory;

@Bean
public SchedulerFactoryBean schedulerFactoryBean(){
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
//覆盖已存在的任务
schedulerFactoryBean.setOverwriteExistingJobs(true);
//延时10秒启动定时任务,避免系统未完全启动却开始执行定时任务的情况
schedulerFactoryBean.setStartupDelay(10);
// 防止Spring依赖注入为null
schedulerFactoryBean.setJobFactory(myAdaptableJobFactory);

return schedulerFactoryBean;
}

/** 创建schedule **/
@Bean(name = "scheduler")
public Scheduler scheduler() {
return schedulerFactoryBean().getScheduler();
}
}

防止Spring依赖注入为null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demoinit.quartz.dynamic;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;


@Component("MyAdaptableJobFactory")
public class MyAdaptableJobFactory extends AdaptableJobFactory {

@Autowired
private AutowireCapableBeanFactory capableBeanFactory;

@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
// 进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}

Java

没有任何框架。

依赖

.quartz依赖

.quartz-jobs

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz-jobs -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.2</version>
</dependency>
配置

使用quartz接口Job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// job实现类
public class MyJob implements Job {

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 获取参数
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();

// 业务逻辑 ...
log.info(jobDataMap.get("name").toString()+","+jobExecutionContext.getTrigger());

log.info("==================开始执行任务==================");
}
}
配置调度和触发器
1. 可以写到main方法中执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public static void main(String[] args) throws SchedulerException, InterruptedException {
// 创建调度器实例
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();

// 创建JobDetail实例,并与MyJob这个Job实现类绑定
JobDetail job = JobBuilder.newJob(MyJob.class).withIdentity("job1", "group1").build();

// SimpleTrigger,从当前时间的下 1 秒开始,每隔 1 秒执行 1 次,重复执行 2 次
/* Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
// .startNow() 默认立即执行
.startAt(DateBuilder.evenSecondDate(new Date())) // 从当前时间的下 1 秒开始执行
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1) // 每隔 1 秒执行 1 次
.repeatForever() // 一直执行
.withRepeatCount(2) // 重复执行 2 次,一共执行 3 次
)
.build();*/

// corn 表达式,先立即执行 1 次,然后每隔 5 秒执行 1 次

// HolidayCalendar cal = new HolidayCalendar();
// cal.addExcludedDate( someDate );
// cal.addExcludedDate( someOtherDate );
//
// sched.addCalendar("myHolidays", cal, false);

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
// .modifiedByCalendar("myHolidays") // but not on holidays
.build();

// 初始化参数传递到 job
job.getJobDataMap().put("myDescription", "Hello Quartz");
job.getJobDataMap().put("myValue", 1990);
List<String> list = new ArrayList<>();
list.add("firstItem");
job.getJobDataMap().put("myArray", list);


// 把作业和触发器注册到任务调度中
scheduler.scheduleJob(job, trigger);

// 执行计划程序
scheduler.start();

// 等待 10 秒,使我们的 job 有机会执行
Thread.sleep(10000);

// 等待作业执行完成时才关闭调度器
scheduler.shutdown(true);

//-------------------Job监听器(Trigger监听器同理)-------------------

// 注册对特定作业的监听器
// scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(new JobKey("job1", "group1")));
/*
// 注册对一个特定组的所有作业的 JobListener
scheduler.getListenerManager().addJobListener(new MyJobListener(), GroupMatcher.jobGroupEquals("group1"));

// 注册对两个特定组的所有作业的 JobListener
scheduler.getListenerManager().addJobListener(new MyJobListener(), OrMatcher.or(GroupMatcher.jobGroupEquals("group1"), GroupMatcher.jobGroupEquals("group2")));

// 注册对所有作业的 JobListener:
scheduler.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs());
*/

//-------------------调度器监听器(Trigger监听器同理)-------------------
// 注册对添加调度器时的 SchedulerListener:
// scheduler.getListenerManager().addSchedulerListener(mySchedListener);

// 注册对删除调度器时的 SchedulerListener:
// scheduler.getListenerManager().removeSchedulerListener(mySchedListener);
}
2. 也可以写implements CommandLineRunner的方法中,会项目启动后就自动运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demoinit.quartz.dynamic;

import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
* 实现项目启动后自运行
*/
@Component
public class JobSchedule implements CommandLineRunner {

@Override
public void run(String... strings) throws Exception {
System.out.println("任务调度开始==============任务调度开始");
// 调度逻辑
System.out.println("任务调度结束==============任务调度结束");
}
}

参考

Quartz框架

https://www.jianshu.com/p/2a5d3b6336ba

Quartz集群实战与原理分析

https://blog.51cto.com/simplelife/2314620

https://segmentfault.com/a/1190000009128277#item-1

https://blog.csdn.net/noaman_wgs/article/details/80984873

Xml https://zhuanlan.zhihu.com/p/46245863

简单配置属性手册 https://www.jianshu.com/p/0bf5d791d3c9

springboot 整合 quartz 实现定时任务的动态修改,启动,暂停等操作 https://hacpai.com/article/1548229459644

Springboot 2.x 整合 quartz 定时任务 实现动态添加、暂停、删除等功能 https://zzzmh.cn/single?id=83

springboot 博客 http://tigerdu.com/2018/01/16/springboot-quartz/

-------------Keep It Simple Stupid-------------
0%