一 简介
在生活中,我们经常会制定一些“计划任务”,即在某个时间点做某件事情。同样地,在企业级应用中,也会经常碰到类似的任务调度的需求,下面来看几个例子。在购物网站,每天凌晨统计商品名、商家排名,每天晚上定点统计当日的销量、销售额、盈利等信息并生成报表,每15分钟查询用户的新订单并推送给对应处理人。在社区网站,每天统计用户的在线时长,并按照某种规则给予一定的称号和奖励等。在后台服务中做系统维护,每个工作日的固定时间将数据进行备份。可见,企业应用中离不开灵活的任务调度。
从以上例子可以看到,调度的核心是以时间为关注点,即在一个特定的时间点,系统执行指定的-个操作。任务调度本身涉及多线程并发、运行时间规则解析、运行现场的保护与恢复及线程池维护等。这项非常复杂的工作,可以通过一个开源任务调度框架来实现,它就是Quartz框架。
Quartz框架是一个开源的企业级任务调度服务,它可以被单独使用,也可以整合进任何Java应用,从小型应用到大型的电子商务系统,Quartz已经被作为任务调度的良好解决方案。Quartz提供了强大的任务调度机制,使用也非常简单。Quartz允许开发人员灵活定义调度时间表,提供和任务进行关联的便捷方法,另外,Quartz提供了调度运行环境的持久化机制,使系统出现故障关闭时,任务调度现场数据可以保存下来不致丢失。
1.1 Quartz框架核心概念
Quartz是一款开源框架,为了在应用中使用它,需要在官方网站(http://quartz-scheduler.org)下载Quartz,并添加到项目的classpath中。本文中,我们选用Quatz的1.8.6版本,此版本在项目中应用较多,也较为稳定。
在Quartz完整的下载包中包括docs文件夹和examples'文件夹。其中从docs文件夹中,可以找到Quarz完整的API文档,为开发提供了帮助;另外,examples文件中的多个示例程序,可以让你快速了解Quartz的核心功能。
下面来看一个具体的问题。
例如在某个OA系统中,允许用户制定工作提醒,包括任务时间和任务内容。通过定时任务,对员工张三的工作任务进行提醒,实现每3秒进行一次任务提醒,定时器在10秒后关闭。效果如图所示。
任务调度的核心是支持“某个时间”执行“一个计划任务”,在以上问题中,时间点为“每隔3秒”,执行的任务是“进行工作提醒”,即根据定制的提醒列表输出提醒信息。因此,需要设置一个调度来决定什么时间调用工作提醒任务。Quartz对任务调度进行了高度抽象,提出了3个核心概念一一任务、触发器和调度器,并在org.quartz中通过类和接口对核心概念进行了描述。
1.任务
顾名思义,任务就是执行的工作内容。Quartz提供Job接口来支持任务定义。Job接口的方法声明如下。
public interface Job {
void execue(JobExceutionContextcontext) throws JobExecutionException;
}
Job接口中只有一个execute()方法,开发者需要在自己的任务类中实现该方法,完成具体任务的执行。通过该方法中传人的JobExecutionContext,可以获取调度上下文的各种信息,如任务名称等。
Quartz每次执行Job时,都会创建一个Job实现类的新的实例,JobDetail类就是Job接口的一个实现类。Quartz允许对Job进行分组。
2.触发器
创建的Job要在什么时间定时执行呢?在Quartz中,触发器Trigger类允许定义触发Job执行的时间触发规则。例如,每隔1小时执行一次,每天15:00执行等。Trigger有两个实现类,分别为SimpleTrigger和CronTrigger,两个不同的触发器为不同的应用场景提供支持。
3.调度器
如何将工作任务和触发器绑定,保证任务可以在正确的时间执行呢?Quartz提供了调度器Scheduler类,它是Quartz独立运行的容器。Trigger和JobDetail可以注册到Scheduler中。Scheduler定义了多个接口方法,允许通过组及名称访问容器中的Trigger和JobDetail。Scheduler可以将Trigger绑定到一个JobDetail上,当Trigger被触发后,一个Job就会被执行。通过任务、触发器和调度器,就可以通过Quartz轻松实现任务调度。除此之外,Quartz还对一些特殊场景提供了支持,如Calendar对象。具体的使用在后面的小节会重点讲解。
1.2 quartz框架的使用流程
1. 建立java工程,导入jar包
2. 编写job
===================TaskEntity.java===================
package com.obtk.entitys;
public class TaskEntity {
private String dateStr;
private String task;
public TaskEntity() {
}
public TaskEntity(String dateStr, String task) {
super();
this.dateStr = dateStr;
this.task = task;
}
public String getDateStr() {
return dateStr;
}
public void setDateStr(String dateStr) {
this.dateStr = dateStr;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
public void showInfo(){
System.out.println(this.dateStr+"===>"+this.task);
}
}
==================TaskDao.java==================
package com.obtk.dao;
import java.util.ArrayList;
import java.util.List;
import com.obtk.entitys.TaskEntity;
public class TaskDao {
public List<TaskEntity> queryAllTask(){
List<TaskEntity> taskList=new ArrayList<TaskEntity>();
TaskEntity task1=new TaskEntity("2018-03-15 08:00:00", "起床执行打假任务");
TaskEntity task2=new TaskEntity("2018-03-15 20:30:00", "到打假办公室315进行研讨");
taskList.add(task1);
taskList.add(task2);
return taskList;
}
}
==================DaJiaJob.java====================
package com.obtk.jobs;
import java.util.List;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import com.obtk.dao.TaskDao;
import com.obtk.entitys.TaskEntity;
public class DaJiaJob implements Job{
private TaskDao taskDao;
public void setTaskDao(TaskDao taskDao) {
this.taskDao = taskDao;
}
public void execute(JobExecutionContext arg0) throws JobExecutionException {
String userName=(String)arg0.getJobDetail().getJobDataMap().get("user");
setTaskDao(new TaskDao());
List<TaskEntity> taskList=taskDao.queryAllTask();
System.out.println("请注意,这是"+userName+"发的消息==>");
for(TaskEntity task : taskList){
//输出打假任务通知的内容
task.showInfo();
}
}
}
======================TestJob.java===================
package com.obtk.test;
import java.util.Date;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
import com.obtk.jobs.DaJiaJob;
public class TestJob {
public static void main(String[] args) {
try {
//创建一个任务
JobDetail job=new JobDetail("remind task", "group1", DaJiaJob.class);
//传递参数
JobDataMap dataMap=job.getJobDataMap();
dataMap.put("user", "段玉");
//创建一个触发器,每隔3s执行一次,重复4次,总共执行5次
SimpleTrigger simTrig=new SimpleTrigger("dajiaTrig",4, 3000);
//1s钟之后启动
simTrig.setStartTime(new Date(System.currentTimeMillis()+1000));
//得到调度者工厂
SchedulerFactory factory=new StdSchedulerFactory();
//由工厂得到调度者对象
Scheduler sched=factory.getScheduler();
//调度任务执行
sched.scheduleJob(job, simTrig);
sched.start();//启动一个单独线程
Thread.sleep(15000); //睡眠15秒
sched.shutdown();
System.out.println("main方法结束了");
} catch (SchedulerException e) {
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}
}
}
1. 对比SimpeTrigger和CronTrigger
在上一节中提到,Quartz提供的Trigger包括SimpleTrigger和CronTrigger,之前的小节中,已经了解了SimpleTrigger,那么,SimpleTrigger和CronTrigger在应用场景和使用方式上有什么差异呢?如表所示。
触发器 | 应用场景 | 使用方式 |
SimpleTrigger | 固定时间间隔的调度任务(例如每隔2小时执行1次) | 通过设置触发器的属性:开始时间、结束时间、重复次数、重复间隔等 |
CronTrigger | 指定时间点的调度任务(例如每天1:00执行1次) | 通过定义Cron表达式 |
通过表7-1可知,CronTrigger允许用户更精准地控制任务的运行时间和日期。它不是定义工作盼频度,而是通过Cron表达式定义准确的运行时间。在实际的企业级应用中,CronTrigger也更加实用。例如,可以使用下面代码替换示例3中的SimpleTrigger,运行效果一样。其中“1/3 * * * * ?"就是一个Cron表达式。
CronTrigger cronTrig=new CronTrigger("remindJob","group1", "1/3 * * * * ?");2 cron表达式
要使用CronTrigger,必须掌握Cr on表达式的定义方法。Cron表达式至少有6个(也可能是7个)由空格分隔的时间元素组成。第7个元素是可选的。从左至右,这些元素定义如表所示。
位 置 | 字段含义 | 范 围 | 允许的特殊字符 |
1 | 秒 | O~59 | * / |
2 | 分钟 | 0~59 | * / |
3 | 小时 | 0~23 | * / |
4 | 月份中的哪一天 | 1~31 | * / ? L |
5 | 月份 | l~l2或者JAN--DEC | * / |
6 | 星期几 | l~7或者SUN-- SAT | * / ? L # |
7 | 年份 | 1970~2099 | * / |
Cron表达式的每个字段,都可以显式地规定一个值(如49)、一个范围(如1-6)、一个列表(如l,3,5)或者一个通配符(如*)表示每个值。有几个特殊的字符,具体说明如下。
(1)“一”:中划线,表示一个范围。
(2)“,”:使用逗号间隔的数据,表示一个列表。
(3)“*”:表示每个值,它可以用于所有字段。例如,在小时字段表示每小时。
(4)“?”:该字符仅用于“月份中的哪一天”字段和“星期几”字段,表示不指定值。当这两个字段其中之一被指定了值之后,为了避免冲突,需要将另外一个字段的值设为“?”。
(5)“/”:通常表示为x/y,x为起始值,y表示值的增量。例如,在分钟字段中使用“0/15”,表示0.15,30,45;在分钟字段中使用“3/20”表示从第3分钟开始,即3,23,43。
(6)“L”:表示“Last”,仅在日期和星期字段中使用,但是在两个字段中表示的含义不同。L在“月份中的哪一天”字段中,表示一个月的最后一天。,L在“星期几”字段中,表示一个星期的最后一天,也就是SAT(或者数字7)。例如,“0 0 14 7*L“表示每个月的最后一个星期的星期六14:00执行。另外,在“星期几”字段上,你可以用一个数字和L连用,表示月份的最后一个星期×。
例如,表达式“0'0 0 7*2L”中“2L”指每个月的最后一个星期一触发。需要注意的是,在使用L时,不要指定列表或范围,否则会导致错误。
(7)“≠≠”:只能用于“星期几”字段,表示这个月的第几个周几。例如,“6#3”指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,则不被触发。
下面通过几个示例来熟悉Cron表达式,如下表所示。
Cron表达式 | 含 义 |
0 15 4 * * ? | 每天凌晨4:15 |
0 0 8-12 ? * MON-FRI | 每个工作日的8:00~12:00 |
30 0 0 1 1 ?2014 | 2014年1月l曰凌晨过30秒 |
0 0 14 1,10,20 * ?* | 每月的l曰、10日、20 E|的14:00 |
0 0 17 L * ? | 每月最后一天17:00运行 |
0 0 10 ? * 6L | 每月最后一个星期五10:00运行 |
0 0/5 15,17 * * ? | 每天15:00-16:00每5分钟运行一次,此外,每天17:00~18:00每5分钟运行一次 |
0 30 10 ? * 6#3 2013 | 2013年每月的第3个星期五10:30触发 |
测试案例:
package com.obtk.test;
import java.util.Date;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
import com.obtk.jobs.DaJiaJob;
public class TestJob2 {
public static void main(String[] args) {
try {
//创建一个任务
JobDetail job=new JobDetail("remind task", "group1", DaJiaJob.class);
//传递参数
JobDataMap dataMap=job.getJobDataMap();
dataMap.put("user", "段玉");
//创建一个crontrigger
//2017年每月13号每分钟的第5秒开始,每5秒执行一次
CronTrigger cronTrig=new CronTrigger("cronTrig", "group1", "5/5 * * 13 * ? 2017");
//1s钟之后启动
cronTrig.setStartTime(new Date(System.currentTimeMillis()+1000));
//得到调度者工厂
SchedulerFactory factory=new StdSchedulerFactory();
//由工厂得到调度者对象
Scheduler sched=factory.getScheduler();
//调度任务执行
sched.scheduleJob(job, cronTrig);
sched.start();//启动一个单独线程
//Thread.sleep(15000); //睡眠15秒
//sched.shutdown();
System.out.println("main方法结束了");
} catch (SchedulerException e) {
e.printStackTrace();
}catch (Exception e) {
e.printStackTrace();
}
}
}
3. 使用Calendar
在某OA系统中,对于计划任务的提醒要求在每个工作日9:30发出,元旦除外。
分析上述问题,这里需要Cron定义一个精确的提醒任务的执行时间,另外,还需要排除每年中的节假日,这时就需要使用Quartz提供的Calendar对象(这个不同于Java API中的java.util.Calendar对象)。Quartz Calendar能够与Trigger进行关联,对于排除Trigger中的时间区间是很有用的。Calendar_是一个接口,下表列出了它的实现类。
Calendar名称 | 作 用 |
WeeklyCalendar | 用于排除星期中的一天或多天 |
MonthlYCalendar | 用于排除月份中的数天 |
AnnualCalendar | 用于排除年份中的一天或多天 |
HolidayCalendar | 用于排除节假日 |
要使用Quartz的Calendar,首先进行类实例化,并加入你要排除的日期,然后用Scheduler注册它,最后将Calendar和对应的Trigger进行关联。这样就实现了上述OA系统的要求,如下所示。
package com.obtk.test;
import java.util.*;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.calendar.AnnualCalendar;
import com.obtk.jobs.RemindJob;
public class SheduleTest2 {
public static void main(String[] args) {
//创建一个任务
JobDetail job=new JobDetail("myjob", "wxgroup", RemindJob.class);
JobDataMap dataMap=job.getJobDataMap();
dataMap.put("userName", "麻子");
CronTrigger cronTrig=null;
//创建调度者
SchedulerFactory sfc=new StdSchedulerFactory();
Scheduler schedul=null;
try {
cronTrig=new CronTrigger("myjobTri", "cronTrigs", "0/3 * * * * ?");
//构建排除日期的对象
AnnualCalendar exculteCal=new AnnualCalendar();
//java.uitl包下面的,mycal表示要排除的日期
Calendar mycal=GregorianCalendar.getInstance();
mycal.set(Calendar.MONTH, Calendar.DECEMBER);
mycal.set(Calendar.DAY_OF_MONTH, 23);
//进行排除
exculteCal.setDayExcluded(mycal, true);
schedul=sfc.getScheduler();
//调度对象排除日期
schedul.addCalendar("test", exculteCal, true, true);
cronTrig.setCalendarName("test");
//调度者把任务和触发器结合起来
schedul.scheduleJob(job, cronTrig);
//启动调度
schedul.start();
//多少秒之后停止
Thread.sleep(10000);
//停止调度
schedul.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
}
}