1.任务调度概述
各种企业应用都会遇到任务调度的需求,比如每天凌晨统计论坛用户的积分排名等等,在特定的时间做特定的事情。如果将任务调度的范围稍微扩大一点,则还应该包括资源上的调度。如Web Server在接收到请求时,会立即创建一个新的线程服务该请求。但是资源是有限的,无限制的使用必然会耗尽亏空,大多数系统都要对资源使用进行控制。首先必须限制服务线程的最大数目;其次可以考虑使用线程池以便共享服务的线程资源,降低频繁创建、销毁线程的消耗。
任务调度本身设计多线程并发,运行时间规则制定及解析、运行线程保持与恢复、线程池维护等诸多方面的工作。如果直接使用自定义线程这种最原始的办法,则开发任务调度程序是一项颇具挑战性的工作。
2.Quartz快速进阶
Quartz允许开发人员灵活地定义触发器的调度时间表,并可对触发器和任务进行关联映射。此外,Quartz提供了调度运行环境的持久化机制,可以保存并恢复调度现场,即使系统因故障关闭,任务调度现场数据也不会丢失。
2.1.Quartz基础结构
Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个核心概念,并且在org.quartz中通过接口和类对核心概念进行了描述
- Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者通
过实现该接口来定义需要执行的任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息都保存在JobDataMap实例中 - JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不是直接接收一个Job实例,而是接收一个Job实现类,以便运行时通过newInstance()的反射调用机制来实例化Job。因此需要通过一个类来描述Job的实现类及其他相关的静态信息,如Job名称、描述、关联监听器等信息,而JobDetail承担了这一角色
- Triger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个类。当仅需要触发一次后者以固定间隔周期性执行时,SimpleTigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂的调度方法,
- Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合
- Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,二者在Scheduler中拥有各自的组及名称。组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称的组合必须唯一,JobDetail的组合名称的组合也必须唯一。
- ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程来提高运行效率
2.2.使用SimpleTrigger
SimpleTrigger拥有多个重载的构造函数,用于在不同场合下构造出对应的实例
- SimpleTrigger(String name,String group):通过该构造函数指定Trigger所属组合名称
- SimpleTrigger(String name,String group,Date startTime):除指定Trigger所属组和名
称外,还可以指定触发的开始时间 - SimpleTrigger(String name,String group,Date startTime,Date endTime,int repeatCount,long repeatInterval):除指定以上信息外,还可以指定结束时间、重复执行次数、时间间隔等参数
- SimpleTrigger(String name,String group,String jobName,String jobGroup,Date startTime,Date endTime,int repeatCount,long repeatInterval):这是最复杂的一个构造
函数,在指定触发参数的同时,通过jobGroup和jobName,使该Trigger和Schedule中的某个任务关联起来
public class SimpleJob implements Job {
public void execute(JobExecutionContext jobCtx) throws JobExecutionException {//实现Job接口方法
System.out.println(jobCtx.getTrigger().getName()+" triggered. time is:" + (new Date()));
}
}
以下是通过SimpleTrigger对SimpleJob进行调度
public class SimpleTriggerRunner {
public static void main(String args[]) {
try {
//创建一个JobDetail实例,指定SimpleJob
JobDetail jobDetail = new JobDetail("job1_1", "jgroup1",
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 (Exception e) {
e.printStackTrace();
}
}
}
2.3.使用CronTrigger
CronTrigger能够提供比SimpleTrigger更具有实际意义的调度方案,调度规则基于Cron表达式。CronTrigger支持日历相关的周期时间间隔(比如每月第一个周一执行),而不是简单的周期时间间隔。一次相对于SimpleTrigger而言,CronTrigger在使用上也要复杂一些。
2.3.1.Cron表达式
Quartz使用类似Linux下的Cron表达式定义时间规则。Cron表达式由6或7个空格分隔的时间间隔字段组成
位置 | 时间域名 | 允许值 | 允许的特殊字符 |
---|---|---|---|
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 | ,-*./ |
- 星号(*):可用在所有字段中,表示对应时间域的每一个时刻。例如,*在分钟字段时,表示“每分钟”
- 问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于占位符
- 减号(-):表达一个范围,如在小时字段中使用“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:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中的意思不同。如果L用在日期字段中,则表示这个月份的最后一天,用在星期中,则表示星期六。但是,如果L出现在星期字段里,而且前面有一个数字N,则表示“这个月的最后N天”。例如,6L表示该月的最后一个星期五
- W:该字符只能出现在在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如,15W表示离该月15日最近的工作日
- LW组合:在日期字段中可以组合使用LW,它的意思是当月的最后一个工作日
-
:该字符只能在星期字段中使用,表示当月的某个工作日,如4#5表示当月的第五个星期三。假设当月没有第五个星期三,则忽略不触发
- C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中的所有日期。例如,5C在日期字段中相当于5日以后的一天,1C在星期字段中相当于星期日后的第一天
2.3.2.CronTrigger实例
下面使用CronTrigger对SimpleJob进行调度,通过Cron表达式制定调度规则
public class CronTriggerRunner {
public static void main(String args[]) {
try {
JobDetail jobDetail = new JobDetail("job1_2", "jgroup1",
SimpleJob.class);
CronTrigger cronTrigger = new CronTrigger("trigger1_2", "tgroup1");
CronExpression cexp = new CronExpression("0/5 * * * * ?");
cronTrigger.setCronExpression(cexp);
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, cronTrigger);
scheduler.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.4.使用Calendar
在实际任务调度中,不可能一成不变地按照某个特定周期调度任务,必须考虑到实现生活中日历上的特殊日期
public class CalendarExample {
public static void main(String[] args) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
AnnualCalendar holidays = new AnnualCalendar();
//五一劳动节
Calendar laborDay = new GregorianCalendar();
laborDay.add(Calendar.MONTH,5);
laborDay.add(Calendar.DATE,1);
holidays.setDayExcluded(laborDay, true);
//国庆节
Calendar nationalDay = new GregorianCalendar();
nationalDay.add(Calendar.MONTH,10);
nationalDay.add(Calendar.DATE,1);
holidays.setDayExcluded(nationalDay, true);
scheduler.addCalendar("holidays", holidays, false, false);
//从5月1号10am开始
Date runDate = TriggerUtils.getDateOf(0,0, 10, 1, 5);
JobDetail job = new JobDetail("job1", "group1", SimpleJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1",
runDate,
null,
SimpleTrigger.REPEAT_INDEFINITELY,
60L * 60L * 1000L);
//让Trigger遵守节日的规则(排除节日)
trigger.setCalendarName("holidays");
scheduler.scheduleJob(job, trigger);
scheduler.start();
try {
// wait 30 seconds to show jobs
Thread.sleep(30L * 1000L);
// executing...
} catch (Exception e) {
}
scheduler.shutdown(true);
}
}
2.5.任务调度信息存储
在默认情况下,Quartz将任务调度的运行信息保存在内存中。这种方法提供了最佳性能,因为在内存中数据访问速度最快;不足之处是缺乏数据的持久性,当程序中途停止或者系统崩溃时,所有运行信息都会丢失。
如果确实需要持久化任务调度信息,则Quzrtz允许用户通过调整其属性文件,将这些信息保存到数据库中。在使用数据库保存了任务调度信息后,即使系统崩溃后重新启动,任务调度信息仍将得到恢复。如前面所说的例子,执行50次系统崩溃后重新运行,计数器将从51开始计数。使用数据库保存信息的任务称为持久化任务。
####### 2.5.1.通过配置文件调整任务调度信息的保存策略
其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件,并提供了默认的配置。如果需要调整配置,则可以在类路径下建立一个新的quartz.properties属性,它将自动被Quartz加载并覆盖默认的配置
//集群的配置,这里不使用集群
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
//配置调度器的线程池
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 = 60000
//配置任务调度现场保存机制
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
Quartz的属性配置文件主要包括三方面的信息:
- 集群信息
- 调度器线程池
- 任务调度现场数据的保存
可以通过下面的设置将任务调度现场的数据保存到数据库中
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_ //1、数据库表前缀
org.quartz.jobStore.dataSource = qzDS //2、数据源名称
//3、定义数据源的具体属性
org.quartz.dataSource.qzDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL = jdbc:mysql://localhost:3306/sampledb
org.quartz.dataSource.qzDS.user = root
org.quartz.dataSource.qzDS.password = 123456
org.quartz.dataSource.qzDS.maxConnections = 30
要将任务调度数据保存到数据库中,就必须使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原来的org.quartz.simpl.RAMJobStore,并提供相应的数据库配置信息。首先在1处指定了Quartz数据库表的前缀,然后在2处定义了一个数据源,然后在3处定义这个数据源的连接信息
用户必须事先在相应的数据库中建立Quartz的数据表(共8张),在Quartz的完整发布包的docs/dbTables目录下拥有对应不同数据库的SQL脚本
2.5.2.查询数据库中的运行信息
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.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 = root
org.quartz.dataSource.qzDS.password = 281926
org.quartz.dataSource.qzDS.maxConnections = 30
3.在Spring中使用Quartz
Spring为创建Quartz的Scheduler、Trigger和JobDetail提供了便利的FactoryBean类,以便能够在Spring容器中享受注入的好处。此外,Spring还提供了一些便利工具类,用于直接将Spring中的Bean包装成合法的任务。Spring进一步降低了使用Quartz的难度,能以更具Spring风格的方式使用Quartz。
3.1.创建JobDetail
用户可以直接使用Quartz的JobDetail在Spring中配置一个JobDetail Bean,但是JobDetail使用带参的构造函数,对于习惯通过属性配置的Spring用户来说存在使用上的不便。为此,Spring通过扩展JobDetail提供了一个更具有Bean风格的JobDetailFactoryBean。此外,Spring还提供了一个MethodInvokingJobDetailFactoryBean,通过这个FactoryBean可以将Spring容器中Bean的方法包装成Quartz任务,这样开发者就不必为Job创建对应的类。
3.1.1.JobDetailFactoryBean
JobDetailFactoryBean扩展于Quartz的JobDetail。使用该Bean声明JobDetail时,Bean的名字即任务的名字,如果没有指定所属组,就是用默认组。除了JobDetail中的属性外,还定义了以下属性
- jobClass:类型为Class,实现Job接口的任务类
- beanName:默认为Bean的id名,通过该属性显示指定Bean名称,它对应任务的名称
- jobDataAsMap:类型为Map,为任务所对应的JobDataMap提供值。之所以需要提供这个属性,是因为用户无法在Spring的配置文件中为JobDataMap类型的属性提供信息,所以Spring通过jobDataAsMap设置JobDataMap的值
- applicationContextJobDataKey:用户可以将Spring ApplicationContext的引用保存
JobDataMap中,以便在Job的代码中访问ApplicationContext.为了达到这个目的,用户需要指定一个键,用于在jobDataAsMap中保存ApplicationContext。如果不设置此键,JobDetailBean就不会将ApplicationContext放入JobDataMap中 - jobListenerNames:类型为String[],指定注册在Scheduler中的JobListener名称,以便让这些监听器对本任务的时间进行监听
配置JobDetail
<bean name="jobDetail" class="org.springframework.scheduling.quartz.JobDetailBean"
p:jobClass="com.smart.quartz.MyJob"
p:applicationContextJobDataKey="applicationContext">
<property name="jobDataAsMap">
<map>
<entry key="size" value="10" />
</map>
</property>
</bean>
public class MyJob implements StatefulJob {
public void execute(JobExecutionContext jctx) throws JobExecutionException {
// Map dataMap = jctx.getJobDetail().getJobDataMap();
Map dataMap = jctx.getTrigger().getJobDataMap();//获取JobDetail关联的JobDataMap
String size =(String)dataMap.get("size");
ApplicationContext ctx = (ApplicationContext)dataMap.get("applicationContext");
System.out.println("size:"+size);
dataMap.put("size",size+"0");//对JobDataMap所做的更改是否会被持久化取决于任务的类型
String count =(String)dataMap.get("count");
System.out.println("count:"+count);
}
}
3.1.2.MethodInvokingJobDetailFactoryBean
通常情况下,任务都定义在一个业务类方法中,这时,为了满足Quartz Job接口的规
定,还需要定义一个引用业务类方法的实现类。为了避免创建这个只包含一行调用代码的Job实现类,Spring提供了MethodInvokingJobDetailFactoryBean,借由该FactoryBean,可以将一个Bean的某个方法封装成满足Quartz要求的Job
<!-- 通过封装服务类方法实现 -->
<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" />
public class MyService {
public void doJob(){
System.out.println("in MyService.dojob().");
}
}
doJob()方法既可以是static的,也可以非static的,但不能拥有方法入参。通过
MethodInvokingJobDetailFactoryBean产生的JobDeatail不能序列化,所以不能被持久化到数据库中。如果希望使用持久化任务,则只能创建正规的Quartz的Job实现类
3.2.创建Trigger
3.2.1.SimpleTriggerFactoryBean
在默认情况下,通过SimpleTriggerFactoryBean配置的Trigger名称即为Bean的名称,属于默认数组。SimpleTriggerFactoryBean在SimpleTrigger的基础上新增了以下属性
- jobDetail:对应的JobDetail
- beanName:默认为Bean的id名,通过该属性显示指定Bean名称,它对应Trigger的名称
- jobDataAsMap:以Map类型为Trigger关联的JobDataMap提供值
- startDelay:延迟多少时间开始触发,单位为毫秒,默认值为0
- triggerListenerNames:类型为String[],指定注册在Scheduler中的TriggerListener名称,以便让这些监听器对本触发器的事件进行监听
<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>
3.2.2.CronTriggerFactoryBean
CronTriggerFactoryBean扩展于CronTrigger,触发器的名称即为Bean的名称,保存在默认组中。在CronTrigger的基础上,新增的属性和SimpleTriggerFactoryBean大致相同,配置的方法也和SimpleTriggerFactoryBean相似
<bean id="checkImagesTrigger"
class="org.springframework.scheduling.quartz.CronTriggerBean"
p:jobDetail-ref="jobDetail"
p:cronExpression="0/5 * * * * ?"/>
3.3.创建Scheduler
Quartz的SchedulerFactory是标准的工厂类,不太适合在Spring环境下使用。此外,为了保证Scheduler能够感知Spring容器的生命周期,在Spring容器启动后,Scheduler自动开始工作,而在Spring容器关闭前,自动关闭Scheduler。为此,Spring提供了SchedulerFactoryBean,这个FactoryBean大致拥有以下功能
- 以更具有Bean风格的方式为Scheduler提供配置信息
- 让Scheduler和Spring容器的生命周期建立关联,相生相息
- 通过属性配置的方式代替Quartz自身的配置文件
<bean id="scheduler"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="simpleTrigger" />
</list>
</property>
<property name="schedulerContextAsMap">
<map>
<entry key="timeout" value="30" />
</map>
</property>
<property name="quartzProperties">
<props>
<prop key="org.quartz.threadPool.class">
org.quartz.simpl.SimpleThreadPool
</prop>
<prop key="org.quartz.threadPool.threadCount">10</prop>
</props>
</property>
</bean>
在实际应用中,我们并不总是在程序部署的时候就确定需要哪些任务,往往需要在运行期根据业务数据动态产生触发器和任务。用户完全可以在运行时通过代码调用SchedulerFactoryBean获取Scheduler实例,然后动态注册触发器和任务。
网友评论