Fork me on GitHub

第16章任务调度和异步执行器

第16章任务调度和异步执行器

任务调度概述

Quartz快速进阶

Quartz 基础结构

  • [ ] Job:开发者通过实现该接口来定义需要执行的的任务。Job 运行时的信息都保存在 JobDataMap 实例中。
  • [ ] JobDetail:描述 Job 的实现类及其他相关的静态信息,如 Job 名称、描述、关联监听器等信息。
  • [ ] Trigger:是一个类,描述触发 Job 执行的时间触发规则。 主要有 SimpleTrigger 和 CronTrigger 这两个子类。当仅需要触发一次或者以固定间隔周期性执行时,SimpleTrigger 是最合适的选择;而 CronTrigger 则可以通过表达式定义出各种复杂的调度方案,如每天早晨 9:00 执行,每周一,周三,周五下午 5:00 执行等。
  • [ ] Calendar:是一些日历特定时间点的集合
  • [ ] Scheduler:代表一个 Quartz 的独立运行容器。
  • [ ] ThreadPool:Scheduler 使用一个线程池作为任务运行的基础设施,任务通过共享线程池的线程来提高运行效率。

如下图,描述了 Scheduler 的内部组件结构。SchedulerContext 提供了 Scheduler 全局可见的上下文信息,每个任务都对应一个 JobDataMap ,虚线框中的 JobDataMap 表示有状态的任务。

一个 Scheduler 可以拥有多个 Trigger 和多个 JobDetail ,它们可以分到不同的组中。在注册 Trigger 和 JobDetail 时,如果不显示指定所属的组,那么 Scheduler 将放到默认的组中,默认的组名为 Scheduler.DEFAULT_GROUP。组名和名称组成了对象的全名,同一类型对象(Job 或 Trigger)的全名不能相同。

Scheduler 本身就是一个容器,它维护者 Quartz 的各种组件并实施调度的规则。Scheduler 还拥有一个线程池,线程池为任务提供执行线程。这比执行任务时简单的创建一个新的线程要拥有更高的效率,同时通过共享机制可以较少资源的占用。基于线程池组件的支持,对于繁忙度高、压力大的任务调度,Quartz 可以提供良好的伸缩性。

使用Simple Trigger

SImpleTrigger 拥有多个重载的构造函数,用于在不同场合下构造对应的实例。

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
public class SimpleTrigger extends Trigger {
...
/*指定 Trigger 所属组和名称*/
public SimpleTrigger(String name, String group) {
this(name, group, new Date(), (Date)null, 0, 0L);
}
/*指定 Trigger 所属组、名称和触发时间*/
public SimpleTrigger(String name, Date startTime) {
this(name, (String)null, startTime);
}
/*指定 Trigger 所属组、名称、开始时间、结束时间、重复次数、时间间隔*/
public SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval) {
super(name, group);
this.startTime = null;
this.endTime = null;
this.nextFireTime = null;
this.previousFireTime = null;
this.repeatCount = 0;
this.repeatInterval = 0L;
this.timesTriggered = 0;
this.complete = false;
this.setStartTime(startTime);
this.setEndTime(endTime);
this.setRepeatCount(repeatCount);
this.setRepeatInterval(repeatInterval);
}
/*这是最复杂的一个构造函数,在指定触发参数的同时,通过 jobGroup 和 jobName ,使该 Trigger 和 Scheduler 中的某个任务关联起来*/
public SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval) {
super(name, group, jobName, jobGroup);
this.startTime = null;
this.endTime = null;
this.nextFireTime = null;
this.previousFireTime = null;
this.repeatCount = 0;
this.repeatInterval = 0L;
this.timesTriggered = 0;
this.complete = false;
this.setStartTime(startTime);
this.setEndTime(endTime);
this.setRepeatCount(repeatCount);
this.setRepeatInterval(repeatInterval);
}
...
}

通过实现 org.quartz.Job 接口,可以是 Java 类变成可调度的任务,如下内容:

1
2
3
4
5
6
public class SimpleJob implements Job {
@Override
public void execute(JobExecutionContext jobCtx) throws JobExecutionException {
System.out.println(jobCtx.getTrigger().getName()+"triggered. time is:"+new Date());
}
}

通过 SimpleTrigger 对 SimpleJob 进行调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SimpleTriggerRunner {
public static void main(String args[]){
try {
/*创建一个 JobDetail 实例,指定 SimpleJob*/
JobDetail jobDetail = new JobDetail("job1_1","jdroup1", SimpleJob.class);
/*通过 SimpleTrigger 定义调度规则:马上启动,每2秒运行一次,共运行100次*/
SimpleTrigger simpleTrigger = new SimpleTrigger("Trigger1_1","tgroup1");
simpleTrigger.setStartTime(new Date());
simpleTrigger.setRepeatInterval(2000);
simpleTrigger.setRepeatCount(100);
/*通过 SchedulerFactory 获取一个调度器实例*/
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
/*注册并进行调度*/
scheduler.scheduleJob(jobDetail,simpleTrigger);
scheduler.start(); /*调度启动*/
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}

还可以通过 SimpleTrigger 的 setStartTime(Date startTime) 和 setEndTime(Date endTime) 的方法指定运行的时间范围。当运行次数和时间范围产生冲突时,超过时间范围的任务不被执行。如可以通过 simpleTrigger.setStartTime(new Date(System.CurrentTimeMillis()+60000L))方法指定 60 秒后开始运行。

执行结果:

使用 CronTrigger

CronTrigger 能够提供比 SimpleTrigger 更有具体实际意义的调度方案,调度规则基于 Cron 表达式。CronTrigger 支持日历相关的周期时间间隔(比如每月第一个周一执行),而不是简单的周期时间间隔。

Cron 表达式

Quartz 使用类似于 Linux 下的 Cron 表达式定义时间规则。Cron 表达式由 6 或 7 个空格的时间字段组成,如下表:

Cron 表达式时间字段:

位置 时间域名 允许值 允许的特殊字符
1 0-59 ,-*/
2 分钟 0-59 ,-*/
3 小时 0-23 ,-*/
4 日期 1-31 ,-*?/L W C
5 月份 1-12 ,-*/
6 星期 1-7 ,-*?/L C #
7 年(可选) 空值1970-2099 ,-*/

Cron 表达式的时间字段除允许设置数值外,该可以使用一些特殊的字符,提供列表、范围、通配符等功能,如下:

  • 星号(*):可用在所有字段中,表示对应时间域的某一时刻。
  • 【例如:*在分钟字段时,表示“每分钟”。】
  • 问好(?):该字符只在日期和星期字段中使用,它通常指定为“毫无意义的值”,相当于占位符
  • 减号(-):表示一个范围。【例如:在小时字段中使用”10-12”,则表示从10点到12点,即10,11,12】
  • 逗号(,):表示一个列表值。【例如:在星期字段中使用”MON,WED,FRI”,则表示星期一、星期三和星期】
  • 斜杠(/):x/y 表达一个等长序列,x 为起始值,y为增量步长值。如在分钟字段中使用 0/15 ,则表示为 0,15,30 和 45 秒;而 5/15 在分钟字段中表示 5,20,35,50 。用户可以使用 */y,它等同于 0/y。
  • L:在日期和星期字段中使用,在日期中表示这个月的最后一天,在星期中使用表示这星期的周六。但是,如果L出现在星期字段里,而且前面有一个数字N,则表示“这个月的最后N天”。【例如:6L 表示该约的最后一个星期五】
  • W:该字符只出现在日期字段里,是对前导日期的修饰,表示该日期最近的工作日。W字符串只能指定单一日期,而不能指定日期范围。
  • LW组合:在日期字段中使用,当月的最后一个工作日。
  • 井号(#):该字符只能在星期字段中使用,表示当月的某个工作日。【例如:6#3表示当月的第3个星期五(6表示星期五,#3 表示当前的第三个),而 4#5 表示当月的第五个星期三。假如当月没有第五个星期三,则忽略不触发。】
  • C:只在日期和星期字段中使用,代表”Calendar”的意思。它是指计划所关联的日期,如果日期没有被关联到,则相当于日历中的所有日期。【例如:5C 在日期字段中相当于5日以后的那一天,1C在星期字段中相当于星期日后的第一天】

Cron 表达式对特殊字符的大小写不敏感,对代表星期的缩写也不敏感。

Cron 表示式示例:

表示式 说明
“0 0 12 ? “ 每天 12:00 运行
“0 15 10 ? 每天 10:15 运行
“0 15 10 ?” 每天 10:15 运行
“0 15 10 ? *” 每天 10:15 运行
“0 15 10 ? 2008” 2008 年的每天 10:15 运行
“0 14 * ?” 每天14点到15点每分钟运行一次。开始于14:00 ,结束于14:59
“0 0/15 14 ?” 每天14点到15点每5分钟运行一次,开始于 14:00 ,结束于14:59
“0 0/5 14,18 ?” 每天14点到15点每5分钟运行一次,此外每天 18点到19点每5分钟也运行一次
“0 0-15 14 ?” 每天14:00 到 14:05,每分钟运行一次
“0 10,44 14 ? 3 WED” 3月每周三的 14:10 到 14:44 ,每分钟运行一次
“0 15 10 ? * MON-FRI” 每周一、二、三、四、五的 10:15 运行
“0 15 10 15 * ?” 每月15 日的 10:15 运行
“0 15 10 L * ?” 每月最后一天星期五的 10:15 运行
“0 15 10 ? * 6L” 每月最后一个星期五的 10:15 运行
“0 15 10 ? 6L 2014-2016” 2014年、2015年、2016年每月最后一个星期五的 10:15 运行
“0 15 10 ? * 6#3” 每月第三个星期五的 10:15 运行

CronTrigger 实例

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
public class CronTriggerRunner {
public static void main(String args[]){
try {
JobDetail jobDetail = new JobDetail("job_2","jGroup1", SimpleJob.class);
/*创建 CronTrigger,指定组及名称*/
CronTrigger cronTrigger = new CronTrigger("trigger1_2","tgroup1");
/*定义 Cron 表达式*/
CronExpression cexp = new CronExpression("0/5 * * * * ?");
/*设置 Cron 表达式*/
cronTrigger.setCronExpression(cexp);
/*②*/
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail,cronTrigger);
scheduler.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行 CronTriggerRunner,每5秒将触发 SimpleJob 运行一次。在默认情况下,Cron 表达式对应当前的时区,可以通过 CronTriggerRunner 的 setTimeZone(TimeZone timeZone) 方法显示指定时区。也可以指定开始时间和结束时间。

注意:在代码 ② 处需要通过 Thread.currentThread.sleep() 方法让主线程睡眠一段时间,使调度器可以继续执行任务调度的工作;否则在调度器启动后,因为主线程立即退出,寄生于主线程的调度器也将关闭,调度器的任务都将相应的销毁,这将导致看不到实际的运行效果。在单元测试的时候,使主线程休眠一段时间以便让任务线程不被提前终止是经常使用的测试方法。对于测试某些长周期执行的调度任务,开发者可以简单地调整操作系统时间进行模拟。

运行结果:

使用 Calender

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
import com.mrsw.adx.admin.service.impl.SimpleJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.calendar.AnnualCalendar;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class CalendarExample {
public static void main(String[] args){
try {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
/*法定节日是以每年为周期的,所以使用 AnnualCalendar*/
AnnualCalendar holidays = new AnnualCalendar();
/*五一劳动节*/
Calendar laborDay = new GregorianCalendar();
laborDay.add(Calendar.MONTH,5);
laborDay.add(Calendar.DATE,1);
/*国庆节*/
Calendar nationalDay = new GregorianCalendar();
nationalDay.add(Calendar.MONTH,10);
nationalDay.add(Calendar.DATE,1);
/*排除这两个特殊日期*/
ArrayList<Calendar> calendars = new ArrayList<Calendar>();
calendars.add(laborDay);
calendars.add(nationalDay);
holidays.setDaysExcluded(calendars);/*①*/
/*向 Scheduler 注册日历*/
scheduler.addCalendar("holidays",holidays,false,false);
/*4月1日上午10点*/
Date runDate = TriggerUtils.getDateOf(0,0,10,1,4);
JobDetail job = new JobDetail("job1","group1", SimpleJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1","group1",runDate,
null,SimpleTrigger.REPEAT_INDEFINITELY,
60L * 60L * 1000L);
trigger.setCalendarName("holidays");
/*让 Trigger 应用指定的日历规则*/
scheduler.scheduleJob(job,trigger);
scheduler.start();
// 在实际应用中主线程不能停止,否则 Scheduler 得不到执行,此处省略
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}

注意:在向 Scheduler 注册日历的时候,addCalendar(String calName,Calendar calendar,boolean replace,boolean updateTrigger)。如果 updateTrigger 为 true,则 Scheduler 中引用 Calendar 的 Trigger 将得到更新,如①所示。

任务调度信息存储

默认情况下,Quartz 将任务调度的运行信息(调度现场信息包括运行次数、调度规则和JobDataMap 中的数据等。)保存在内存中。这种方法提供了最佳的性能,因为在内存中数据访问速度最快;不足之处在于缺乏数据的持久性,当程序中途停止或系统崩溃时,所有运行的信息都会丢失。

持久化任务调度信息,可以通过调整 Quartz 的属性文件,将这些数据保存到数据库。

通过配置文件调整任务调度信息的保存策略

在 Quartz JAR 文件的 org.quartz 包下就包含了一个 quartz.properties 属性配置文件,并提供了默认配置。如果需要调整默认配置,则可以直接在类路径下建立一个新的 quartz.properties 属性文件,它将被 Quartz加载并覆盖默认的配置。

Quartz 的属性文件配置主要包括以下三方面的信息:

  1. 集群信息
  2. 调度器线程池
  3. 任务调度现场数据的保存

注意:如果任务数目很大,则可以通过增大线程池获得更好的性能。

可以通过 以下设置将任务调度现场数据保存到数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 要将任务调度保存到数据库,必须使用 JobStoreTX 代替原来的 RAMJobStore
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# 数据库表前缀
org.quartz.jobStore.tablePrefix = QRTZ_
# 数据源名称
org.quartz.jobStore.dataSource = qzDS
# 定义数据源的具体属性
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/sampledb
org.quartz.dataSource.qzDS.user = stamen
org.quartz.dataSource.qzDS.password = abd
org.quartz.dataSource.qzDS.maxConnections = 10

注意:必须事先在相应的数据库中创建 Quartz 的数据表(8张),在 Quartz 的完整发布的 dosc/dbTables 目录下拥有对应的不同的数据库脚本。

查询数据库的运行信息

在Spring中使用Quartz

Spring 为创建 Quartz 的 Scheduler 、Trigger 和 JobDetail 提供了便利的 FactoryBean 类,以便能够在 Spring 容器中享受注入的好处。

Spring 为 Quartz 提供了两个方面的支持:

(1)为 Quartz 的重要组件提供更具 Bean 风格的扩展类

(2)提供创建 Scheduler 的 BeanFactory 类,方便在 Spring 环境下创建对应的组件对象,并结合 Spring 容器生命周期执行启动和停止的动作。

创建 JobDetail

Spring 通过扩展 JobDetail 提供了一个更具 Bean 风格的 JobDetailFactoryBean。还提供了一个 MethodInvokingJobDetailBean,通过这个 FactoryBean 可以将 Spring 容器中 Bean 的方法包装成 Quartz 任务,这样开发者就不必为 Job 创建对应的类。

JobDetailFactoryBean

扩展于 Quartz 的 JobDetail 。使用该 Bean 声明 JobDetail 时,bean 的名字即任务的名字,如果没有指定所属组,就使用默认组。除了 JobDetail 的属性外,还定义了以下属性:

  • jobCalss:类型为 Class,实现 Job 接口的任务类
  • beanName:默认为 Bean 的 id 名,通过该属性显示指定 Bean 名称,它对应任务的名称。
  • jobDataAsMap:类型为 Map ,为任务所对应的 JobDataMap 提供值。
  • applicationContextJobDataKey:可以将 Spring ApplicationContext 的引用保存到 JobDataMap 中,以便在 Job 的代码中访问 ApplicationContext。需要指定一个健用于在 jobDataAsMap 中保存 ApplicationContext。
  • jobListenerName:类型为 String[] ,指定注册在 Scheduler 中的 JobDataMap 名称,以便让这些监听器对本任务的事件进行监听。

在下面的配置片段中使用 JobDetailBean 在 Spring 中配置一个 JobDetail

1
2
3
4
5
6
7
8
9
<bean name="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean"
p:jobClass="com.smart.quartz.MyJob"
p:applicationContextJobDataKey="applicationContext">
<property>
<map>
<entry key=“size” value="10"/>
</map>
</property>
</bean>

说明:JobDetailFactoryBean 封装了 MyJob 任务,并为 Job 对应的 JobDataMap 设置了一个健为 size 的数据。此外,通过指定 applicationContextJobDataKey ,让 Job 的 JobDataMap 持有 Spring ApplicationContext 的引用。

这样,MyJob 在运行时就可以通过 JobDataMap 访问到 size 和 ApplicationContext。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jctx) throws JobExecutionException {
/*获取 JobDetail 关联的 JobDataMap*/
Map dataMap = jctx.getJobDetail().getJobDataMap();
String size = (String) dataMap.get("size");
ApplicationContext ctx = (ApplicationContext)dataMap.get("applicationContext");
System.out.println("size:"+size);
/*① 对 JobDataMap 所做的更改是否被持久化取决于任务的类型*/
dataMap.put("size",size+"0");
}
}

在代码 ① 处对 JobDataMap 进行修改。如果 MyJob 实现了 Job 接口,则这种更改对于下一次执行是不可见的;如果 MyJob 实现了 StatefulJob 接口,则这种更改对于下一次执行是可见的。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyJob implements StatefulJob {
public void execute(JobExecutionContext jctx) throws JobExecutionException {
// Map dataMap = jctx.getJobDetail().getJobDataMap();
Map dataMap = jctx.getTrigger().getJobDataMap();
String size =(String)dataMap.get("size");
ApplicationContext ctx = (ApplicationContext)dataMap.get("applicationContext");
System.out.println("size:"+size);
dataMap.put("size",size+"0");
String count =(String)dataMap.get("count");
System.out.println("count:"+count);
}
}

MethodInvokingJobDetailFactryBean

1
2
3
4
5
<!-- 通过封装服务类方法实现 -->
<bean id="jobDetail_1" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
p:targetObject-ref="myService" p:targetMethod="doJob"
p:concurrent="false"/>
<bean id="myService" class="com.smart.service.MyService" />

jobDetail_1 将 MyService#doJob() 封装成一个任务,同时通过 concurrent 属性指定任务的类型。默认情况下为无状态的任务。如果希望封装为有状态的任务,仅需将 concurrent 属性设置为 false 就可以了。Sping 通过名为 concurrent 的属性指定任务类型,能够更直接的描述任务执行的方式(有状态的任务不能并发执行,无状态的任务可以并发执行),对于不熟悉 Quartz 内部机制的用户来说,比起 stateful ,concurrent 更简明达意。

1
2
3
4
5
6
public class MyService {
/*被封装成任务的目标方法*/
public void doJob(){
System.out.println("in MyService.dojob().");
}
}

注意:通过 MethodInvokingJobDetailFactoryBean 产生的 JobDetail 不能被序列化,所以不能持久化到数据库。若希望使用持久化任务,则只能创建正规的 Quartz 的 Job 实现类 。

创建 Trigger

SimpleTriggerFactoryBean

在默认情况下,通过 SimpleTriggerFactoryBean 配置的 Trigger 名称即为 Bean 的名称,属于默认组。SimpleTriggerFactoryBean 在 SimpleTrigger 的基础上新增了以下属性。

jobDetail:对应的 JobDetail。

beanName:默认为 Bean 的 id 名,通过该属性显示指定 Bean 名称,它对应 Trigger 的名称。

jobDataAsMap:以 Map 类型为 Trigger 关联的 JobDataMap 提供值。

startDelay:延迟多少时间开始触发,单位为毫秒,默认值为0

triggerListenerNames:类型为 String[],指定注册在 Scheduler 中的 TriggerListener 名称,以便让这些监听器对本触发器的时间进行监听。

1
2
3
4
5
6
7
8
9
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"
p:jobDetail-ref="jobDetail" p:startDelay="1000" p:repeatInterval="2000"
p:repeatCount="100">
<property name="jobDataAsMap"><!--①-->
<map>
<entry key="count" value="10" />
</map>
</property>
</bean>

代码①处配置的 Map 数据将填充到 Trigger 的 JobDataMap 中,执行任务时必须通过以下方式获取配置的值:

1
2
3
4
5
6
7
8
public class MyJob implements StatefulJob {
public void execute(JobExecutionContext jctx) throws JobExecutionException {
Map dataMap = jctx.getTrigger().getJobDataMap();
/*对 JobDataMap 的更改不会被持久化,不影响下次的执行*/
String count =(String)dataMap.get("count");
System.out.println("count:"+count);
}
}

CronTriggerFactoryBean

扩展于 CronTrigger ,触发器的名称即为 Bean 的名称,保存在默认组中。在 CronTrigger 的基础上,新增的属性和 SimpleTriggerFactoryBean 大致相同,配置的方法也和 SimpleTriggerFactoryBean 相同。

1
2
3
4
<bean id="checkImagesTrigger"
class="org.springframework.scheduling.quartz.CronTriggerBean"
p:jobDetail-ref="jobDetail"
p:cronExpression="0/5 * * * * ?"/>

创建Scheduler

Quartz 的 SchedulerFactory 是标准的工厂类,不太合适在 Spring 环境下使用。此外,为了保证 Scheduler 能够感知 Spring 容器的生命周期,在 Spring 容器启动后,Scheduler 自动开始工作,而在 Spring 容器关闭之前,自动关闭 Scheduler 。Spring 提供了 SchedulerFactoryBean,这个 FactoryBean 大致拥有以下功能。

  • 以更具 Bean 风格的方式为 FactoryBean 提供配置信息。
  • 让 Scheduler 和 Spring 容器的生命周期建立关联,相生相息
  • 通过属性配置的方式代替 Quartz 自身的配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactory">
<!--注册多个 trigger-->
<property name="triggers">
<list>
<ref bean="simpleTrigger"/>
</list>
</property>
<!--以Map类型设置 SchedulerContext 数据-->
<property name="schedulerContextAsMap">
<map>
<entry key="timeout" value="30"/>
</map>
</property>
<!--显示指定 quartz 的配置文件地址-->
<properties name="configLocation" value="classpath:com/smart/quartz/quartz.properties"/>
</bean>

SchedulerFactoryBean 还有以下常见的属性:

  • calendars:类型为 Map,通过该属性向 Scheduler 注册 Calendar
  • jobDetails:类型为 JobDetail[],通过该属性向 Scheduler 注册 JobDetail
  • autoStartup:SchedulerFactoryBean 在初始化后是否马上启动 Scheduler,默认为 true。若设置为 false,则需要手动启动 Scheduler
  • startupDelay:在 SchedulerFactoryBean 在初始化完成后,延迟多少秒后启动 Scheduler,默认为0。除非拥有需要立即执行的任务,一般情况下,可以通过 startupDelay 属性让 Scheduler 延迟一小段时间后启动,以便让 Spring 能够更快初始化容器中剩余的 Bean

SchedulerFactoryBean 的一个重要功能是允许用户将 Quartz 配置文件的信息转移到 Spring 配置文件中。SchedulerFactoryBean 通过以下属性代替框架的自身配置文件:

  • dataSource:当需要持久化任务调度数据时,在 Quartz 中配置数据源,也可以直接在 Spring 中通过 dataSource 指定一个 Spring 管理的数据源。如果指定了该属性,即使 quartz.properties 中已经定义了数据源,也会被 dataSource 覆盖
  • transactionManager:可以通过该属性设置一个 Spring 事务管理器
  • nonTransactionalDataSource:在全局事务的情况下,如果不希望 Scheduler 执行的相关数据操作参与到全局事务中,则可以通过该属性指定数据源。在 Spring 本地事务的情况下,使用 dataSource 属性就足够了
  • quartzProperties:类型为 properties ,允许用户在 Spring 中定义 Quartz 的属性,其值将覆盖 quartz.properties 配置文件中的设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="scheduler"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="quartzProperties">
<props>
<!--属性值1-->
<prop key="org.quartz.threadPool.class">
org.quartz.simpl.SimpleThreadPool
</prop>
<!--属性值2-->
<prop key="org.quartz.threadPool.threadCount">10</prop>
</props>
</property>
</bean>

在Spring中使用JDK Timer

Timer 和 TimerTask

TimerTask 代表一个需要多次执行的任务,它实现了 Runnable 接口,可以在 run() 方法定义任务逻辑。而 Timer 负责制定调度规则并调度 TimerTask。

TimerTask

相当于 Quartz 的 Job ,代表一个被调度的任务。二者区别在于,每次执行任务时,Quartz 都创建一个 Job 实例,而 JDK Timer 则使用相同的 TimerTask 实例。

实现了 Runnable 接口,是一个抽象类,只有以下3个方法:

  1. abstract void run():子类覆盖这个方法并定义任务执行逻辑,每次执行任务时,run() 方法就被调用一次。
  2. boolean cancel():取消任务。
  3. long scheduledExecutionTime():返回词此任务的计划执行时间。该方法一般在固定频率执行时使用才会有意义。

Timer

Timer 的构造函数在创建 Timer 对象的同时将启动一个 Time 背景线程。构造函数如下:

  • Timer():创建一个Timer,背景线程是一个非守护线程
  • Timer(boolean isDaemon):当 isDaemon 为 true,背景线程为守护线程,守护线程将在应用程序主线程停止后自动退出。
  • Timer(String name):与 Timer() 类似,只是通过 name 指定守护线程名称。

通过以下方法执行任务:

  • schedule(TimerTask task,Date time):在特定的时间点执行一次任务。
  • schedule(TimerTask task,long delay):延迟指定时间后执行一次任务,delay的单位为毫秒

通过以下方按固定时间间隔执行任务:

  • schedule(TimerTask task,Date firstTime,long period):从指定时间开始周期性地执行任务,period 为毫秒,后一次执行将在前一次执行完成后才开始计时。如任务被安排每 2 秒执行一次,假设第一次任务在 0 秒时间点开始执行并花费了 1.5 秒,则第二次将在第 3.5 秒时执行。
  • schedule(TimerTask task,long delay,long period):在延迟指定时间后,周期性地执行任务

通过以下方法按照固定频率执行任务:

  • scheduleAtFixedRate(TimerTask task,Date firstTime,long period):在指定时间点后,以指定频率执行任务。

Java Timer 实例

Spring 对 Java Timer 的支持

Spring 对 Java 5.0 Executor 的支持

了解 Java 5.0 的 Executor

Spring 对 Executor 所提供的抽象

实际应用中的调度

对于那些运行规则固定的静态任务(如每隔30分钟更新缓存),可通过 Spring 配置文件定义调度规则并在 Spring 容器中启动运行调度。若任务的执行时间非常重要,不允许发生时间漂移,那么 Quartz 是最好的选择。

如何产生任务

在业务流程中产生

如果任务的执行时间点离业务的操作时间点不是很长,则可以使用。例如:电力传输管理系统的功能,将一条传输线路在某段时间内停止供电。用户在执行线路停电安排的业务时,立即向 Scheduler 中注册两个任务:某段时间执行断电和执行恢复供电的两个任务。

扫描线程产生

有严格的执行时间点并减小数据库的影响,需要一个用于产生最近执行任务的扫描任务定期查询数据库,并为那些在一小段时间后就要执行的潜在任务进行动态安排。

说明:T0 对应一个定时的任务,它负责周期性地扫描业务表,查找在后续的扫描周期时间范围内要执行的任务,并创建这些任务。这中方式带来的好处如下:

  1. 降低对数据库的影响
  2. 缩短调度器中任务列队的长度(由于不是将所有潜在任务提前一段很长时间就进行安排,而仅是对一个扫描周期内的任务进行安排,所以调度器中任务列表的长度可以得到有效的控制)
  3. 保证任务在精确的时间点执行

任务调度对应程序集群的影响

对于有集群要求的 Web 应用来说,如果应用系统本身有任务调度的功能,就必须在系统设计初期仔细分析任务调度功能是否适合集群。按任务执行结果影响的范围,可以将任务分为如下两类:

  • 全局任务:指定那些执行结果会影响到应用系统全局的任务。例如:每天凌晨生成业务报表、定期调用短信接口发送短信、定期清理系统过期数据,它们的执行结果都会给系统带来“全局可见”的结果。所以,在传统的集群系统中,全局任务最好在一个独立部署的服务节点执行,否则可能会因重复多次执行而引发系统逻辑的错误。

  • 本地任务:指执行结果的影响范围仅限于本地,不会造成全局影响的任务。例如:定期刷新本地缓存、定期清除本地节点临时文件,它们的执行结果只对本地服务节点有影响,需要在每个本地服务节点部署任务。

    Quartz 可支持集群部署,其原理很简单,即让多个调度节点虎威热备,在同一时刻只有一个节点是激活的,任务只有在激活的节点中执行,其他节点都是“休眠”状态;当激活的调度节点崩溃时,则唤醒某一个“休眠”的调度节点,以接管任务调度的工作。

    Quartz 可通过两种方式实现集群:1.通过一个中间数据库,使集群节点相互感知,以实现故障切换;2.通过 Terracotta。

任务调度云

Web应用程序中调度器的启动和关闭问题

我们知道,静态变量是 ClassLoad 级别的,如果 Web 应用程序停止,那么这些静态变量也会从 JVM 中清除。但线程是 JVM 级别的,如果用户在 Web 应用中启动了一个线程,那么线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使停止了 Web 应用,这个线程依旧是活动的。

问题:

如果手工使用 JDK Timer (Quartz 的 Scheduler),在 Web 容器启动非守护线程的 Timer ,当 Web 容器关闭时,除非用户手动关闭这个 Timer ,否则 Timer 中的任务还会继续。

解决方法:

Spring 为 JDK Timer 和 Quartz Scheduler 所提供的 TimerFactoryBean 和 SchedulerFactoryBean 能够与 Spring 容器的生命周期关联,在 Spring 容器启动时启动调度器,而在 Spring 容器关闭时停止调度器。所以在 Spring 中通过配置两个 FactoryBean 配置调度器,再从 Spring IOC 中获取调度器的引用进行任务调度,这样就不会出现这种 Web 容器关闭而任务依然执行的问题。

小结

Quartz 提供了极为丰富的任务调度功能,不但可以制定周期性执行的任务调度方案,还可以让用户按照日历相关的方式进行任务调度。

Quartz 框架的重要组件包括 Job、JobDetail、Trigger、Scheduler 及辅助性的 JobDataMap 和 SchedulerContext。

Quartz 拥有一个线程池,通过线程池为任务提供执行线程,可以通过配置配置文件对线程池进行参数定制。

Quartz 还有一个重要功能,将任务调度信息持久化到数据库中,以便系统重启时能够恢复已经安排的任务。

Quartz 还拥有完善的事件体系,允许用户注册各种事件的监听器。


Spring 为 Quartz 的 JobDetail 和 Trigger 提供了更具 Bean 风格的支持类,使得用户能够方便地在 Spring 中通过配置定制这些组件的实例。

Spring 的 SchedulerFactoryBean 让用户可以脱离 Quartz 自身的体系,而以更具 Spring 风格的方式定义 Scheduler。Scheduler 生命周期和 Spring 容器生命周期绑定。


JDK Timer 可以满足一些简单的任务调度需求,好处就是用户不必引用 JDK 之外的第三方类库;只能支持小型的任务且任务很快就能完成。

JDK Timer 只能做到近似时间安排。

坚持原创技术分享,您的支持将鼓励我继续创作!