Quartz知识点总结

简单说明

简单的定时任务使用Timer或者ScheduledExecutorService

quartz支持复杂的定时执行功能。支持ram存储(内存存储)和持久化存储。quartz有分布式和集群能力

简单使用

  1. 获取任务调度器Schedule。任务调度器可以管理任务。
  2. 创建任务实例。使用JobBuilder(任务类.class)创建,会返回一个JobDetail。任务是一个实现了Job,并实现execute方法的类而已,execute方法记录要执行的事情。
  3. 创建触发器Trigger。Trigger是用来指定触发策略的,包括什么时候开始执行、循环多少次、多久执行一次呀,等等。支持链式编程。
  4. 把任务和触发器告诉任务调度器,使用scheduleJob方法来完成。
  5. 使用任务调度器启动任务
  6. 使用任务调度器关闭任务

例子:

java 复制代码
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class SimpleJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("SimpleJob is executing at: " + new java.util.Date());
    }
}
java 复制代码
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzExample {
    public static void main(String[] args) {
        try {
            // 1. 获取任务调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 2. 创建任务实例
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
                    .withIdentity("simpleJob", "group1")
                    .build();

            // 3. 创建触发器
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("simpleTrigger", "group1")
                    .startNow()
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                            .withIntervalInSeconds(10) // 每10秒执行一次
                            .repeatForever()) // 无限重复
                    .build();

            // 4. 把任务和触发器告诉任务调度器
            scheduler.scheduleJob(job, trigger);

            // 5. 使用任务调度器启动任务
            scheduler.start();

            // 让任务运行一段时间
            Thread.sleep(60000); // 60秒

            // 6. 使用任务调度器关闭任务
            scheduler.shutdown();

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用了 Builder 模式(建造者模式):

java 复制代码
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("simpleJob", "group1")
.build();
java 复制代码
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("simpleTrigger", "group1")
        .startNow()
        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(10)
                .repeatForever())
        .build();

Quartz基本的实现原理

  1. job和jobdetail的关系是什么?

Job(任务)是一个接口,表示一个具体的任务。你需要实现这个接口,并在 execute 方法中定义任务的具体逻辑。Job 只关注任务的执行逻辑,即 做什么。

JobDetail(任务详情)JobDetail 是一个类,用于描述任务的详细信息。它包含了任务的元数据,例如任务类、任务名称、任务组、任务数据等。JobDetail 关注任务的 元信息,即 谁来做 和 怎么做。

java 复制代码
JobDetail job = JobBuilder.newJob(SimpleJob.class)
        .withIdentity("simpleJob", "group1")
        .build();

这里 JobDetail 描述了任务的详细信息:

  • 任务类是 SimpleJob.class。
  • 任务名称是 "simpleJob"。
  • 任务组是 "group1"。

总结:

  • Job:定义任务的具体逻辑(做什么)。
  • JobDetail:描述任务的元信息(谁来做、怎么做)。
  • 关系:JobDetail 是 Job 的包装器,包含了 Job 的类信息和任务的额外信息。
  • 设计目的:解耦任务逻辑和任务元信息,提高灵活性和可扩展性。
  1. 原理图:
  1. Job、JobDetail和Trigger的关系

Trigger:

  • 一个 Trigger 只能绑定一个 JobDetail。

JobDetail:

  • 一个 JobDetail 可以被多个 Trigger 绑定。相当于是一个Job信息可以被多次使用。

Job:

  • 一个 Job 类可以创建多个 JobDetail 对象。相当于是可以允许多个JobDetail使用一个Job的execute逻辑。
  • 默认情况下,Quartz 每次触发器触发时都会创建一个新的 Job 实例,再去执行execute方法。(这样可以线程安全)

例子:

java 复制代码
package com.example;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.*;
import static org.quartz.JobBuilder.newJob;

public class SimpleTriggerMisfireExample {
    public static void main(String[] args) throws SchedulerException {
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();

        JobDetail job = newJob(MyJob.class)
                .withIdentity("myJob", "group1")
                .storeDurably()
                .build();

        Trigger trigger1 = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(5)
                        .repeatForever())
                .build();

        Trigger trigger2 = TriggerBuilder.newTrigger()
                .withIdentity("trigger2", "group1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(3)
                        .repeatForever())
                .build();

        Set<Trigger> triggers = new HashSet<>();
        triggers.add(trigger1);
        triggers.add(trigger2);

        Map<JobDetail, Set<? extends Trigger>> triggersAndJobs = new HashMap<>();
        triggersAndJobs.put(job, triggers);

        scheduler.scheduleJobs(triggersAndJobs, false);

        System.out.println("调度器启动时间: " + new Date());
        scheduler.start();

        try {
            Thread.sleep(60000); // 等待 60 秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        scheduler.shutdown();
    }

    public static class MyJob implements Job {
        @Override
        public void execute(JobExecutionContext context) {
            Trigger trigger = context.getTrigger();
            System.out.println("任务执行开始: " + new Date() +
                    ", Trigger: " + trigger.getKey() +
                    ", 触发时间: " + trigger.getPreviousFireTime());
            try {
                Thread.sleep(3000); // 模拟任务执行耗时 3 秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("任务执行结束: " + new Date());
        }
    }
}

执行结果:

java 复制代码
D:\jdk1.8.0_172\bin\java.exe "-javaagent:D:\IDEA\IntelliJ IDEA 2021.3.2\lib\idea_rt.jar=55136:D:\IDEA\IntelliJ IDEA 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath D:\jdk1.8.0_172\jre\lib\charsets.jar;D:\jdk1.8.0_172\jre\lib\deploy.jar;D:\jdk1.8.0_172\jre\lib\ext\access-bridge-64.jar;D:\jdk1.8.0_172\jre\lib\ext\cldrdata.jar;D:\jdk1.8.0_172\jre\lib\ext\dnsns.jar;D:\jdk1.8.0_172\jre\lib\ext\jaccess.jar;D:\jdk1.8.0_172\jre\lib\ext\jfxrt.jar;D:\jdk1.8.0_172\jre\lib\ext\localedata.jar;D:\jdk1.8.0_172\jre\lib\ext\nashorn.jar;D:\jdk1.8.0_172\jre\lib\ext\sunec.jar;D:\jdk1.8.0_172\jre\lib\ext\sunjce_provider.jar;D:\jdk1.8.0_172\jre\lib\ext\sunmscapi.jar;D:\jdk1.8.0_172\jre\lib\ext\sunpkcs11.jar;D:\jdk1.8.0_172\jre\lib\ext\zipfs.jar;D:\jdk1.8.0_172\jre\lib\javaws.jar;D:\jdk1.8.0_172\jre\lib\jce.jar;D:\jdk1.8.0_172\jre\lib\jfr.jar;D:\jdk1.8.0_172\jre\lib\jfxswt.jar;D:\jdk1.8.0_172\jre\lib\jsse.jar;D:\jdk1.8.0_172\jre\lib\management-agent.jar;D:\jdk1.8.0_172\jre\lib\plugin.jar;D:\jdk1.8.0_172\jre\lib\resources.jar;D:\jdk1.8.0_172\jre\lib\rt.jar;E:\develop\spring\target\classes;C:\Users\Administrator\.m2\repository\org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar;C:\Users\Administrator\.m2\repository\com\mchange\c3p0\0.9.5.4\c3p0-0.9.5.4.jar;C:\Users\Administrator\.m2\repository\com\mchange\mchange-commons-java\0.2.15\mchange-commons-java-0.2.15.jar;C:\Users\Administrator\.m2\repository\com\zaxxer\HikariCP-java7\2.4.13\HikariCP-java7-2.4.13.jar;C:\Users\Administrator\.m2\repository\org\slf4j\slf4j-api\1.7.36\slf4j-api-1.7.36.jar;C:\Users\Administrator\.m2\repository\org\slf4j\slf4j-simple\1.7.36\slf4j-simple-1.7.36.jar com.example.SimpleTriggerMisfireExample
[main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
[main] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: main
[main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
[main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
[main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
[main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

[main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
[main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
调度器启动时间: Wed Mar 19 19:29:33 CST 2025
任务执行开始: Wed Mar 19 19:29:33 CST 2025, Trigger: group1.trigger1, 触发时间: Wed Mar 19 19:29:33 CST 2025
[main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
任务执行开始: Wed Mar 19 19:29:33 CST 2025, Trigger: group1.trigger2, 触发时间: Wed Mar 19 19:29:33 CST 2025
任务执行开始: Wed Mar 19 19:29:36 CST 2025, Trigger: group1.trigger2, 触发时间: Wed Mar 19 19:29:36 CST 2025
任务执行结束: Wed Mar 19 19:29:36 CST 2025
任务执行结束: Wed Mar 19 19:29:36 CST 2025
任务执行开始: Wed Mar 19 19:29:38 CST 2025, Trigger: group1.trigger1, 触发时间: Wed Mar 19 19:29:38 CST 2025
任务执行开始: Wed Mar 19 19:29:39 CST 2025, Trigger: group1.trigger2, 触发时间: Wed Mar 19 19:29:39 CST 2025
任务执行结束: Wed Mar 19 19:29:39 CST 2025
任务执行结束: Wed Mar 19 19:29:41 CST 2025
任务执行开始: Wed Mar 19 19:29:42 CST 2025, Trigger: group1.trigger2, 触发时间: Wed Mar 19 19:29:42 CST 2025
任务执行结束: Wed Mar 19 19:29:42 CST 2025
任务执行开始: Wed Mar 19 19:29:43 CST 2025, Trigger: group1.trigger1, 触发时间: Wed Mar 19 19:29:43 CST 2025
任务执行结束: Wed Mar 19 19:29:45 CST 2025
任务执行开始: Wed Mar 19 19:29:45 CST 2025, Trigger: group1.trigger2, 触发时间: Wed Mar 19 19:29:45 CST 2025
任务执行结束: Wed Mar 19 19:29:46 CST 2025

进程已结束,退出代码130

Scheduler

只要知道Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();就行

Trigger

Trigger 的触发器中主要是下面两种比较常用:

  1. SimpleTrigger:适用于单次或固定间隔重复执行的任务。
  2. CronTrigger:适用于复杂调度规则的任务。他也能和SimpleTrigger一样,指定开始执行和结束执行的时间。

SimpleTrigger适合场景:

  1. 定时任务:比如指定晚上12点去导出订单
  2. 延时任务:比如,当用户提交订单后,启动任务,并且延时30秒钟后检测订单状态,如果没有支付就取消订单并释放库存
  3. 循环任务:可以无限循环、可以指定循环次数、可以指定循环到某个时间停止

具体使用可以让GBT给出简单使用案例。

CronTrigger适合场景:

  1. 适合需要复杂调度规则的任务(例如每天、每周、每月执行、每个星期几执行、第几周执行)。
  2. 需要精确控制执行时间的任务。

Cron 表达式由 6 或 7 个字段组成(各字段使用空格分割,最后一个年份是可选的),格式如下:

java 复制代码
秒 分 时 日 月 周 年(可选)

cron表达式的规则:

  • 秒(0-59):表示每分钟的秒数。
  • 分(0-59):表示每小时的分钟数。
  • 时(0-23):表示每天的小时数。
  • 日(1-31):表示每月的日期。
  • 月(1-12 或 JAN-DEC):表示每年的月份。使用月份的前3个字母也是可以表示月份的。
  • 周(1-7 或 SUN-SAT):表示每周的星期几(1 表示周日,7 表示周六)。
  • 年(可选,1970-2099):表示年份(可以省略)。省略表示对年份没有要求,任何年份都可以执行,如果指定年份,那么久只能在对应的年份才能执行。

Cron 表达式中可以使用以下符号来定义时间的范围和规则:

  1. 星号(*)
    含义:表示"每"或"任意"。

示例:

0 * * * * ?:每分钟的第 0 秒执行(即每分钟执行一次)。

0 0 * * * ?:每小时的 0 分 0 秒执行(即每小时执行一次)。

  1. 问号(?)
    含义:表示"无特定值",它通常用于 "日"字段 或 "周"字段,以避免这两个字段之间的冲突。因为"日"和"周"字段是互斥的,不能同时指定具体的值,所以需要用 ? 来表示其中一个字段不指定具体值。

示例:

0 0 12 ? * MON:每周一的 12:00:00 执行。

0 0 0 1 * ?:每月 1 日的 00:00:00 执行。

  1. 逗号(,)
    含义:表示"或",用于列举多个值。

示例:

0 0 12,18 * * ?:每天的 12:00:00 和 18:00:00 执行。

0 0 0 1,15 * ?:每月 1 日和 15 日的 00:00:00 执行。

  1. 连字符(-)
    含义:表示"范围",用于定义一个连续的时间范围。

示例:

0 0 9-17 * * ?:每天 9:00:00 到 17:00:00,每小时执行一次。

0 0 0 1-5 * ?:每月 1 日到 5 日的 00:00:00 执行。

  1. 斜杠(/)
    含义:表示"间隔",用于定义时间间隔。

示例:

0 0/5 * * * ?:每 5 分钟执行一次(从 0 分钟开始)。

0 0 0/2 * * ?:每 2 小时执行一次(从 0 点开始)。

0 0 10/7 * * ?:从 10 点开始,每 7 个小时执行一次。过了范围后还是一样从10点开始的。

执行的时间如下:

  • 2025-03-13 10:00:00
  • 2025-03-13 17:00:00
  • 2025-03-14 10:00:00
  • 2025-03-14 17:00:00
  • 2025-03-15 10:00:00
  1. 井号(#)
    含义:表示"第几个",用于指定某月的第几个星期几。

示例:

0 0 12 ? * 6#3:每月的第 3 个星期五的 12:00:00 执行。

0 0 12 ? * 2#1:每月的第 1 个星期一的 12:00:00 执行。

  1. 字母(L、W)
    L:L加在时间上面,就是代表最后一个这个时间才会执行。比如,加在日上,就是最后一个满足条件的日才会执行。加在周上,就是最后一个满足条件的周才执行。允许使用在月、日、星期几上。如果在月、日、周上面写L,不写其他的,分别就是指满足条件的最后一个月、满足条件的最后一天、满足条件的星期六。

示例:

0 0 0 L * ?:每月的最后一天的 00:00:00 执行。

0 0 12 ? * 5L:每月的最后一个星期五的 12:00:00 执行。

0 0 12 L * ?:表示每月最后一天的12:00:00执行。

W:表示"工作日"(Weekday),用于"日"字段,指定离给定日期最近的工作日。

示例:

0 0 0 15W * ?:每月 15 日最近的工作日的 00:00:00 执行。

比如:当前是2025年3月12日

  • 2025-03-14 00:00:00(周五)
  • 2025-04-15 00:00:00(周二)
  • 2025-05-15 00:00:00(周四)
  • 2025-06-16 00:00:00(周一)
  • 2025-07-15 00:00:00(周二)

例如:

  1. 0 0 12 * * ?:每天中午 12 点执行。
  2. 0 0/5 14 * * ?:每天下午 2 点到 2:55,每 5 分钟执行一次。

使用案例:创建一个任务在每天的 12:00:00 执行。

java 复制代码
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class SimpleJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("SimpleJob is executing at: " + new java.util.Date());
    }
}
java 复制代码
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class CronTriggerExample {
    public static void main(String[] args) {
        try {
            // 1. 获取任务调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 2. 创建任务实例
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
                    .withIdentity("cronJob", "group1")
                    .build();

            // 3. 创建 CronTrigger
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("cronTrigger", "group1")
                    .withSchedule(CronScheduleBuilder.cronSchedule("0 0 12 * * ?")) // 每天中午 12:00:00 执行
                    .build();

            // 4. 把任务和触发器告诉任务调度器
            scheduler.scheduleJob(job, trigger);

            // 5. 使用任务调度器启动任务
            scheduler.start();

            // 让任务运行一段时间
            Thread.sleep(60000); // 60秒

            // 6. 使用任务调度器关闭任务
            scheduler.shutdown();

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

触发器的Misfire策略

misfire解释

Misfire(错过触发) 发生在触发器的计划时间已经过去,但由于某些原因(例如,调度器未启动、任务执行时间过长、系统负荷等),未能按预定时间执行触发器。这种情况通常是由于调度器(Scheduler)没有运行(可能是没有启动、暂停、关闭等原因)、线程池没有空闲的线程供调度器调度而产生的(调度器去执行任务的时候,其实是使用线程池中的线程去执行的)。如下图:

你可以理解为,Trigger创建时,你没有指定start就是指计划开始执行的时间是Trigger创建的时间,如果你指定了某个时间,就是计划了从你指定的开始时间开始什么时候去执行什么任务(注意,如果你创建trigger的时候,设置开始时间是过去的时间,那么你start这个Scheduler的时候,就可能就直接就会直接补偿了)。相当于是有一个计划表,如果当前时间超过了那个任务指定要执行的时间,但是又没有执行,那么就是misfire了,如果超过了,但是执行了那就是正常的。相当于在时间表上,打了✓,但是如果是misfire,那么就是相当于打×。Trigger会有补偿机制,会去补偿执行错过的任务,补偿机制是什么样的取决于你设置的misfire处理策略。

misfire发生的场景:

①:所有的工作线程都在忙碌,导致某些trigger得不到触发.(如:simplethreadpool 默认是10个工作线程,但我有15个trigger同时触发, 恰巧这10个trigger关联的job耗时都很长,剩下的5个trigger超过了等待时间仍然没有得到触发)

②:调度器(sheduler)中途挂了,某个时间又恢复了

③:设置的trigger的开始时间早于当前时间

Misfire 补偿

Quartz 为不同类型的触发器(如 SimpleTrigger 和 CronTrigger)提供了不同的 Misfire 处理策略。

我们主要使用的是SimpleTrigger 和 CronTrigger,所以我们只介绍这两个触发器的Misfire处理策略。

SimpleTrigger

略。这个我测试发现结果和很多博客、GBT的结果不一样,并且因为这个用的也没有很多,这里不记录了。主要了解CronTrigger 就行了。

CronTrigger

CronTrigger 提供了以下几种处理 Misfire 策略:

  • withMisfireHandlingInstructionDoNothing:错过的不进行补偿,然后正常调度。
  • withMisfireHandlingInstructionFireAndProceed:错过的全部合成一次,并且立即进行补偿(即时任务终止时间已经到达),然后正常调度。默认是这个。
  • withMisfireHandlingInstructionIgnoreMisfires:错过的全部立即进行补偿(即时任务终止时间已经到达),然后正常调度

代码:

java 复制代码
package com.example.springbootdemo.config;

import com.example.springbootdemo.job.MyJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfiguration {
    private String jobName="myjob";
    @Bean("myJobDetail")
    public JobDetail jobDetail(){
        return JobBuilder.newJob(MyJob.class)
                .storeDurably()
                .withIdentity(jobName)
                .build();
    }

    @Bean
    public Trigger trigger(){
        String cronExpression="0 0/2 * * * ?";
        CronScheduleBuilder cronScheduleBuilder=
                CronScheduleBuilder.cronSchedule(cronExpression)
                        .withMisfireHandlingInstructionDoNothing();
        return TriggerBuilder.newTrigger()
                .startAt(DateBuilder.todayAt(10,50,0))
                .withIdentity(jobName+"_trigger")
                .withSchedule(cronScheduleBuilder)
                .build();
    }

    @Bean
    public Scheduler scheduler(Trigger trigger, JobDetail jobDetail) throws SchedulerException {
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.scheduleJob(jobDetail, trigger);
        return scheduler;
    }
}
java 复制代码
package com.example.springbootdemo.job;

import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取任务关联的jobDetail
        JobDetail jobDetail = context.getJobDetail();
        // 获取任务的名字
        String jobName = jobDetail.getKey().getName();
        // 获取任务的本次执行时间
        Date fireTime = context.getFireTime();
        String executeTime = new SimpleDateFormat("yyyy-HW-dd HH:mm:ss").format(fireTime);
        // 输出任务的相关信息
        System.out.println("|---"+jobName+"---!");
        System.out.println("\t本次执行时间:"+executeTime);
    }
}
java 复制代码
package com.example.springbootdemo.run;

import org.quartz.Scheduler;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

// 在 Spring Boot 应用启动时,QuartzStartupRunner 的 run 方法会被自动调用。
// 在 run 方法中,scheduler.start() 会启动 Quartz 调度器。
// 一旦调度器启动,所有配置好的任务(Job)和触发器(Trigger)就会开始按照预定的时间表执行。
@Component
public class QuartzStartupRunner implements CommandLineRunner {

    private final Scheduler scheduler;

    public QuartzStartupRunner(Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    @Override
    public void run(String... args) throws Exception {
        scheduler.start();
    }
}
java 复制代码
package com.example.springbootdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemoApplication.class, args);
    }

}

Job、JobDetail和Scheduler

quartz中提供了一个Job接口,如果你想要有一个定时执行的任务,那么你就需要创建一个类,实现这个Job接口,并且实现其execute方法,execute方法中写要定时执行的逻辑。然后创建一个JobDetail,JobDetail创建的时候要指定Job实现类,你也可以指定一些其他的任务信息。然后创建一个Trigger,指定执行的时间。接着,把JobDetail和Trigger告诉调度器,让调度器去按你计划执行,这样就实现了一个定时执行的任务了。

Job只是记录做什么事情而已,不记录任何其他的,所以很单一。

Detail会绑定一个Job的class对象,相当于是把任务执行的逻辑告诉给JobDetail。当然JobDetail还会记录一些其他的任务信息,不只是记录任务的逻辑是什么,还记录其他的,比如任务叫什么名字,任务属于什么组,任务描述信息呀,等等。

JobDetail 只是一个描述 Job 的元数据对象,它不会直接调用 Job 的 execute 方法。也不会new一个Job实例。

真正会调用Job逻辑的东西其实是Scheduler,他会根据Trigger指定的时间去执行,即,到了该执行的时间,就会去创建Job,然后调用他的execute方法。默认情况下,Scheduler每次在Trigger指定的时间去执行Job的execute的时候,都是会创建一个新的Job对象去执行的。因为可能会有多个调度器会用一个Job的execute逻辑。

Job的状态

分类

Job 可以分为两种类型:有状态的 Job(Stateful Job) 和 无状态的 Job(Stateless Job)。它们的区别主要体现在任务执行过程中是否保留状态,以及是否支持并发执行。

  1. 无状态的 Job(Stateless Job)

无状态的 Job 是 Quartz 的默认行为。每次任务执行时,Quartz 都会创建一个新的 Job 实例,且不会保留任何状态。

特点:

  • 每次执行都是独立的:每次触发器触发时,Quartz 都会创建一个新的 Job 实例,任务执行完成后,实例会被销毁。
  • 支持并发执行:多个触发器可以同时触发同一个 Job,Quartz 会为每个触发器创建一个新的 Job 实例
  • 不保留状态:JobDataMap 中的数据在任务执行后不会被保留。即使你在 execute 方法中修改了 JobDataMap,并且把修改后的值put进去,这些修改也不会影响下一次任务的执行。

适用场景:

  • 任务是无状态的,不需要保留任何数据。
  • 任务可以并发执行,且不需要考虑线程安全问题。
  1. 有状态的 Job(Stateful Job)

有状态的 Job 会在任务执行过程中保留状态,且不支持并发执行。Quartz 通过 @PersistJobDataAfterExecution 和 @DisallowConcurrentExecution 注解来实现有状态的 Job。

特点:

  • 保留状态:任务执行过程中对 JobDataMap 的修改会被保留,并影响下一次任务的执行。
  • 不支持并发执行:同一时间只能有一个触发器执行该 Job,其他触发器需要等待当前任务执行完成后才能执行。
  • 单例行为:即使有多个触发器同时执行,Quartz 也会确保同一时间只有一个任务实例在执行。

适用场景:

  • 任务需要保留状态(例如,记录任务的执行次数或中间结果)。
  • 任务不能并发执行,需要确保线程安全。

例子:

java 复制代码
@PersistJobDataAfterExecution // 保留 JobDataMap 的状态
@DisallowConcurrentExecution  // 禁止并发执行
public class StatefulJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        int count = dataMap.getInt("count", 0);
        count++;
        dataMap.put("count", count); // 修改状态
        System.out.println("Stateful Job Executed! Count: " + count);
    }
}

如何选择无状态 Job 和有状态 Job?

  • 如果你的任务是独立的,不需要保留任何状态,且可以并发执行,使用无状态 Job。
  • 如果你的任务需要保留状态(例如,记录执行次数或中间结果),使用有状态 Job。

注意事项

  • 有状态 Job 的性能:由于有状态 Job 不支持并发执行,可能会影响任务的执行效率。如果任务执行时间较长,可能会导致其他触发器等待。
  • 线程安全:即使是有状态 Job,也需要确保 JobDataMap 中的数据类型是线程安全的(例如,使用 AtomicInteger 而不是普通的 int)。
  • 状态持久化:如果使用 @PersistJobDataAfterExecution,JobDataMap 的状态会被持久化到数据库中(如果配置了持久化存储)。确保数据库连接和配置正确。

参数传递

Job的抽象方法execute的参数是JobExecutionContext类型的,他是一个非常重要的对象,它提供了任务执行时的上下文信息。很重要的一个作用就是,他可以接收Trigger和JobDetail传递的参数。

你在Trigger或者JobDetail中可以把一些数据保存到JobDataMap中,然后具体执行execute方法的时候,execute中可以通过JobExecutionContext去获取到对应的JobDataMap数据,进行你需要的逻辑。

例子:

java 复制代码
package com.example;

import com.example.job.MyJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzExample {
    public static void main(String[] args) throws SchedulerException {
        // 创建调度器
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 启动调度器
        scheduler.start();

        // 定义 JobDetail,并设置 JobDataMap
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
        .withIdentity("myJob", "group1")
        .usingJobData("message", "Hello from JobDetail!") // JobDetail 中的参数
        .usingJobData("count", 0) // JobDetail 中的参数
        .build();

        // 定义 Trigger,并设置 JobDataMap
        Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("myTrigger", "group1")
        .startNow()
        .usingJobData("message", "Hello from Trigger!") // Trigger 中的参数
        .usingJobData("count", 100) // Trigger 中的参数
        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                      .withIntervalInSeconds(5) // 每 5 秒执行一次
                      .repeatForever())
        .build();

        // 将 JobDetail 和 Trigger 注册到调度器中
        scheduler.scheduleJob(jobDetail, trigger);
    }
}
java 复制代码
package com.example.job;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobDataMap;

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取 JobDetail 中的 JobDataMap
        JobDataMap jobDetailDataMap = context.getJobDetail().getJobDataMap();
        // 获取 Trigger 中的 JobDataMap
        JobDataMap triggerDataMap = context.getTrigger().getJobDataMap();
        // 获取合并后的 JobDataMap(Trigger 中的参数会覆盖 JobDetail 中的同名参数)
        JobDataMap mergedDataMap = context.getMergedJobDataMap();

        // 从 JobDetail 中获取参数
        String jobDetailMessage = jobDetailDataMap.getString("message");
        int jobDetailCount = jobDetailDataMap.getInt("count");

        // 从 Trigger 中获取参数
        String triggerMessage = triggerDataMap.getString("message");
        int triggerCount = triggerDataMap.getInt("count");

        // 从合并后的 JobDataMap 中获取参数
        String mergedMessage = mergedDataMap.getString("message");
        int mergedCount = mergedDataMap.getInt("count");

        // 打印参数
        System.out.println("JobDetail Message: " + jobDetailMessage);
        System.out.println("JobDetail Count: " + jobDetailCount);
        System.out.println("Trigger Message: " + triggerMessage);
        System.out.println("Trigger Count: " + triggerCount);
        System.out.println("Merged Message: " + mergedMessage);
        System.out.println("Merged Count: " + mergedCount);

        // 更新计数(如果需要)
        mergedCount++;
        mergedDataMap.put("count", mergedCount); // 更新合并后的 JobDataMap
    }
}

输出:

java 复制代码
"D:\1program file\Java\jdk1.8.0_231\bin\java.exe" "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2021.3.3\lib\idea_rt.jar=10177:D:\Program Files\JetBrains\IntelliJ IDEA 2021.3.3\bin" -Dfile.encoding=UTF-8 -classpath "D:\1program file\Java\jdk1.8.0_231\jre\lib\charsets.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\deploy.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\access-bridge-64.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\cldrdata.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\dnsns.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\jaccess.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\jfxrt.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\localedata.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\nashorn.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\sunec.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\sunjce_provider.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\sunmscapi.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\sunpkcs11.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\ext\zipfs.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\javaws.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\jce.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\jfr.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\jfxswt.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\jsse.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\management-agent.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\plugin.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\resources.jar;D:\1program file\Java\jdk1.8.0_231\jre\lib\rt.jar;E:\SpringBootDemo1\target\classes;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot-starter-web\2.5.0\spring-boot-starter-web-2.5.0.jar;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot-starter\2.5.0\spring-boot-starter-2.5.0.jar;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot\2.5.0\spring-boot-2.5.0.jar;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot-starter-logging\2.5.0\spring-boot-starter-logging-2.5.0.jar;E:\develop\maven_repository\maven_repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;E:\develop\maven_repository\maven_repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;E:\develop\maven_repository\maven_repository\org\apache\logging\log4j\log4j-to-slf4j\2.14.1\log4j-to-slf4j-2.14.1.jar;E:\develop\maven_repository\maven_repository\org\apache\logging\log4j\log4j-api\2.14.1\log4j-api-2.14.1.jar;E:\develop\maven_repository\maven_repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;E:\develop\maven_repository\maven_repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;E:\develop\maven_repository\maven_repository\org\yaml\snakeyaml\1.28\snakeyaml-1.28.jar;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot-starter-json\2.5.0\spring-boot-starter-json-2.5.0.jar;E:\develop\maven_repository\maven_repository\com\fasterxml\jackson\core\jackson-databind\2.12.3\jackson-databind-2.12.3.jar;E:\develop\maven_repository\maven_repository\com\fasterxml\jackson\core\jackson-annotations\2.12.3\jackson-annotations-2.12.3.jar;E:\develop\maven_repository\maven_repository\com\fasterxml\jackson\core\jackson-core\2.12.3\jackson-core-2.12.3.jar;E:\develop\maven_repository\maven_repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.12.3\jackson-datatype-jdk8-2.12.3.jar;E:\develop\maven_repository\maven_repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.12.3\jackson-datatype-jsr310-2.12.3.jar;E:\develop\maven_repository\maven_repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.12.3\jackson-module-parameter-names-2.12.3.jar;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot-starter-tomcat\2.5.0\spring-boot-starter-tomcat-2.5.0.jar;E:\develop\maven_repository\maven_repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.46\tomcat-embed-core-9.0.46.jar;E:\develop\maven_repository\maven_repository\org\apache\tomcat\embed\tomcat-embed-el\9.0.46\tomcat-embed-el-9.0.46.jar;E:\develop\maven_repository\maven_repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.46\tomcat-embed-websocket-9.0.46.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-web\5.3.7\spring-web-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-beans\5.3.7\spring-beans-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-webmvc\5.3.7\spring-webmvc-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-aop\5.3.7\spring-aop-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-context\5.3.7\spring-context-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-expression\5.3.7\spring-expression-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar;E:\develop\maven_repository\maven_repository\com\mchange\mchange-commons-java\0.2.15\mchange-commons-java-0.2.15.jar;E:\develop\maven_repository\maven_repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-core\5.3.7\spring-core-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-jcl\5.3.7\spring-jcl-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\projectlombok\lombok\1.18.20\lombok-1.18.20.jar;E:\develop\maven_repository\maven_repository\com\baomidou\mybatis-plus-boot-starter\3.3.1\mybatis-plus-boot-starter-3.3.1.jar;E:\develop\maven_repository\maven_repository\com\baomidou\mybatis-plus\3.3.1\mybatis-plus-3.3.1.jar;E:\develop\maven_repository\maven_repository\com\baomidou\mybatis-plus-extension\3.3.1\mybatis-plus-extension-3.3.1.jar;E:\develop\maven_repository\maven_repository\com\baomidou\mybatis-plus-core\3.3.1\mybatis-plus-core-3.3.1.jar;E:\develop\maven_repository\maven_repository\com\baomidou\mybatis-plus-annotation\3.3.1\mybatis-plus-annotation-3.3.1.jar;E:\develop\maven_repository\maven_repository\com\github\jsqlparser\jsqlparser\3.1\jsqlparser-3.1.jar;E:\develop\maven_repository\maven_repository\org\mybatis\mybatis\3.5.3\mybatis-3.5.3.jar;E:\develop\maven_repository\maven_repository\org\mybatis\mybatis-spring\2.0.3\mybatis-spring-2.0.3.jar;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot-autoconfigure\2.5.0\spring-boot-autoconfigure-2.5.0.jar;E:\develop\maven_repository\maven_repository\org\springframework\boot\spring-boot-starter-jdbc\2.5.0\spring-boot-starter-jdbc-2.5.0.jar;E:\develop\maven_repository\maven_repository\com\zaxxer\HikariCP\4.0.3\HikariCP-4.0.3.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-jdbc\5.3.7\spring-jdbc-5.3.7.jar;E:\develop\maven_repository\maven_repository\org\springframework\spring-tx\5.3.7\spring-tx-5.3.7.jar" com.example.QuartzExample
22:29:49.528 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
22:29:49.539 [main] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: main
22:29:49.559 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
22:29:49.559 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
22:29:49.559 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
22:29:49.559 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

22:29:49.559 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
22:29:49.559 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
22:29:49.561 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
22:29:49.561 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers
22:29:49.570 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
22:29:49.571 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=com.example.job.MyJob
22:29:49.571 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
22:29:49.571 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
JobDetail Message: Hello from JobDetail!
JobDetail Count: 0
Trigger Message: Hello from Trigger!
Trigger Count: 100
Merged Message: Hello from Trigger!
Merged Count: 100
22:29:54.570 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=com.example.job.MyJob
22:29:54.570 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
22:29:54.570 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
JobDetail Message: Hello from JobDetail!
JobDetail Count: 0
Trigger Message: Hello from Trigger!
Trigger Count: 100
Merged Message: Hello from Trigger!
Merged Count: 100
22:29:59.579 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=com.example.job.MyJob
22:29:59.579 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
22:29:59.579 [DefaultQuartzScheduler_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
JobDetail Message: Hello from JobDetail!
JobDetail Count: 0
Trigger Message: Hello from Trigger!
Trigger Count: 100
Merged Message: Hello from Trigger!
Merged Count: 100
22:30:04.563 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=com.example.job.MyJob
22:30:04.563 [DefaultQuartzScheduler_Worker-4] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
JobDetail Message: Hello from JobDetail!
JobDetail Count: 0
Trigger Message: Hello from Trigger!
Trigger Count: 100
Merged Message: Hello from Trigger!
Merged Count: 100
22:30:04.563 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
22:30:09.565 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=com.example.job.MyJob
22:30:09.566 [DefaultQuartzScheduler_Worker-5] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
JobDetail Message: Hello from JobDetail!
JobDetail Count: 0
Trigger Message: Hello from Trigger!
Trigger Count: 100
Merged Message: Hello from Trigger!
Merged Count: 100
22:30:09.566 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
22:30:14.572 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=com.example.job.MyJob
22:30:14.573 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
22:30:14.573 [DefaultQuartzScheduler_Worker-6] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
JobDetail Message: Hello from JobDetail!
JobDetail Count: 0
Trigger Message: Hello from Trigger!
Trigger Count: 100
Merged Message: Hello from Trigger!
Merged Count: 100
......

可以看到一些其他信息:

quartz默认使用的是SimpleThreadPool,并且线程为10个。RAMJobStore 是 Quartz 默认的 Job 存储实现,它将所有的调度数据(如 JobDetail、Trigger、调度状态等)存储在内存中。RAMJobStore 不会将调度数据持久化到磁盘或数据库中。所有的数据都存储在内存中,一旦应用程序停止或崩溃,数据就会丢失。存在内存的好处是快。存在内存容易丢失,断电或者重启程序就丢失job、trigger等信息了,并且,内存存储的方式无法做集群部署的定时任务,只能做简单的单机应用的定时任务。如果需要quartz支持持久化或者集群,那么需要将 JobStore 切换为 JDBCJobStore。

从输出结果可以看到,Merged Count 的值始终是 100,没有递增。这是因为:默认情况下,Quartz 的 Job 是无状态的,每次执行时都会创建一个新的 Job 实例。对 JobDataMap 的修改不会影响下一次任务的执行。

监听器

监听器可以用于监控任务的执行、触发器的触发、调度器的启动和关闭等事件。Quartz 提供了三种监听器JobListener(任务监听器)、TriggerListener(触发器监听器)、 SchedulerListener(调度器监听器)

JobListener

用于监听与 Job 相关的事件,例如:

  • 任务执行前触发(jobToBeExecuted)。
  • 任务执行完成时触发(jobWasExecuted)。
  • 任务执行失效时触发(jobExecutionVetoed)。

使用例子:

java 复制代码
package com.example.listeners;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;

public class MyJobListener implements JobListener {
    @Override
    public String getName() {
        return "MyJobListener"; // 监听器的名称
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println("Job is about to be executed: " + context.getJobDetail().getKey());
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println("Job execution was vetoed: " + context.getJobDetail().getKey());
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println("Job was executed: " + context.getJobDetail().getKey());
        if (jobException != null) {
            System.out.println("Job execution failed with exception: " + jobException.getMessage());
        }
    }
}
java 复制代码
package com.example;

import com.example.job.SimpleJob;
import com.example.listeners.MyJobListener;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;

public class CronTriggerExample {
    public static void main(String[] args) {
        try {
            // 1. 获取任务调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 2. 创建任务实例
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
            .withIdentity("cronJob", "group1")
            .build();

            // 3. 创建 CronTrigger
            Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity("cronTrigger", "group1")
            .withSchedule(CronScheduleBuilder.cronSchedule("0 11 23 * * ?"))
            .build();

            // 4. 把任务和触发器告诉任务调度器
            scheduler.scheduleJob(job, trigger);

            // 5. 注册任务监听器
            //            scheduler.getListenerManager().addJobListener(new MyJobListener());// (监听当前调度器上的所有任务)
            //            scheduler.getListenerManager().addJobListener(new MyJobListener(), GroupMatcher.jobGroupEquals("group1"));// (只监听group1组的Job任务)
            scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(new JobKey("cronJob", "group1")));// (只监听group1组中叫cronJob的Job任务)

            // 6. 使用任务调度器启动任务
            scheduler.start();

            // 让任务运行一段时间
            Thread.sleep(60000); // 60秒

            // 7. 使用任务调度器关闭任务
            scheduler.shutdown();

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

注意:一个任务调度器Scheduler是可以绑定多个任务的,所以,当我们给Scheduler中注册监听器的时候,要说明是监听这个调度器所有的任务,还是只监听某个组的任务,还是说只监听某个key的任务。

TriggerListener

用于监听与 Trigger 相关的事件,例如:

  • 触发器触发时触发(triggerFired)。
  • 触发器触发完成时触发(triggerComplete)。
  • 触发器错过触发时触发(triggerMisfired)。
  • 触发器执行失效时触发(vetoJobExecution)。

例子:

java 复制代码
package com.example.listeners;

import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.TriggerListener;

public class MyTriggerListener implements TriggerListener {
    @Override
    public String getName() {
        return "MyTriggerListener"; // 监听器的名称
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        System.out.println("Trigger fired: " + trigger.getKey());
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        System.out.println("Checking if job execution should be vetoed for trigger: " + trigger.getKey());
        return false; // 返回 true 表示否决任务执行
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        System.out.println("Trigger misfired: " + trigger.getKey());
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext jobExecutionContext, Trigger.CompletedExecutionInstruction completedExecutionInstruction) {
        System.out.println("Trigger completed: " + trigger.getKey());
    }

}
java 复制代码
package com.example;

import com.example.job.SimpleJob;
import com.example.listeners.MyJobListener;
import com.example.listeners.MyTriggerListener;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;

public class CronTriggerExample {
    public static void main(String[] args) {
        try {
            // 1. 获取任务调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 2. 创建任务实例
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
            .withIdentity("cronJob", "group1")
            .build();

            // 3. 创建 CronTrigger
            Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity("cronTrigger", "group1")
            .withSchedule(CronScheduleBuilder.cronSchedule("0 11 23 * * ?"))
            .build();

            // 4. 把任务和触发器告诉任务调度器
            scheduler.scheduleJob(job, trigger);

            // 5. 注册任务监听器
            //            scheduler.getListenerManager().addJobListener(new MyJobListener());// (监听当前调度器上的所有任务)
            //            scheduler.getListenerManager().addJobListener(new MyJobListener(), GroupMatcher.jobGroupEquals("group1"));// (只监听group1组的Job任务)
            //            scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(new JobKey("cronJob", "group1")));// (只监听group1组中叫cronJob的Job任务)

            // 5. 注册触发器监听器
            scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());// 注册触发器监听器

            // 6. 使用任务调度器启动任务
            scheduler.start();

            // 让任务运行一段时间
            Thread.sleep(60000); // 60秒

            // 7. 使用任务调度器关闭任务
            scheduler.shutdown();

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

SchedulerListener

用于监听与 Scheduler 相关的事件,例如:

  • 调度器启动时(schedulerStarted)。
  • 调度器关闭时(schedulerShutdown)。
  • 任务或触发器被添加、删除、暂停、恢复时。

例子:

java 复制代码
package com.example.listeners;

import org.quartz.*;

public class MySchedulerListener implements SchedulerListener {

    @Override
    public void jobScheduled(Trigger trigger) {
        System.out.println("Job scheduled: " + trigger.getJobKey());
    }

    @Override
    public void jobUnscheduled(TriggerKey triggerKey) {
        System.out.println("Job unscheduled: " + triggerKey);
    }

    @Override
    public void triggerFinalized(Trigger trigger) {
        System.out.println("Trigger finalized: " + trigger.getKey());
    }

    @Override
    public void triggerPaused(TriggerKey triggerKey) {
        System.out.println("Trigger paused: " + triggerKey);
    }

    @Override
    public void triggersPaused(String triggerGroup) {
        System.out.println("Triggers paused in group: " + triggerGroup);
    }

    @Override
    public void triggerResumed(TriggerKey triggerKey) {
        System.out.println("Trigger resumed: " + triggerKey);
    }

    @Override
    public void triggersResumed(String triggerGroup) {
        System.out.println("Triggers resumed in group: " + triggerGroup);
    }

    @Override
    public void jobAdded(JobDetail jobDetail) {
        System.out.println("Job added: " + jobDetail.getKey());
    }

    @Override
    public void jobDeleted(JobKey jobKey) {
        System.out.println("Job deleted: " + jobKey);
    }

    @Override
    public void jobPaused(JobKey jobKey) {
        System.out.println("Job paused: " + jobKey);
    }

    @Override
    public void jobsPaused(String jobGroup) {
        System.out.println("Jobs paused in group: " + jobGroup);
    }

    @Override
    public void jobResumed(JobKey jobKey) {
        System.out.println("Job resumed: " + jobKey);
    }

    @Override
    public void jobsResumed(String jobGroup) {
        System.out.println("Jobs resumed in group: " + jobGroup);
    }

    @Override
    public void schedulerError(String msg, SchedulerException cause) {
        System.out.println("Scheduler error: " + msg);
        cause.printStackTrace();
    }

    @Override
    public void schedulerInStandbyMode() {
        System.out.println("Scheduler in standby mode");
    }

    @Override
    public void schedulerStarted() {
        System.out.println("Scheduler started");
    }

    @Override
    public void schedulerStarting() {
        System.out.println("Scheduler starting");
    }

    @Override
    public void schedulerShutdown() {
        System.out.println("Scheduler shutdown");
    }

    @Override
    public void schedulerShuttingdown() {
        System.out.println("Scheduler shutting down");
    }

    @Override
    public void schedulingDataCleared() {
        System.out.println("Scheduling data cleared");
    }
}
java 复制代码
package com.example;

import com.example.job.SimpleJob;
import com.example.listeners.MyJobListener;
import com.example.listeners.MySchedulerListener;
import com.example.listeners.MyTriggerListener;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.KeyMatcher;

public class CronTriggerExample {
    public static void main(String[] args) {
        try {
            // 1. 获取任务调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 2. 创建任务实例
            JobDetail job = JobBuilder.newJob(SimpleJob.class)
            .withIdentity("cronJob", "group1")
            .build();

            // 3. 创建 CronTrigger
            Trigger trigger = TriggerBuilder.newTrigger()
            .withIdentity("cronTrigger", "group1")
            .withSchedule(CronScheduleBuilder.cronSchedule("0 20 23 * * ?"))
            .build();

            // 4. 把任务和触发器告诉任务调度器
            scheduler.scheduleJob(job, trigger);

            // 5. 注册任务监听器
            //            scheduler.getListenerManager().addJobListener(new MyJobListener());// (监听当前调度器上的所有任务)
            //            scheduler.getListenerManager().addJobListener(new MyJobListener(), GroupMatcher.jobGroupEquals("group1"));// (只监听group1组的Job任务)
            //            scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(new JobKey("cronJob", "group1")));// (只监听group1组中叫cronJob的Job任务)

            // 5. 注册触发器监听器
            //            scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());// 注册触发器监听器

            // 5. 注册调度器监听器
            scheduler.getListenerManager().addSchedulerListener(new MySchedulerListener());// 注册调度器监听器

            // 6. 使用任务调度器启动任务
            scheduler.start();

            // 让任务运行一段时间
            Thread.sleep(60000); // 60秒

            // 7. 使用任务调度器关闭任务
            scheduler.shutdown();

        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

其实也是和前面两个类似的。

Quartz存储及持久化

JobStore

JobStore负责存储调度器的工作数据(Job、Trigger、JobDataMap),通俗地讲,就是JobStore中存什么,调度器就会执行什么。

其实,Quartz是先把数据存到JobStore中,然后再使用Scheduler去JobStore中拿信息进行调度的。即,Scheduler 接收到 JobDetail 和 Trigger 后,会将它们的相关数据存储到 JobStore 中。scheduler.scheduleJob(job, trigger);这个语句会把相关数据存储到 JobStore 中。Scheduler 启动后(scheduler.start()),它会从 JobStore 中读取任务和触发器的数据,按预定计划进行执行。

Quartz默认是存储到内存的,所以可能会出现下面这个问题:比如我们要执行100次任务,当执行了40次时系统崩溃了,系统重启时任务的执行计数器默认会从0开始。虽然这种策略能够满足多数业务场景需求。但是在某个特定的场景中我们需要继续之前的任务执行,我们可以通过对Jobstore进行持久化来实现。

Quartz提供了两种作业存储方式:

  • RAMJobStore:基于内存,重启服务器会丢失数据,但是速度很快。不适合集群部署的情况。
  • JDBCJobStore:基于数据库,重启服务器不会丢失数据,速度会比内存慢一点。适合集群部署的情况。

注意:如果集群部署应用,那么你就必须使用JDBCJobStore了,因为,如果多个地方部署Quartz的应用,那么就会有多个调度器了,并且如果都使用内存保存JobStore信息,那么,一个任务可能会被多次执行。但是,如果你多个应用的多个调度器使用一个DB,保存JobStore信息,那么执行就不会重复了,因为执行的信息保存到一个地方,A调度器执行了,告诉数据库,B调度器去执行前,先看数据库,就知道这个任务被A执行了,所以B不会再执行了,这样就不会进行多次执行同一个任务了。

要Quartz使用JDBCJobStore,那么需要自己进行额外的配置的,因为默认是使用RAMJobStore的。集群部署时,需要在多个应用的调度器中配置JDBCJobStore时,使用同一个数据库就行了。

Job主要有volatility、durability两个属性。其中,volatility表示任务是否被持久化,durability表示再没有trigger关联的时候,任务是否被保留。

相关推荐
某不知名網友2 分钟前
linux_进程地址空间(虚拟地址空间)
java·linux·算法
每次的天空25 分钟前
移动应用开发:自定义 View 处理大量数据的性能与交互优化方案
android·java·学习·交互
纪元A梦28 分钟前
贪心算法应用:最小反馈顶点集问题详解
java·算法·贪心算法
九转苍翎1 小时前
Java SE(10)——抽象类&接口
java
明月与玄武1 小时前
Spring Boot中的拦截器!
java·spring boot·后端
矢鱼1 小时前
单调栈模版型题目(3)
java·开发语言
n33(NK)1 小时前
Java中的内部类详解
java·开发语言
为美好的生活献上中指1 小时前
java每日精进 5.07【框架之数据权限】
java·开发语言·mysql·spring·spring cloud·数据权限
菲兹园长1 小时前
SpringBoot统一功能处理
java·spring boot·后端
一刀到底2112 小时前
java 多核,多线程,分布式 并发编程的现状 :从本身的jdk ,到 spring ,到其它第三方。
java·分布式·高并发