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关联的时候,任务是否被保留。

相关推荐
H309192 分钟前
设计模式-生成器模式
android·java·设计模式
不修×蝙蝠4 分钟前
SpringBoot 第二课(Ⅰ) 整合springmvc(详解)
java·spring boot·后端·spring·整合springmvc
守护者17030 分钟前
JAVA学习-练习试用Java实现“编写一个Spark程序,结合Elasticsearch对大数据进行全文搜索和筛选“
java·学习
江沉晚呤时39 分钟前
深入解析 .NET Core 垃圾回收(GC):概念、工作原理与优化策略
java·jvm·算法·c#·asp.net·.netcore·net
爱的叹息1 小时前
java TCP UDP 客户端访问例子和对比差异
java·tcp/ip·udp
江沉晚呤时1 小时前
C# 事件机制详解:定义、订阅、触发与应用实践
java·前端·c#·.netcore
WanderInk1 小时前
深入理解 JWT 中 Claims 的设计及其合理性
java·后端
yyyyyyykk2 小时前
Java线程
java·开发语言
爱喝热水的呀哈喽2 小时前
Java继承与反思,单例模式与静态的思考
java·python·单例模式
糖心何包蛋爱编程2 小时前
(六)Reactive-Stream 响应式流
java·开发语言·响应式编程·干货分享