使用SpringBoot创建定时任务非常简单,目前主要有以下三种创建方式:

一、基于注解(@Scheduled) )

二、基于接口(SchedulingConfigurer) 前者相信大家都很熟悉,但是实际使用中我们往往想从数据库中读取指定时间来动态执行定时任务,这时候基于接口的定时任务就派上用场了。

三、基于注解设定多线程定时任务

一、静态:基于注解

基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。

(1)创建定时器

使用SpringBoot基于注解来创建定时任务非常简单,只需几行代码便可完成。 代码如下:
代码示例:

  1. @Configuration //1.主要用于标记配置类,兼备Component的效果。
  2. @EnableScheduling // 2.开启定时任务
  3. public class SaticScheduleTask {
  4. //3.添加定时任务
  5. @Scheduled(cron = "0/5 * * * * ?")
  6. //或直接指定时间间隔,例如:5秒
  7. //@Scheduled(fixedRate=5000)
  8. private void configureTasks() {
  9. System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
  10. }
  11. }

Cron表达式参数分别表示:

  • 秒(0~59) 例如0/5表示每5秒
  • 分(0~59)
  • 时(0~23)
  • 日(0~31)的某天,需计算
  • 月(0~11)
  • 周几( 可填1-7 或 SUN/MON/TUE/WED/THU/FRI/SAT)

    @Scheduled:除了支持灵活的参数表达式cron之外,还支持简单的延时操作
    例如 fixedDelay ,fixedRate 填写相应的毫秒数即可。

  1. // Cron表达式范例:
  2. 每隔5秒执行一次:*/5 * * * * ?
  3. 每隔1分钟执行一次:0 */1 * * * ?
  4. 每天23点执行一次:0 0 23 * * ?
  5. 每天凌晨1点执行一次:0 0 1 * * ?
  6. 每月1号凌晨1点执行一次:0 0 1 1 * ?
  7. 每月最后一天23点执行一次:0 0 23 L * ?
  8. 每周星期天凌晨1点实行一次:0 0 1 ? * L
  9. 26分、29分、33分执行一次:0 26,29,33 * * * ?
  10. 每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

(2)测试启动任务

启动应用,可以看到控制台打印出如下信息:
JAVA定时任务 - 图1
注意:显然,使用@Scheduled 注解很方便,但缺点是当我们调整了执行周期的时候,需要重启应用才能生效,这多少有些不方便。为了达到 **实时生效**的效果,可以使用**接口**来完成定时任务

二、动态:基于接口

基于接口(SchedulingConfigurer)

(1)导入jar依赖

  1. <dependencies>
  2. <dependency><!--添加Web依赖 -->
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency><!--添加MySql依赖 -->
  7. <groupId>mysql</groupId>
  8. <artifactId>mysql-connector-java</artifactId>
  9. </dependency>
  10. <dependency><!--添加Mybatis依赖 配置mybatis的一些初始化的东西-->
  11. <groupId>org.mybatis.spring.boot</groupId>
  12. <artifactId>mybatis-spring-boot-starter</artifactId>
  13. <version>1.3.1</version>
  14. </dependency>
  15. <dependency><!-- 添加mybatis依赖 -->
  16. <groupId>org.mybatis</groupId>
  17. <artifactId>mybatis</artifactId>
  18. <version>3.4.5</version>
  19. <scope>compile</scope>
  20. </dependency>
  21. </dependencies>

(2)添加数据库记录:

开启数据库mysql,创建一个执行周期表,添加执行周期,如下:

  1. CREATE TABLE `cron` (
  2. `cron_id` varchar(30) NOT NULL PRIMARY KEY,
  3. `cron` varchar(30) NOT NULL
  4. );
  5. INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');

JAVA定时任务 - 图2
然后在项目中的application 添加数据源:

  1. #mysql配置
  2. spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver
  3. spring.datasource.url = jdbc:mysql://rm-2ze29e1gr6iu0p31oko.mysql.rds.aliyuncs.com:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=UTC
  4. spring.datasource.username = root
  5. spring.datasource.password = zy123456@

(3)创建定时器:

数据库准备好数据之后,我们编写定时任务,注意这里添加的是TriggerTask,目的是循环读取我们在数据库设置好的执行周期,以及执行相关定时任务的内容。
具体代码如下:

  1. package com.example.studyprojects.JOB;
  2. import org.apache.ibatis.annotations.Mapper;
  3. import org.apache.ibatis.annotations.Select;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.scheduling.annotation.EnableScheduling;
  7. import org.springframework.scheduling.annotation.SchedulingConfigurer;
  8. import org.springframework.scheduling.config.ScheduledTaskRegistrar;
  9. import org.springframework.scheduling.support.CronTrigger;
  10. import org.springframework.util.StringUtils;
  11. import java.time.LocalDateTime;
  12. @Configuration //1.主要用于标记配置类,兼备Component的效果。
  13. @EnableScheduling // 2.开启定时任务
  14. public class DynamicScheduleTask implements SchedulingConfigurer {
  15. @Mapper
  16. public interface CronMapper {
  17. @Select("select cron from cron limit 1")
  18. public String getCron();
  19. }
  20. @Autowired //注入mapper
  21. @SuppressWarnings("all")
  22. CronMapper cronMapper;
  23. /**
  24. * 执行定时任务.
  25. */
  26. @Override
  27. public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
  28. taskRegistrar.addTriggerTask(
  29. //1.添加任务内容(Runnable)
  30. () -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),
  31. //2.设置执行周期(Trigger)
  32. triggerContext -> {
  33. //2.1 从数据库获取执行周期
  34. String cron = cronMapper.getCron();
  35. System.out.println("执行动态定时任务获取执行周期: "+cron);
  36. //2.2 合法性校验.
  37. if (StringUtils.isEmpty(cron)) {
  38. // Omitted Code ..
  39. }
  40. //2.3 返回执行周期(Date)
  41. return new CronTrigger(cron).nextExecutionTime(triggerContext);
  42. }
  43. );
  44. }
  45. }

(4)测试启动任务:

启动应用后,查看控制台,打印时间是我们预期的每5秒一次(也就是数据库中的每5秒一次):
JAVA定时任务 - 图3
如果我们需要改变执行周期,只需要修改数据库中的记录即可,无需重启项目
注意: 如果在数据库修改时格式出现错误,则定时任务会停止,即使重新修改正确;此时只能重新启动项目才能恢复。

三、多线程定时任务

基于注解设定多线程定时任务

(1)创建多线程定时任务

  1. //@Component注解用于对那些比较中立的类进行注释;
  2. //相对与在持久层、业务层和控制层分别采用 @Repository、@Service 和 @Controller 对分层中的类进行注释
  3. @Component
  4. @EnableScheduling // 1.开启定时任务
  5. @EnableAsync // 2.开启多线程
  6. public class MultithreadScheduleTask {
  7. @Async
  8. @Scheduled(fixedDelay = 1000) //间隔1秒
  9. public void first() throws InterruptedException {
  10. System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
  11. System.out.println();
  12. Thread.sleep(1000 * 10);
  13. }
  14. @Async
  15. @Scheduled(fixedDelay = 2000)
  16. public void second() {
  17. System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
  18. System.out.println();
  19. }
  20. }

注: 这里的@Async注解很关键

(2)启动测试

启动应用后,查看控制台:
JAVA定时任务 - 图4

从控制台可以看出,第一个定时任务和第二个定时任务互不影响;
并且,由于开启了多线程,第一个任务的执行时间也不受其本身执行时间的限制,所以需要注意可能会出现重复操作导致数据异常.

四、JDK原生定时工具:Timer

(1) Timer介绍

所在包:

java.util.Timer
java.util.TimerTask
Timer(后台执行线程)对TimerTask(业务线程)的定时调用

Timer是一个定时器类,通过该类可以为指定的定时任务进行配置。TimerTask类是一个定时任务类,该类实现了Runnable接口,而且是一个抽象类,如下所示:
可以通过继承该类,来实现自己的定时任务。

public abstract class TimerTask implements Runnable

Timer定时器实例有多种构造方法:

  • 创建一个新计时器: Timer()
  • 创建一个新计时器,可以指定其相关的线程作为守护程序运行: Timer(boolean isDaemon)
  • 创建一个新计时器,其相关的线程具有指定的名称: Timer(String name)
  • 创建一个新计时器,其相关的线程具有指定的名称,并且可以指定作为守护程序运行:Timer(String name, boolean isDaemon)

    (2) Timer执行流程图

JAVA定时任务 - 图5

(3) Timer方法

定时执行方法
1、在特定时间执行任务,只执行一次

  1. public void schedule(TimerTask task,Date time)

2、在特定时间之后执行任务,只执行一次

  1. public void schedule(TimerTask task,long delay)

3、指定第一次执行的时间,然后按照间隔时间,重复执行

  1. public void schedule(TimerTask task,Date firstTime,long period)

4、在特定延迟之后第一次执行,然后按照间隔时间,重复执行

  1. public void schedule(TimerTask task,long delay,long period)

参数说明:
delay: 延迟执行的毫秒数,即在delay毫秒之后第一次执行
period:重复执行的时间间隔

5、第一次执行之后,特定频率执行,与3同

  1. public void scheduleAtFixedRate(TimerTask task,Date firstTime,long period)

6、在delay毫秒之后第一次执行,后按照特定频率执行

  1. public void scheduleAtFixedRate(TimerTask task,long delay,long period)

方法名称schedule()和scheduleAtFixedRate()两者的区别:

  • schedule()方法更注重保持间隔时间的稳定:保障每隔**period**时间可调用一次
  • scheduleAtFixedRate()方法更注重保持执行频率的稳定:保障多次调用的频率趋近于**period**时间,如果任务执行时间大于**period**,会在任务执行之后马上执行下一次任务

Timer注销

  1. timer.cancel();

(4) Timer案例

(4.1) 特定时间后执行1

  1. public void schedule(TimerTask task,long delay)

参数说明:

  • task为:执行任务
  • delay:时间毫秒数

方法的含义:
在delay毫秒后执行任务task,只执行一次。

(4.2) 案例一

1分钟后同步数据。

案例实现:

  1. 同步任务: ```java package com.yank.framework.common;

import java.util.TimerTask;

public class SynchroTimerTask extends TimerTask {

  1. @Override
  2. public void run() {
  3. // TODO Auto-generated method stub
  4. System.out.println("Synchro data to other servers.");
  5. }

}

  1. 2. 定时任务:
  2. ```java
  3. package com.yank.framework.common;
  4. import java.util.Timer;
  5. import java.util.TimerTask;
  6. public class SynchroTest {
  7. public static void main(String[] args) {
  8. // TODO Auto-generated method stub
  9. TimerTask task = new SynchroTimerTask();
  10. Timer timer = new Timer();
  11. timer.schedule(task, 1000);
  12. }
  13. }

(4.3) 案例二

按点吃饭

首先定义吃饭的任务,制定饭点,没小时进行检查,到点就吃饭。

  1. package com.yank.framework.common;
  2. import java.util.ArrayList;
  3. import java.util.Calendar;
  4. import java.util.List;
  5. import java.util.TimerTask;
  6. /*
  7. * 定时吃饭
  8. * */
  9. public class EatTimerTask extends TimerTask {
  10. //吃饭时间
  11. private static List<Integer> eatTimes;
  12. /*
  13. * 静态初始化
  14. * */
  15. static {
  16. initEatTimes();
  17. }
  18. /*
  19. * 初始化吃饭时间
  20. * */
  21. private static void initEatTimes(){
  22. eatTimes = new ArrayList<Integer>();
  23. eatTimes.add(8);
  24. eatTimes.add(12);
  25. eatTimes.add(18);
  26. }
  27. /*
  28. * 执行
  29. * */
  30. @Override
  31. public void run() {
  32. // TODO Auto-generated method stub
  33. Calendar calendar = Calendar.getInstance();
  34. System.out.println("检查是否到了吃饭的点");
  35. int hour = calendar.get(Calendar.HOUR_OF_DAY);
  36. if(eatTimes.contains(hour))
  37. {
  38. System.out.println("饿了,吃饭...");
  39. }
  40. }
  41. }

定时检查执行:

  1. package com.yank.framework.common;
  2. import java.util.Calendar;
  3. import java.util.Date;
  4. import java.util.Timer;
  5. import java.util.TimerTask;
  6. public class EatTimerTaskTest {
  7. public static void main(String[] arg){
  8. TimerTask task = new EatTimerTask();
  9. Calendar calendar= Calendar.getInstance();
  10. Date firstTime = calendar.getTime();
  11. //间隔:1小时
  12. long period = 1000 * 60 * 60;
  13. //测试时间每分钟一次
  14. //period = 1000 * 60;
  15. Timer timer = new Timer();
  16. timer.schedule(task, firstTime, period);
  17. }
  18. }

五、定时任务框架Quartz

(1) 介绍Quartz

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

(2) Quartz 框架的特点

(2.1) Quartz 优点

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

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

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

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

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

(2.1) Quartz 缺点

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

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

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

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

(3) Quartz 核心组件

核心组件图和架构:
JAVA定时任务 - 图6

关键概念:
(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,其中 triggerjob任务调度的元数据, scheduler 是实际执行调度的控制器

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

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

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

(4.2) Quartz 核心元素关系图

JAVA定时任务 - 图9

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

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

(4.4) 数据存储

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

Quartz 中,JobStoreSupport 使用一个驱动代理来操作 triggerjob 的数据存储:StdJDBCDelegateStdJDBCDelegate 实现了大部分基于标准 JDBC 的功能接口,但是对于各种数据库来说,需要根据其具体实现的特点做某些特殊处理,因此各种数据库需要扩展 StdJDBCDelegate 以实现这些特殊处理。 Quartz 已经自带了一些数据库的扩展实现,可以直接使用,如下图所示:
JAVA定时任务 - 图12
作为嵌入式数据库的代表,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)。这个 CalendarTrigger 一起使用,但是它们的作用相反,它是用于排除任务不被执行的情况。例如,按照 Trigger 的规则在 10 月 1 号需要执行任务,但是 Calendar 指定了 10 月 1 号是节日(国庆),所以任务在这一天将不会被执行。通常来说,**Calendar** 用于排除节假日的任务调度,从而使任务只在工作日执行。

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

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

JAVA定时任务 - 图13

无状态任务一般指可以并发的任务,即任务之间是独立的,不会互相干扰。例如我们定义一个 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 比较合适。

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

  1. SimpleThreadPoolorg.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 源代码所示:
JAVA定时任务 - 图14
但是在实际应用中,有些用户需要保留以前的trigger,作为历史记录,或者作为以后创建其他trigger 的依据。如何保留结束的trigger 呢?
一个办法是应用开发者自己维护一份数据备份记录,并且与 Quartz 原表的记录保持一定的同步。这个办法实际操作起来比较繁琐,而且容易出错,不推荐使用。
另外一个办法是通过修改并重新编译Quartztrigger 类,修改其默认的行为。我们以org.quartz.SimpleTrigger 为例,修改上面代码中if (!mayFireAgain()) 部分的代码如下:
JAVA定时任务 - 图15

另外我们需要在 SimpleTrigger 中定义一个新的类属性:needRetain,如下所示:
JAVA定时任务 - 图16
在定义自己的 trigger 时,设置该属性,就可以选择是否在trigger 结束时删除trigger。如下代码所示:
JAVA定时任务 - 图17
有人可能会考虑通过定义一个新的类,然后继承 org.quartz.SimpleTrigger 类并覆盖 executionComplete( ) 方法来实现。但是这种方法是行不通的,因为Quartz 内部在处理时会根据trigger 的类型重新生成SimpleTrigger 类的实例,而不是使用我们自己定义的类创建的实例。这一点应该是Quartz 的一个小小的不足之处,因为它把扩展 trigger的能力堵死了。好在Quartz是开源的,我们可以根据需要进行修改。

(6)Quartz 框架,简单应用

(6.1) Quartz 应用场景

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

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

    (6.2.1) 引入依赖
    1. <dependency>
    2. <groupId>org.quartz-scheduler</groupId>
    3. <artifactId>quartz</artifactId>
    4. <version>2.3.0</version>
    5. </dependency>

(6.2.2) 创建HelloJob实现Job接口
  1. public class HelloJob implements Job{
  2. private static final SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3. @Override
  4. public void execute(JobExecutionContext context) throws JobExecutionException {
  5. Date now = new Date();
  6. String currentTime = sdf.format(now);
  7. System.out.println("执行时间为:"+currentTime);
  8. }
  9. }

(6.2.3) 创建HelloScheduler触发任务
  1. public class HelloScheduler {
  2. public static void main(String[] args) throws SchedulerException {
  3. //创建jobDetail绑定HelloJob
  4. JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
  5. .withIdentity("myJob","myGroup").build();
  6. //创建触发器trigger每个2秒执行一次,一直执行
  7. Trigger trigger = TriggerBuilder.newTrigger().withIdentity("mtTrigger", "myGroup").startNow()
  8. .withSchedule(SimpleScheduleBuilder.simpleSchedule()
  9. .withIntervalInSeconds(2).repeatForever()).build();
  10. //创建调度者工厂
  11. SchedulerFactory schedulerFactory = new StdSchedulerFactory();
  12. //创建调度者
  13. Scheduler scheduler = schedulerFactory.getScheduler();
  14. //启动调度器
  15. scheduler.start();
  16. //设置调度任务
  17. scheduler.scheduleJob(jobDetail, trigger);
  18. }
  19. }

执行结果:

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

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

(6.3.1) 引入依赖
  1. <dependency>
  2. <groupId>org.quartz-scheduler</groupId>
  3. <artifactId>quartz</artifactId>
  4. <version>2.3.0</version>
  5. </dependency>

(6.3.2)创建HelloJob实现job接口,任务执行时输出时间
  1. public class HelloJob implements Job{
  2. private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3. @Override
  4. public void execute(JobExecutionContext arg0) throws JobExecutionException {
  5. Date now = new Date();
  6. String currentDate = sdf.format(now);
  7. System.out.println("现在时间是:"+currentDate+":开始执行任务生成表格,或者发送邮件");
  8. }
  9. }

(6.2.3) 创建触发类CronScheduler
  1. public class CronScheduler {
  2. public static void main(String[] args) throws Exception {
  3. JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
  4. .withIdentity("myJob").build();
  5. Trigger trigger = TriggerBuilder.newTrigger()
  6. .withIdentity("cronTrigger")
  7. //cron表达式 这里定义的是 在每天下午2点到下午2:59期间的每1分钟触发
  8. .withSchedule(CronScheduleBuilder.cronSchedule("0 * 14 * * ?"))
  9. .build();
  10. SchedulerFactory factory = new StdSchedulerFactory();
  11. //创建调度器
  12. Scheduler scheduler = factory.getScheduler();
  13. //启动调度器
  14. scheduler.start();
  15. //jobDetail和trigger加入调度
  16. scheduler.scheduleJob(jobDetail, trigger);
  17. }
  18. }

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

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

(7) Quartz的三大API

(7.1) Job

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

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

(7.2) Tigger

(7.2.1)startTime和endTime

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

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

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

  1. 执行时间为:2019-04-16 10:36:09
  2. 执行时间为:2019-04-16 10:36:11
  3. 执行时间为:2019-04-16 10:36:13
  4. 执行时间为:2019-04-16 10:36:15

(7.2.2)BaseCalndar

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

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

这里使用CronCalendar排除

  1. public class HelloJob implements Job{
  2. private static final SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3. @Override
  4. public void execute(JobExecutionContext context) throws JobExecutionException {
  5. Date now = new Date();
  6. String currentTime = sdf.format(now);
  7. System.out.println("执行时间为:"+currentTime);
  8. }
  9. }
  1. public class HelloScheduler {
  2. public static void main(String[] args) throws SchedulerException, ParseException {
  3. //创建jobDetail绑定HelloJob
  4. JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
  5. .withIdentity("myJob","myGroup").build();
  6. //创建触发器trigger每个2秒执行一次,一直执行
  7. Trigger trigger = TriggerBuilder.newTrigger().withIdentity("mtTrigger", "myGroup")
  8. .withSchedule(SimpleScheduleBuilder.simpleSchedule()
  9. .withIntervalInSeconds(2).repeatForever())
  10. //将calendar排除规则绑定到触发器
  11. .modifiedByCalendar("myCalendar")
  12. .build();
  13. //创建调度者工厂
  14. SchedulerFactory schedulerFactory = new StdSchedulerFactory();
  15. //创建调度者
  16. Scheduler scheduler = schedulerFactory.getScheduler();
  17. CronCalendar calendar = new CronCalendar("* * 0-12,18-23 ? * *");
  18. //向Scheduler注册日历
  19. scheduler.addCalendar("myCalendar", calendar, false, false);
  20. //启动调度器
  21. scheduler.start();
  22. //设置调度任务
  23. scheduler.scheduleJob(jobDetail, trigger);
  24. }
  25. }
  1. 11:39:12.270 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers
  2. 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 执行间隔的单位(秒,分钟,小时,天,月,年,星期)
    1. CalendarIntervalScheduleBuilder
    2. .calendarIntervalSchedule()
    3. .withIntervalInDays(1) //每天执行一次
    4. //.withIntervalInHours(1)
    5. //.withIntervalInMinutes(1)
    6. //.withIntervalInMonths(1)
    7. //.withIntervalInSeconds(1)
    8. //.withIntervalInWeeks(1)
    9. //.withIntervalInHours(1)
    10. .build()

(7.3.2) DailyTimeIntervalTrigger

指定每天的某个时间段内,以一定的时间间隔执行任务。并且它可以支持指定星期。
它适合的任务类似于:指定每天9:00 至 18:00 ,每隔70秒执行一次,并且只要周一至周五执行。
它的属性有:

  • startTimeOfDay 每天开始时间
  • endTimeOfDay 每天结束时间
  • daysOfWeek 需要执行的星期
  • interval 执行间隔
  • intervalUnit 执行间隔的单位(秒,分钟,小时,天,月,年,星期)
  • repeatCount 重复次数

    1. public static void main(String[] args) throws SchedulerException {
    2. SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    3. //1.创建一个jobDetail的实例,将该实例与HelloJob Class绑定
    4. JobDetail jobDetail = JobBuilder
    5. .newJob(HelloJob.class)
    6. .withIdentity("myJob", "group1") //定义name 和 group
    7. .build();
    8. //2.创建一个Trigger触发器的实例
    9. Trigger simpleTrigger = TriggerBuilder.newTrigger()
    10. .withIdentity("zhlTrigger")
    11. .withSchedule(
    12. DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()
    13. .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(8, 0)) //每天8:00开始
    14. .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(17, 0)) //17:00 结束
    15. .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) //周一至周五执行
    16. .withIntervalInHours(1) //每间隔1小时执行一次
    17. .withRepeatCount(100) //最多重复100次(实际执行100+1次)
    18. )
    19. .modifiedByCalendar("holidays") //将我们设置好的Calander与trigger绑定
    20. .build();
    21. //3.创建schedule实例
    22. StdSchedulerFactory factory = new StdSchedulerFactory();
    23. Scheduler scheduler = factory.getScheduler();
    24. System.out.println("现在的时间 :"+sf.format(new Date()));
    25. System.out.println();
    26. System.out.println("最近的一次执行时间 :"+sf.format(scheduler.scheduleJob(jobDetail,simpleTrigger))); //scheduler与jobDetail、trigger绑定,并打印出最近一次执行的事件
    27. scheduler.start();
    28. }

(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且不能被重启