JAVA定时任务系列(三、定时任务框架Quartz)

接上文......

五、定时任务框架Quartz

(1) 介绍Quartz

Quartz框架是Java领域最著名的开源任务调度工具,也是目前事实上的定时任务标准,几乎全部的开源定时任务框架都是基于Quartz核心调度构建而成。

(2) Quartz 框架的特点

(2.1) Quartz 优点

作为一个优秀的开源调度框架Quartz 具有以下优点

强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;

灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;

分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。本文暂不讨论该部分内容

另外,作为 Spring 默认的调度框架,Quartz 很容易与 Spring 集成实现灵活可配置的调度功能。

(2.1) Quartz 缺点

不适合大量的短任务,不适合过多节点部署

需要把任务信息持久化到业务数据表,和业务有耦合;

调度逻辑和执行逻辑并存于同一个项目中,在机器性能固定的情况下,业务和调度之间不可避免地会相互影响;

quartz集群模式下,是通过数据库独占锁来唯一获取任务,任务执行并没有实现完善的负载均衡机制;

(3) Quartz 核心组件

核心组件图和架构:

关键概念:
(1) Scheduler任务调度器,是执行任务调度的控制器。本质上是一个计划调度容器,注册了全部Trigger和对应的JobDetail, 使用线程池作为任务运行的基础组件,提高任务执行效率。

(2) Trigger触发器,用于定义任务调度的时间规则,告诉任务调度器什么时候触发任务,其中CronTrigger是基于cron表达式构建的功能强大的触发器。

(3) Calendar日历特定时间点的集合。一个trigger可以包含多个Calendar,可用于排除或包含某些时间点

(4) JobDetail:是一个可执行的工作,用来描述Job实现类及其它相关的静态信息,如Job的名称、监听器等相关信息。

(5) Job任务执行接口,只有一个execute方法,用于执行真正的业务逻辑

(6) JobStore任务存储方式,主要有RAMJobStoreJDBCJobStoreRAMJobStore是存储在JVM的内存中,有丢失和数量受限的风险,JDBCJobStore是将任务信息持久化到数据库中,支持集群。

(3) Quartz 框架使用推荐

(1)业务使用要满足动态修改和重启不丢失, 一般需要使用数据库进行保存。

Quartz本身支持JDBCJobStore,但是其配置的数据表比较多,官方推荐配置可参照官方文档,超过10张表,业务使用比较重。

在使用的时候只需要存在基本trigger配置和对应任务以及相关执行日志的表即可满足绝大部分需求。

(2)组件化

quartz动态任务配置信息持久化到数据库,将数据操作包装成基本jar包,供项目之间使用,引用项目只需要引入jar包依赖和配置对应的数据表,使用时就可以对Quartz配置透明。

(3)扩展

集群模式

通过故障转移和负载均衡实现了任务的高可用性,通过数据库的锁机制来确保任务执行的唯一性,但是集群特性仅仅只是用来HA,节点数量的增加并不会提升单个任务的执行效率,不能实现水平扩展。

Quartz插件*

可以对特定需要进行扩展,比如增加触发器和任务执行日志,任务依赖串行处理场景,可参考quartz插件——实现任务之间的串行调度

(4)Quartz 任务调度的基本实现原理

(4.1) 核心元素

\(~~~~~~\) Quartz 任务调度的核心元素是 scheduler(核心调度器), triggerjob,其中 trigger job任务调度的元数据, scheduler 是实际执行调度的控制器

\(~~~~~~\)Quartz 中,trigger用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz 中主要提供了四种类型的 triggerSimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIcludedDayTrigger。这四种 trigger 可以满足企业应用中的绝大部分需求。进一步讨论四种 trigger 的功能。

\(~~~~~~\) Quartz 中,job 用于表示被调度的任务。主要有两种类型的job无状态的(stateless)和有状态的(stateful。对于同一个 trigger 来说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job 主要有两种属性:volatilitydurability,其中** volatility 表示任务是否被持久化到数据库存储,而 durability 表示在没有 trigger 关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。一个 job 可以被多个 trigger 关联,但是一个 trigger 只能关联一个 job。**

\(~~~~~~\)Quartz中, schedulerscheduler 工厂创建DirectSchedulerFactory 或者 StdSchedulerFactory。 第二种工厂 StdSchedulerFactory 使用较多,因为 DirectSchedulerFactory 使用起来不够方便,需要作许多详细的手工编码设置。 Scheduler 主要有三种:RemoteMBeanSchedulerRemoteScheduler StdScheduler。本文以最常用的 StdScheduler 为例讲解。这也是笔者在项目中所使用的 scheduler 类。

(4.2) Quartz 核心元素关系图

(4.3) Quartz 线程视图和调度流程图

在 Quartz 中,有两类线程,Scheduler 调度线程和任务执行线程,其中任务执行线程通常使用一个线程池维护一组线程。

Scheduler 调度线程主要有两个:** 执行常规调度的线程,和执行 misfired trigger 的线程。常规调度线程轮询存储的所有 trigger,如果有需要触发的 trigger,即到达了下一次触发的时间,则从任务执行线程池获取一个空闲线程,执行与该 trigger 关联的任务。Misfire 线程是扫描所有的 trigger,查看是否有 misfired trigger**,如果有的话根据 misfire 的策略分别处理。下图描述了这两个线程的基本流程:

(4.4) 数据存储

Quartz 中的triggerjob 需要存储下来才能被使用。Quartz 中有两种存储方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是将triggerjob 存储在内存中,而 JobStoreSupport 是基于 jdbctriggerjob 存储到数据库中。RAMJobStore 的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在通常应用中,都是使用 JobStoreSupport

Quartz 中,JobStoreSupport 使用一个驱动代理来操作 triggerjob 的数据存储:StdJDBCDelegateStdJDBCDelegate 实现了大部分基于标准 JDBC 的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展 StdJDBCDelegate 以实现这些特殊处理。 Quartz 已经自带了一些数据库的扩展实现,可以直接使用,如下图所示:

作为嵌入式数据库的代表,Derby 近来非常流行。如果使用 Derby 数据库,可以使用上图中的 CloudscapeDelegate 作为 triggerjob 数据存储的代理类。

(5)Quartz 开发过程中的应用

(5.1) 如何使用不同类型的 Trigger

前面我们提到Quartz 中四种类型的 Trigger:SimpleTriggerCronTirggerDateIntervalTrigger, 和 NthIncludedDayTrigger

  • SimpleTrigger : 一般用于实现每隔一定时间执行任务,以及重复多少次,如每 2 小时执行一次,重复执行 5 次
    SimpleTrigger 内部实现机制是通过计算间隔时间来计算下次的执行时间,这就导致其不适合调度定时的任务。例如我们想每天的 1:00AM 执行任务,如果使用 SimpleTrigger 的话间隔时间就是一天。注意这里就会有一个问题,即当有 misfired 的任务并且恢复执行时,该执行时间是随机的(取决于何时执行 misfired 的任务,例如某天的 3:00PM)。这会导致之后每天的执行时间都会变成 3:00PM,而不是我们原来期望的 1:00AM。

  • CronTirgger 类似于 LINUX 上的任务调度命令crontab即利用一个包含 7 个字段的表达式来表示时间调度方式。例如,"0 15 10 * * ? *" 表示每天的 10:15AM 执行任务。对于涉及到星期和月份的调度,CronTirgger 是最适合的,甚至某些情况下是唯一选择。例如,"0 10 14 ? 3 WED" 表示三月份的每个星期三的下午 14:10PM 执行任务。读者可以在具体用到该 trigger 时再详细了解每个字段的含义。

  • DateIntervalTriggerQuartz 1.7 之后的版本加入的,其最适合调度类似每 N(1, 2, 3...)小时,每 N 天,每 N 周等的任务。虽然 SimpleTrigger 也能实现类似的任务,但是 DateIntervalTrigger 不会受到我们上面说到的 misfired 任务的影响。另外,DateIntervalTrigger 也不会受到 DST(Daylight Saving Time, 即中国的夏令时)调整的影响。笔者就曾经因为该原因将项目中的 SimpleTrigger 改为了 DateIntervalTrigger,因为如果使用 SimpleTrigger,本来设定的调度时间就会由于 DST 的调整而提前或延迟一个小时,而 DateIntervalTrigger 不会受此影响。

  • NthIncludedDayTrigger 的用途比较简单明确,即用于每隔一个周期的第几天调度任务,例如,每个月的第 3 天执行指定的任务。

除了上面提到的 4 种 TriggerQuartz 中还定义了一个 Calendar 类(注意,是 org.quartz.Calendar)。这个 Calendar Trigger 一起使用,但是它们的作用相反,它是用于排除任务不被执行的情况。例如,按照 Trigger 的规则在 10 月 1 号需要执行任务,但是 Calendar 指定了 10 月 1 号是节日(国庆),所以任务在这一天将不会被执行。通常来说,Calendar 用于排除节假日的任务调度,从而使任务只在工作日执行。

(5.2) 使用有状态(StatefulJob)还是无状态的任务(Job)

Quartz 中,Job 是一个接口,企业应用需要实现这个接口以定义自己的任务。基本来说,任务分为有状态和无状态两种。实现 Job 接口的任务缺省为无状态的。Quartz 中还有另外一个接口 StatefulJob实现 StatefulJob 接口的任务为有状态的,上一节的简单实例中,我们定义的 SampleJob 就是实现了 StatefulJob 接口的有状态任务。下图列出了 Quartz 中 Job 接口的定义以及一些自带的实现类:

无状态任务一般指可以并发的任务,即任务之间是独立的,不会互相干扰。例如我们定义一个 trigger,每 2 分钟执行一次,但是某些情况下一个任务可能需要 3 分钟才能执行完,这样,在上一个任务还处在执行状态时,下一次触发时间已经到了。对于无状态任务,只要触发时间到了就会被执行,因为几个相同任务可以并发执行。但是对有状态任务来说,是不能并发执行的,同一时间只能有一个任务在执行。

在笔者项目中,某些任务需要对数据库中的数据进行增删改处理。这些任务不能并发执行,否则会造成数据混乱。因此我们使用 StatefulJob 接口。现在回到上面的例子,任务每 2 分钟执行一次,若某次任务执行了 5 分钟才完成,Quartz 会怎么处理呢?按照 trigger 的规则,第 2 分钟和第 4 分钟分别会有一次预定的触发执行,但是由于是有状态任务,因此实际不会被触发。在第 5 分钟第一次任务执行完毕时,Quartz 会把第 2 和第 4 分钟的两次触发作为 misfired job进行处理。对于 misfired jobQuartz 会查看其 misfire 策略是如何设定的,如果是立刻执行,则会马上启动一次执行,如果是等待下次执行,则会忽略错过的任务,而等待下次(即第 6 分钟)触发执行。

(5.3) 如何设置 Quartz 的线程池和并发任务

Quartz 中自带了一个线程池的实现:SimpleThreadPool。类如其名,这只是线程池的一个简单实现,没有提供动态自发调整等高级特性。Quartz 提供了一个配置参数:org.quartz.threadPool.threadCount可以在初始化时设定线程池的线程数量,但是一次设定后不能再修改。假定这个数目是 10,则在并发任务达到 10 个以后,再有触发的任务就无法被执行了,只能等待有空闲线程的时候才能得到执行。因此有些 trigger 就可能被 misfire。但是必须指出一点,这个初始线程数并不是越大越好。当并发线程太多时,系统整体性能反而会下降,因为系统把很多时间花在了线程调度上。根据一般经验,这个值在 10 -- 50 比较合适。

对于一些注重性能的线程池来说,会根据实际线程使用情况进行动态调整,例如初始线程数,最大线程数,空闲线程数等。读者在应用中,如果有更好的线程池,则可以在配置文件中通过下面参数替换

SimpleThreadPool:org.quartz.threadPool.class = myapp.GreatThreadPool

(5.4) 如何处理 Misfired 任务

Quartz 应用中,misfired job 是经常遇到的情况。一般来说,下面这些原因可能造成 misfired job

系统因为某些原因被重启。在系统关闭到重新启动之间的一段时间里,可能有些任务会被 misfire

Trigger 被暂停(suspend)的一段时间里,有些任务可能会被 misfire

线程池中所有线程都被占用,导致任务无法被触发执行,造成 misfire

有状态任务在下次触发时间到达时,上次执行还没有结束;

为了处理 misfired job,Quartz 中为trigger定义了处理策略,主要有下面两种:

MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:针对 misfired job 马上执行一次;

MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次触发;

建议读者在应用开发中,将该设置作为可配置选项,使得用户可以在使用过程中,针对已经添加的 tirgger 动态配置该选项。

(5.5) 如何保留已经结束的 Trigger

Quartz中,一个tirgger在最后一次触发完成之后,会被自动删除。Quartz 默认不会保留已经结束的 trigger,如下面 Quartz 源代码所示:

但是在实际应用中,有些用户需要保留以前的 trigger,作为历史记录,或者作为以后创建其他 trigger 的依据。如何保留结束的 trigger 呢?

一个办法是应用开发者自己维护一份数据备份记录,并且与 Quartz 原表的记录保持一定的同步。这个办法实际操作起来比较繁琐,而且容易出错,不推荐使用。

另外一个办法是通过修改并重新编译Quartz trigger 类,修改其默认的行为。我们以 org.quartz.SimpleTrigger 为例,修改上面代码中 if (!mayFireAgain()) 部分的代码如下:

另外我们需要在 SimpleTrigger 中定义一个新的类属性:needRetain,如下所示:

在定义自己的 trigger 时,设置该属性,就可以选择是否在 trigger 结束时删除 trigger。如下代码所示:

有人可能会考虑通过定义一个新的类,然后继承 org.quartz.SimpleTrigger 类并覆盖 executionComplete( ) 方法来实现。但是这种方法是行不通的,因为 Quartz 内部在处理时会根据 trigger 的类型重新生成 SimpleTrigger 类的实例,而不是使用我们自己定义的类创建的实例。这一点应该是 Quartz 的一个小小的不足之处,因为它把扩展 trigger 的能力堵死了。好在Quartz是开源的,我们可以根据需要进行修改。

(6)Quartz 框架,简单应用

(6.1) Quartz 应用场景

  • 餐厅系统会在每周四晚上的22点自动审核并生成报表
  • 人事系统会在每天早晨8点给有待办的人员自动发送Email提醒

(6.2) 简单使用(重复执行)

(6.2.1) 引入依赖
<dependency>
	<groupId>org.quartz-scheduler</groupId>
	<artifactId>quartz</artifactId>
	<version>2.3.0</version>
</dependency>
(6.2.2) 创建HelloJob实现Job接口

public class HelloJob implements Job{
	private static final SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		Date now = new Date();
		String currentTime = sdf.format(now);
		System.out.println("执行时间为:"+currentTime);
	}
}
(6.2.3) 创建HelloScheduler触发任务
public class HelloScheduler {
	public static void main(String[] args) throws SchedulerException {
		//创建jobDetail绑定HelloJob
		JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
						.withIdentity("myJob","myGroup").build();
		//创建触发器trigger每个2秒执行一次,一直执行
		Trigger trigger = TriggerBuilder.newTrigger().withIdentity("mtTrigger", "myGroup").startNow()
						.withSchedule(SimpleScheduleBuilder.simpleSchedule()
						.withIntervalInSeconds(2).repeatForever()).build();
		//创建调度者工厂
		SchedulerFactory schedulerFactory = new StdSchedulerFactory();
		//创建调度者
		Scheduler scheduler = schedulerFactory.getScheduler();
		//启动调度器
		scheduler.start();
		//设置调度任务
		scheduler.scheduleJob(jobDetail, trigger);
	}
}

执行结果:

执行时间为:2019-04-15 23:45:41
23:45:43.388 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.xuxu.quartz.HelloJob
23:45:43.388 [DefaultQuartzScheduler_Worker-9] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob
23:45:43.388 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
执行时间为:2019-04-15 23:45:43
23:45:45.387 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'myGroup.myJob', class=com.xuxu.quartz.HelloJob
23:45:45.387 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
23:45:45.391 [DefaultQuartzScheduler_Worker-10] DEBUG org.quartz.core.JobRunShell - Calling execute on job myGroup.myJob
执行时间为:2019-04-15 23:45:45

(6.3) 定时执行使用cron表达式确定时间

(6.3.1) 引入依赖
<dependency>
	<groupId>org.quartz-scheduler</groupId>
	<artifactId>quartz</artifactId>
	<version>2.3.0</version>
</dependency>
(6.3.2)创建HelloJob实现job接口,任务执行时输出时间
public class HelloJob implements Job{
	private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	@Override
	public void execute(JobExecutionContext arg0) throws JobExecutionException {
		Date now = new Date();
		String currentDate = sdf.format(now);
		System.out.println("现在时间是:"+currentDate+":开始执行任务生成表格,或者发送邮件");
	}
}
(6.2.3) 创建触发类CronScheduler
public class CronScheduler {
    public static void main(String[] args) throws Exception {
        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
                .withIdentity("myJob").build();
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("cronTrigger")
                //cron表达式 这里定义的是  在每天下午2点到下午2:59期间的每1分钟触发
                .withSchedule(CronScheduleBuilder.cronSchedule("0 * 14 * * ?"))
                .build();
        SchedulerFactory factory = new StdSchedulerFactory();
        //创建调度器
        Scheduler scheduler = factory.getScheduler();
        //启动调度器
        scheduler.start();
        //jobDetail和trigger加入调度
        scheduler.scheduleJob(jobDetail, trigger);
    }
}

这里通过cron表达式确定时间规则
一般我们会使用cron生成器
执行结果如下:

现在时间是:2019-04-16 09:21:00:开始执行任务生成表格,或者发送邮件

(7) Quartz的三大API

(7.1) Job

JobDetail & Job & JobDataMap
JobDetail任务的定义,而Job是任务的执行逻辑。在JobDetail里会引用一个Job Class定义。每一个JobDetail都会有一个JobDataMapJobDataMap本质就是一个Map的扩展类,只是提供了一些更便捷的方法,比如getString()之类的。

public class CronScheduler {
	public static void main(String[] args) throws Exception {
		JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
				//添加jobname,jobgroup
				.withIdentity("myJob","myGroup")
				//jobDataMap信息
				.usingJobData("message","this is a message")
				.build();
		Trigger trigger = TriggerBuilder.newTrigger()
				.withIdentity("cronTrigger")
				//cron表达式 这里定义的是4月16日早上9点21分开始执行
				.withSchedule(CronScheduleBuilder.cronSchedule("0 00 10 16 4 ? *"))
				.build();
		SchedulerFactory factory = new StdSchedulerFactory();
		//创建调度器
		Scheduler scheduler = factory.getScheduler();
		//启动调度器
		scheduler.start();
		//jobDetail和trigger加入调度
		scheduler.scheduleJob(jobDetail, trigger);
	}
}
public class HelloJob implements Job{
	private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	@Override
	public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
		Date now = new Date();
		String currentDate = sdf.format(now);
		JobDetail jobDetail = jobExecutionContext.getJobDetail();
		JobDataMap jobDataMap = jobDetail.getJobDataMap();
		JobKey jobKey = jobDetail.getKey();
		String jobName = jobKey.getName();
		String group = jobKey.getGroup();
		String message = (String) jobDataMap.get("message");
		System.out.println("现在时间是:"+currentDate+":开始执行任务生成表格,或者发送邮件");
		System.out.println("jobName---"+jobName);
		System.out.println("group---"+group);
		System.out.println("message---"+message);
	}
}
现在时间是:2019-04-16 10:00:00:开始执行任务生成表格,或者发送邮件
jobName---myJob
group---myGroup
message---this is a message

(7.2) Tigger

(7.2.1)startTime和endTime

有时候我们希望一个定时任务在一定的时间内是每天执行,比如2017年11月24日到2017年12月15日之间执行,这时候我们就要使用startTimeendTime来限定事件范围了。例子中我们把时间规定在几秒钟之内运行,方便查看效果。

public class HelloJob implements Job{
	private static final SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		Date now = new Date();
		String currentTime = sdf.format(now);
		System.out.println("执行时间为:"+currentTime);
	}
}
public class HelloScheduler {
	public static void main(String[] args) throws SchedulerException {
		//创建jobDetail绑定HelloJob
		JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
						.withIdentity("myJob","myGroup").build();
		//设定开始时间,结束时间确定范围
		Date triggerStartTime = new Date();
		//3秒后开始执行
		triggerStartTime.setTime(triggerStartTime.getTime()+3000);
		Date triggerEndTime = new Date();
		//10秒后结束执行
		triggerEndTime.setTime(triggerEndTime.getTime()+10000);
		//创建触发器trigger每个2秒执行一次,一直执行
		Trigger trigger = TriggerBuilder.newTrigger().withIdentity("mtTrigger", "myGroup")
						.startAt(triggerStartTime)
						.endAt(triggerEndTime)
						.withSchedule(SimpleScheduleBuilder.simpleSchedule()
						.withIntervalInSeconds(2).repeatForever()).build();
		//创建调度者工厂
		SchedulerFactory schedulerFactory = new StdSchedulerFactory();
		//创建调度者
		Scheduler scheduler = schedulerFactory.getScheduler();
		//启动调度器
		scheduler.start();
		//设置调度任务
		scheduler.scheduleJob(jobDetail, trigger);
	}
}

3秒后执行,10秒内结束执行

执行时间为:2019-04-16 10:36:09
执行时间为:2019-04-16 10:36:11
执行时间为:2019-04-16 10:36:13
执行时间为:2019-04-16 10:36:15
(7.2.2)BaseCalndar

calendar不是java.util.Calendarcalendar是为了补充Trigger的时间,可以排除或加入一下特定的时间QuartzCalender 专门用于屏闭一个时间区间,使 Trigger 在这个区间中不被触发。

  • AnnualCalendar:排除每一年中指定的一天或者多少天 ,精度是天
  • CronCalendar:使用表达式排除某些时间段不执行,精度取决于Cron表达式,最大精度到秒
  • DailyCalendar:指定的时间范围内的每一天不执行,指定每天的时间段,格式是HH:MM[:SS[:mmm]]。也就是最大精度可以到毫秒。
  • HolidayCalendar:排除节假日,精度到天
  • MonthlyCalendar:排除月份中的数天,可选值为1-31。精度是天
  • WeeklyCalendar:排除星期中的一天或多天,可选值比如为java.util.Calendar.SUNDAY,精度是天。

这里使用CronCalendar排除

public class HelloJob implements Job{
	private static final SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		Date now = new Date();
		String currentTime = sdf.format(now);
		System.out.println("执行时间为:"+currentTime);
	}
}
public class HelloScheduler {
	public static void main(String[] args) throws SchedulerException, ParseException {
		//创建jobDetail绑定HelloJob
		JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
						.withIdentity("myJob","myGroup").build();
		//创建触发器trigger每个2秒执行一次,一直执行
		Trigger trigger = TriggerBuilder.newTrigger().withIdentity("mtTrigger", "myGroup")
				.withSchedule(SimpleScheduleBuilder.simpleSchedule()
						.withIntervalInSeconds(2).repeatForever())
				//将calendar排除规则绑定到触发器
				.modifiedByCalendar("myCalendar")
				.build();
		//创建调度者工厂
		SchedulerFactory schedulerFactory = new StdSchedulerFactory();
		//创建调度者
		Scheduler scheduler = schedulerFactory.getScheduler();
		CronCalendar calendar = new CronCalendar("* * 0-12,18-23 ? * *");
		//向Scheduler注册日历
		scheduler.addCalendar("myCalendar", calendar, false, false);
		//启动调度器
		scheduler.start();
		//设置调度任务
		scheduler.scheduleJob(jobDetail, trigger);
	}
}
11:39:12.270 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers
11:39:12.381 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers

上面指定的是0-12 18-23不执行发现12点之前没有执行

(7.3) Trigger的实现类

(7.3.1) CalendarIntervalTrigger

CalendarIntervalTrigger:是一个具体的Trigger,用来触发基于定时重复的JobDetail

Trigger将会每隔N个calendar在trigger中定义的时间单元触发一次。这个trigger不适合使用SimpleTrigger完成(例如由于每一个月的时间不是固定的描述),也不适用于CronTrigger(例如每5个月)。

相较于SimpleTrigger有两个优势:1、更方便,比如每隔1小时执行,你不用自己去计算1小时等于多少毫秒。 2、支持不是固定长度的间隔,比如间隔为月和年。但劣势是精度只能到秒

它适合的任务类似于:9:00 开始执行,并且以后每周 9:00 执行一次

它的属性有:

  • interval 执行间隔
  • intervalUnit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)
CalendarIntervalScheduleBuilder
     .calendarIntervalSchedule()
     .withIntervalInDays(1)  //每天执行一次
   //.withIntervalInHours(1)
   //.withIntervalInMinutes(1)
   //.withIntervalInMonths(1)
   //.withIntervalInSeconds(1)
   //.withIntervalInWeeks(1)
   //.withIntervalInHours(1)
     .build()
(7.3.2) DailyTimeIntervalTrigger

指定每天的某个时间段内,以一定的时间间隔执行任务。并且它可以支持指定星期。

它适合的任务类似于:指定每天9:00 至 18:00 ,每隔70秒执行一次,并且只要周一至周五执行。

它的属性有:

  • startTimeOfDay 每天开始时间
  • endTimeOfDay 每天结束时间
  • daysOfWeek 需要执行的星期
  • interval 执行间隔
  • intervalUnit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)
  • repeatCount 重复次数
public static void main(String[] args) throws SchedulerException {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //1.创建一个jobDetail的实例,将该实例与HelloJob Class绑定
        JobDetail jobDetail = JobBuilder
                .newJob(HelloJob.class)
                .withIdentity("myJob", "group1") //定义name 和 group
                .build();
 
        //2.创建一个Trigger触发器的实例
        Trigger simpleTrigger = TriggerBuilder.newTrigger()
                .withIdentity("zhlTrigger")
                .withSchedule(
                        DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
                                .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(8, 0)) //每天8:00开始
                                .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(17, 0)) //17:00 结束
                                .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) //周一至周五执行
                                .withIntervalInHours(1) //每间隔1小时执行一次
                                .withRepeatCount(100) //最多重复100次(实际执行100+1次)
                )
                .modifiedByCalendar("holidays")   //将我们设置好的Calander与trigger绑定
                .build();
 
        //3.创建schedule实例
        StdSchedulerFactory factory = new StdSchedulerFactory();
        Scheduler scheduler = factory.getScheduler();
        System.out.println("现在的时间 :"+sf.format(new Date()));
        System.out.println();
        System.out.println("最近的一次执行时间 :"+sf.format(scheduler.scheduleJob(jobDetail,simpleTrigger))); //scheduler与jobDetail、trigger绑定,并打印出最近一次执行的事件
        scheduler.start();
    }

(8) Scheduler工厂模式

所有的Scheduler实例应该由SchedulerFactory来创建,一般包含:StdSchedulerFactory、DirectSchedulerFactory(参数信息需要在代码中维护故不常用)。

StdSchedulerFactory使用一组参数来创建和初始化Quartz调度器,配置参数一般存储在quartz.properties文件中,调用getScheduler方法就能创建和初始化调度器对象。

Scheduler的主要函数:

  • Data scheduleJob(JobDetail jobDetail,Trigger trigger);
  • void start();——启动Scheduler;
  • void standby();——将Scheduler暂时挂起,可以用start()继续执行任务;
  • void shutDown()关闭Scheduler且不能被重启

JAVA定时任务系列

JAVA定时任务系列(一、基于注解,基于接口)
JAVA定时任务系列(二、JDK原生定时工具:Timer)
JAVA定时任务系列(三、定时任务框架Quartz)

posted @ 2021-05-21 16:51  Mr*宇晨  阅读(4723)  评论(0编辑  收藏  举报