Quartz 学习
入门 —— Quartz 需要了解的几个概念:
- 触发器 Trigger
- 任务 Job
- 调度器 Scheduler
例程:
// QuartzUtil.java
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
public class QuartzUtil {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
Trigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(
simpleSchedule().withIntervalInSeconds(2)
.withRepeatCount(5) // 0-5
).build();
JobDetail jobDetail = newJob(MailJob.class)
.withIdentity("mailJob", "mailgroup")
.usingJobData("email", "admin@10086.com")
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
Thread.sleep(15000); // 15秒
scheduler.shutdown();
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
}
}
}
// MailJob.java
import org.quartz.*;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MailJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDetail detail = context.getJobDetail();
JobDataMap data = detail.getJobDataMap();
String email = data.getString("email");
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
String now = sdf.format(new Date());
System.out.printf("时间%s,发送邮件%s%n", now, email);
}
}
注:Trigger 和 Job 都可以通过 withIdentity() 函数进行分组,分组的目的是方便管理,例如同时启动或关闭同一组的 Trigger 或 Job。
Job 管理:
一、Job 的组成部分:
- JobDetail:对具体的 Job 进行描述
- Job 类:具体执行任务的 Job 类
- JobDataMap:给 Job 提供参数数据
除了上面例程添加数据的方式,还可以在 JobDetail 对象创建好之后,通过获得 JobDataMap 对象,添加或修改参数数据:
jobDetail.getJobDataMap().put("email", "admin@taobao.org");
jobDetail.getJobDataMap().put("test", "test data");
二、Job 的并发
Quartz 默认是并发的。调度器 Scheduler 什么时候关闭,需要考虑 Job 本身的执行时间、Job 执行的间隔时间(一般是单线程执行时考虑)。
可以设置非并发执行:
@DisallowConcurrentExecution
public class MailJob implements Job
三、Job 异常处理
通常有两种办法:
- 通知所有管理这个 Job 的调度器 Scheduler,停止运行它
- 修改参数,重新运行
分别如下:
// ExceptionJobOne.java
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class ExceptionJobOne implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
int i = 0;
try {
System.out.println(100 / i);
} catch (Exception e) {
System.out.println("异常,取消调度");
JobExecutionException exception = new JobExecutionException(e);
exception.setUnscheduleAllTriggers(true);
throw exception;
}
}
}
// ExceptionJobTwo.java
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class ExceptionJobTwo implements Job {
private static int i = 0;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
System.out.println(100 / i);
} catch (Exception e) {
System.out.println("异常,重新执行");
i = 1;
JobExecutionException exception = new JobExecutionException(e);
exception.setRefireImmediately(true);
throw exception;
}
}
}
中断 Job:
需要实现 InterruptableJob 接口,而不只是 Job 接口:
// StoppableJob.java
import org.quartz.*;
public class StoppableJob implements InterruptableJob {
private boolean stop = false;
@Override
public void interrupt() {
System.out.println("调度叫停");
stop = true;
}
@Override
public void execute(JobExecutionContext context) {
// Job 一直运行,直到被叫停
while (!stop) {
try {
System.out.println("一秒一次,检测是否停止");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("持续工作中");
}
}
}
// QuartzUtil.java
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
public class QuartzUtil {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
Trigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.build();
JobDetail jobDetail = newJob(StoppableJob.class)
.withIdentity("stoppableJob", "someGroup")
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
Thread.sleep(5000); // 让 Job 飞一会
scheduler.interrupt(jobDetail.getKey()); // group.job
scheduler.shutdown();
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
}
}
}
触发器:
触发器:指定什么时间开始触发、触发多少次、隔多久触发一次。
SimpleTrigger:
一、约定时间长度,触发执行(DateBuilder.nextGivenSecondDate 不准时):
// MailJob.java
import org.quartz.*;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MailJob implements Job {
@Override
public void execute(JobExecutionContext context) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
String now = sdf.format(new Date());
System.out.printf("发送邮件, 执行时间:%s%n", now);
}
}
// QuartzUtil.java
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
public class QuartzUtil {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 到规定时间后,才触发执行。返回值为实际执行时间(时间很少是准的,一般就几秒钟)
Date startTime = DateBuilder.nextGivenSecondDate(null, 8);
SimpleTrigger trigger = (SimpleTrigger) newTrigger()
.withIdentity("trigger1", "group1")
.startAt(startTime).build();
JobDetail jobDetail = newJob(MailJob.class)
.withIdentity("mailJob", "mailGroup").build();
// 返回调度时间,也就是实际执行时间(scheduleTime==startTime)
Date scheduleTime = scheduler.scheduleJob(jobDetail, trigger);
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.printf("new Date:%s%n", sdf.format(new Date())); // 当前时间
System.out.printf("startTime:%s%n", sdf.format(startTime));
System.out.printf("任务:%s,scheduleTime:%s,运行%d次,间隔%d毫秒%n",
jobDetail.getKey(), sdf.format(scheduleTime),
trigger.getRepeatCount() + 1, trigger.getRepeatInterval());
scheduler.start();
Thread.sleep(20000); // 让 Job 飞一会
scheduler.shutdown();
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
}
}
}
二、规定时间长度,触发执行:
与上面代码的区别在于 startTime 的不同,效果上在于这个是准时的:
Date startTime = DateBuilder.futureDate(10,
DateBuilder.IntervalUnit.SECOND);
三、无限重复:
SimpleTrigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(
simpleSchedule().repeatForever().withIntervalInSeconds(2)
).startAt(startTime).build();
CronTrigger:
Cron 是 Linux 下的一个定时器,功能很强大,但是表达式较复杂。可以触发的情况更多。
CronTrigger 使用 Cron 表达式设置触发时间和次数。表达式由秒、分、时、日(Day of Month)、月、星期(Day of Week:星期几)、年(可选),7个部分组成,用空格分割。
其中字段中可出现的字符如下:
首先所有字段都可以使用的字符有:逗号(,)、减号(-)、星号(*)、斜杠(/)。
然后是每个字段自己能使用的字符,和有效范围:
- 日(Day of Month):?、L、W、C。有效范围为 0 - 31
- 月:有效范围为 1 - 12 或 JAN - DEC
- 星期(Day of Week):?、L、C、#。有效范围为 1 - 7 或 SUN - SAT,所以 1 对应着星期天 SUN,2 对应星期一 MON,依此类推 —— 星期为 num - 1
- 年:有效范围为 1970 - 2099
以上一些特殊字符的含义如下:
- 逗号 ,:表示列出枚举值(即匹配一个列表)。例如 second 字段为 5,20,那么每分钟内,5 秒和 20 秒的时候,会分别触发一次
- 减号 -:表示范围。例如 minute 字段为 5 - 20,那么每个小时的 5 分至 20 分,每分钟都会触发一次
- 星号 *:匹配该字段的任意值(即每个值,因为匹配就触发了)。例如每分钟、每秒
- 斜杠 /:用法是 —— 起始时间/间隔时间,即从起始时间开始,每隔一段时间都触发一次。例如,minute 字段为 5/20,那么每小时内,在 5 分、25 分、45 分时,分别触发一次
- 问号 ?:如上,只能用在日(Day of Month)、星期(Day of Week)两个字段。它也匹配字段的任意值,但因为两个字段互相影响,所以实际为 "无意义的值",如其字面上的意思
- 字母 L:只能用在日、星期,表示最后的一个指定日期。例如,星期为 5L,那么只在最后的一个星期四触发
- 字母 W:只能出现在 Day of Month,表示有效工作日(Work Day),即周一至周五,那么调度器会在离指定日期最近的有效工作日触发 —— 如果 Day of Month 是 5W。如果 5 日是星期六,选择就近的工作日星期五,所以 4 日触发;如果 5 日是星期日,就在星期一(6 号)触发;如果 5 日是 Work Day,那么就在 5 日触发。此外,W 就近寻找时,不会跨月份
- LW 连用:Latest Work Day of Month,某月最后的一个有效工作日(但最后一个工作日不一定是星期五)
- 井号 #:只能出现在 Day of Week,用于确定每个月,第几个星期几(游戏规则是 DayofWeek#num)。如 4#2,表示某月的第 2 个星期 3
Cron 表达式的例子就算了,网上都有。下面是 CronTrigger 的例程:
// MailJob.java
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MailJob implements Job {
@Override
public void execute(JobExecutionContext context) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()));
}
}
// QuartzUtil.java
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
public class QuartzUtil {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
CronTrigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.withSchedule(cronSchedule("0/2 * * * * ?")).build(); // 每两秒触发一次
JobDetail jobDetail = newJob(MailJob.class)
.withIdentity("mailJob", "mailGroup").build();
scheduler.scheduleJob(jobDetail, trigger);
System.out.println(trigger.getCronExpression());
scheduler.start();
Thread.sleep(20000); // 让 Job 飞一会
scheduler.shutdown();
} catch (SchedulerException | InterruptedException e) {
e.printStackTrace();
}
}
}
监听器 Listener
quartz 的监听器有 Job 监听器、Trigger 监听器、Scheduler 监听器,从而可以对不同层面进行监控。用得较多的是 Job 监听器,用于监听是否执行了,下面只介绍它。
Job 监听器:
监听器类需要实现 JobListener 接口,其有四个抽象方法:
- String getName():返回一个字符串,对 JobListener 的名称进行说明。对于注册为全局的监听器,getName() 主要用于记录日志;对于特定 Job 的 JobListener,注册在 JobDetail 上的监听器名称,必须匹配监听器类 getName() 方法的返回值(说白了,这里好像不用管了)
- void jobToBeExecuted(JobExecutionContext):Job 执行前调用
- void jobExecutionVetoed(JobExecutionContext):Job 执行前,被取消执行时(Job 中断、TriggerListener 否决等)调用
- void jobWasExecuted(JobExecutionContext, JobExecutionException):执行后调用
// MailJobListener.java
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
public class MailJobListener implements JobListener {
@Override
public String getName() {
return "listener of mail job";
}
@Override
public void jobToBeExecuted(JobExecutionContext context) {
System.out.println("准备执行:" + context.getJobDetail().getKey());
}
@Override
public void jobExecutionVetoed(JobExecutionContext context) {
System.out.println("取消执行:" + context.getJobDetail().getKey());
}
@Override
public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
System.out.println("执行结束:" + context.getJobDetail().getKey());
}
}
// QuartzUtil.java 和上一个例程相比,只有下面的一些改动
// MailJob 监听
MailJobListener mailJobListener = new MailJobListener();
KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(mailJobDetail.getKey());
scheduler.getListenerManager().addJobListener(mailJobListener, keyMatcher);