Timer与Quartz--Android和Java开发你都需要了解

前言:关于任务定时调度,Android基本用的要么Timer要么Handler配合Timer,而Java后端基本也是Timer;而其实,除了Timer,还有一个更强大的任务调度工具----Quartz。

Timer【概述】


Timer就不多说了

  • 作用: 在一个子线程中执行定时或循环的任务。
  • 包:java.util.Timer
  • 相关方法:Timer.schedule(参数)
    参数如下:

举一个简单的例子:
问:现在有两个任务,即时任务A 【执行一次/ 1s】和耗时任务B【执行一次/ 500s】,我想先启动任务A,5s后启动任务B,并且10s后让两个任务都结束。
答:

Timer timer = new Timer();
timer.schedule(new TimerTask() {// 任务A
     @Override
     public void run() {
        System.out.println("Timer正在调度 线程:" + Thread.currentThread().getName() + " 执行任务 A 中。。。");
       }
}, 0, 1000)
timer.schedule(new TimerTask() {// 任务B
     @Override
     public void run() {
        System.out.println("Timer正在调度 线程:" + Thread.currentThread().getName() + " 执行任务 B 中。。。");
       try {
          System.out.println("任务B很慢,它让Timer单例线程睡眠3s");
              Thread.sleep(3000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
    }
}, 5000, 500);
try {
    System.out.println("主线程睡眠10s");
    Thread.sleep(10000);
    timer.cancel();
    System.out.println("主线程关闭了Timer的所有任务");
} catch (InterruptedException e) {
    e.printStackTrace();
}

Quartz【重点】


Quartz,一句话: 他就是Timer 的升级版
github地址: https://github.com/quartz-scheduler/quartz

Quartz 总体架构

一、最主要的接口和类

  • 接口
    • Job:任务。
    • JobDetail: 任务详情(extends Serializable, Cloneable)。
    • Trigger: 触发器(extends Serializable, Cloneable, Comparable<Trigger>)。
    • SchedulerFactory:调度器工厂。
    • Scheduler: 调度器。
    • JobBuilder:任务详情构造器。
    • TriggerBuilder:触发器构造器。
    • StdSchedulerFactory:具体的调度器工厂。
    • SimpleTrigger:Trigger实现类,和Timer实现功能差不多。
    • CronTrigger:Trigger实现类,也是Quartz拉开Timer的地方。
    • StdSchedulerFactory:标准的调度器工厂类

二、例子

上个代码可能更容易懂:

  1. 导入Quartz 包:
  • Maven:
    <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.0</version>
    </dependency>
    
  • Gradle:
    compile 'org.quartz-scheduler:quartz:2.3.0'
    
  1. 首先,创建一个Job实现类,名为JobImpl
public class JobImpl implements Job{
    ///获取key:value 方法二:通过设置与key同名同属性的成员变量,并设置getter和setter方法来实现值获取
    // 在Quartz反射构建Job对象实例时,会自动判断和调用相应的setter方法,传入key同名的参数。
    private String stringkeyA;
    private Double doubleKeyB;
    public String getStringkeyA() {
        return stringkeyA;
    }

    public void setStringkeyA(String stringkeyA) {
        this.stringkeyA = stringkeyA;
    }

    public Double getDoubleKeyB() {
        return doubleKeyB;
    }

    public void setDoubleKeyB(Double doubleKeyB) {
        this.doubleKeyB = doubleKeyB;
    }

    public void execute(JobExecutionContext context) throws JobExecutionException {
        ///do sth

        ///Job 可以获取 的 信息如下:

        ///获取 JobDetail 对象引用
        JobDetail jobDetail = context.getJobDetail();

        ///获取job 的 标识名 和 所在组别
        JobKey jobKey = jobDetail.getKey();
        jobKey.getName();
        jobKey.getGroup();

        /// 获取 Trigger 对象引用
        Trigger trigger  = context.getTrigger();

        ///获取trigger 的 标识名 和 所在组别
        TriggerKey triggerKey = trigger.getKey();
        triggerKey.getName();
        triggerKey.getGroup();

        // 获取 JobDetail 中设定的key:value
        JobDataMap jobMap = jobDetail.getJobDataMap();
        // 获取 Trigger 中设定的key:value
        JobDataMap triggerMap = trigger.getJobDataMap();
        /// 合并并获取 JobDetail 和 Trigger 对象上设置的 key:value
        JobDataMap map  = context.getMergedJobDataMap();

        ///获取key:value 方法一:通过Map中获取
        String stringKeyA = map.getString("stringKeyA");
        String doubleKeyA = map.getString("doubleKeyA");
    }
}
  1. 然后,根据这个JobImpl和一些任务参数配置,构造一个专门做JobImpl这类任务的JobDetail对象:
//创建一个JobDetail ,并让其与 JobImpl 绑定起来
JobDetail jobDetail = JobBuilder
    .newJob(JobImpl.class)
    .withIdentity("JobA", "group1")
    .usingJobData("stringkeyA","任务DataA")
    .usingJobData("doublekeyB",2.15D)
    .build();
  1. 接着,创建一个Trigger对象,用于待会被调度器触发,Trigger用于设置任务的触发时间以及触发周期等。
//创建一个Trigger实例,并定义该job立即执行,每隔2s重复执行一次,直到永远
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("triggerA", "group1")
    .startNow()
    .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever())
    .usingJobData("stringkeyA","Trigger DataB")
    .usingJobData("floatkeyC",3.1f)
    .build();
  1. 最后,创建一个调度器,让其开始跑任务。
SchedulerFactory factory = new StdSchedulerFactory();///标准工厂
Scheduler scheduler = factory.getScheduler();///构建 调度器 实例
scheduler.scheduleJob(jobDetail,cronTrigger);// 让调度器 绑定 任务详情 和 触发器
scheduler.start();//执行任务
//........
scheduler.standby();//我想暂时挂起调度器,不让其执行任务
//........
scheduler.start();/// 重启调度器
//........
scheduler.shutdown(true);//终止调度器(true:等所有job都跑完,才终止;false:不等job,直接终止)

三、关于Trigger实现类

  1. SimpleTrigger
//以上的Trigger 实际上默认就是一个SimpleTrigger
Date startTime = new Date(System.currentTimeMillis());
Date endTime = new Date(System.currentTimeMillis()+3000);
SimpleTrigger simpleTrigger = (SimpleTrigger) TriggerBuilder.newTrigger()
      .withIdentity("mySimpleTrigger","G1")
      .startAt(startTime)
      .endAt(endTime)
      .withSchedule(SimpleScheduleBuilder
              .simpleSchedule()
              .withRepeatCount(3)//重复次数:0,正整数,或无穷
              .withIntervalInSeconds(2))// 执行周期: 0,长整数
      .build();
  1. CropTrigger【重点】
  • 基于Cron表达式:
    1. 由7个子表达式组成 的 字符串,描述了时间表的详细信息
    2. 格式: [秒] [分] [小时] [日] [月] [周] [年]
  • Cron表达式规则:


    • 符号意义:[, 或者] [- 范围] [* 每] [/ 每隔] [? 不关心(与*意义差不多)] [L 最后的一X] [W 距离X号最近的一个工作] [# 第]。
    • Cron表达式例子:
      1. 0 15 10 ? * *:每天10点15分触发。
      2. 0 0/5 14 * * ?:每天下午2点到2点59分,每隔5分钟执行一次。
      3. 0 15 10 ? * MON-FRI:周一到周五每天上午10点15分触发。
      4. 0 15 10 ? * 7#3:每月第三周的星期六 触发。
      5. 0 15 10 ? * 6L 2016-2017:从2016年到2017年每个月最后一周的周五触发。
  • CropTrigger例子:
    ///CronTrigger 基于日历的调度(功能强大, 可定义每周周几 执行一次任务 之类的)
    /**
     * Cron表达式:
     * 由7个子表达式组成 的 字符串,描述了时间表的详细信息
     * 格式: [秒] [分] [小] [时] [日] [月] [周] [年]
     */
    

CronTrigger cronTrigger = TriggerBuilder
.newTrigger()
.withIdentity("cronTrigger", "G1")
.withSchedule(
CronScheduleBuilder.cronSchedule("* * * * * ? *")///每一秒执行一次
)
.build();


### 四、注意点
1. **`ExecutionContext`在构建任务调度器的时候,就会被引入到`Job`中。**
2. **`ExecutionContext`本身拥有`JobDetail` 和`Trigger`的引用,这样,就可以给`Job`的执行过程中,随时获取到`JobDetail`和`Trigger`的信息。**
3. **`JobDataMap`是`ExecutionContext`携带的重要参数,它携带了`JobDetail`和`Trigger`在构造时设置的一些基本数据类型的参数。**
4. **`Job`获取`JobDataMap`中的参数的方式**【上述例子中有注释】
  * 法一:直接从Map对象中获取的方式
    ```
 JobDataMap jMap = jobExecutionContext.getJobDetail().getJobDataMap();
 JobDataMap tMap = jobExecutionContext.getTrigger().getJobDataMap();
 JobDataMap mergeMap = jobExecutionContext.getMergedJobDataMap();
    ```
    > 注意:上述`mergeMap`会将`Trigger`中设定的属性,覆盖掉`JobDetail`中设定的同名属性。
  * 法二:自定义Job子类中事先定义几个成员属性并实现其getter和setter方法,在构建Job实例时,会自动调用Job子类的setter方法,将key同名的value赋值到Job实例中。
5. **关于quartz.properties文件**
  * 它是调度器参数的配置文件,包括:
    1. 调度器属性
    2. 线程池属性
    3. 存储调度信息
    4. 插件属性配置
  * `StdSchedulerFactory`使用`Java.util.Properties`来创建和初始化Quartz调度器,在创建`Scheduler`对象时,就是根据`quartz.properties`文件中配置信息。
  * Quartz 默认情况下,会读取project下的`quartz.properties`文件,找不到文件,则会去查找引入的jar包下的`quartz.properties`文件,此文件拥有Quartz的一些相关配置信息。
  * 用法:
    1. 一般情况下,将`Jar包目录/org.quartz/`下的`quartz.properties`文件copy 到你的项目根目录下。
    2. 然后,可以自行配置各个参数。【具体配置可以查看[Quartz官网](http://www.quartz-scheduler.org/)】

# 总结一波
---
 实际上, Timer 和 Quartz 有一个共同的理念,就是:将想要在某些时刻执行的任务交给工作线程,并让该线程通过阻塞等机制,到了某一时刻执行。
 只不过,Timer内部是一个单例线程和一个数组类型的队列结构,多个任务可以入队等待,但同一时刻只有一个任务会被执行,并没有并发这一点,每个任务在执行一次后,如果有循环,那么则会重新入队。
  而Quartz ,相对于Timer的优势,总体来说,两点:
  1. 更强的定时调度能力:Quartz的CronTrigger,拥有Timer无法实现的日历定时调度功能。
  2. 任务并发能力:Quartz内部通过线程池管理多个线程,能够让任务并发执行,从而避免了使用Timer时由于前一个耗时任务导致队列中后一个任务错过执行时间的情况。

推荐阅读更多精彩内容