杰瑞科技汇

Java Quartz定时器如何配置与使用?

这篇教程将从以下几个方面展开,让你对 Quartz 有一个从入门到精通的完整理解:

Java Quartz定时器如何配置与使用?-图1
(图片来源网络,侵删)
  1. 什么是 Quartz?
  2. 核心概念 (Job, Trigger, Scheduler)
  3. 快速入门:第一个 Quartz 程序
  4. 进阶:使用 Cron 表达式
  5. JobDataMap:传递数据给 Job
  6. Job 的持久化与集群
  7. Spring/Spring Boot 集成
  8. 总结与最佳实践

什么是 Quartz?

Quartz 是一个功能强大、开源、轻量级的作业调度框架,它完全由 Java 编写,它可以与任何 Java 应用程序(从最小的独立应用到最大的电子商务系统)集成,用于执行定时任务。

主要特点:

  • 强大的调度功能:可以定义非常复杂的调度规则,每周五下午 3 点”或“每个月的最后一天”。
  • 灵活的 Trigger:通过 Trigger(触发器)来控制 Job 的执行时间。
  • 持久化:可以将 Job 和 Trigger 的信息保存到数据库中,实现任务的持久化和集群部署。
  • 集群支持:通过数据库锁机制,可以搭建 Quartz 集群,确保高可用性和负载均衡。
  • 集成性:可以非常方便地与 Spring、Spring Boot 等主流框架集成。

核心概念

理解 Quartz 的三个核心概念是掌握它的关键:

1 Job (任务)

Job 是一个接口,代表一个被调度的任务,你只需要实现这个接口,并在 execute() 方法中编写你的业务逻辑即可。

Java Quartz定时器如何配置与使用?-图2
(图片来源网络,侵删)
public interface Job {
    void execute(JobExecutionContext context) throws JobExecutionException;
}

示例:一个简单的打印 "Hello, World!" 的 Job

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取当前时间
        Date date = new Date();
        // 打印 Hello World 和当前时间
        System.out.println("Hello World! - " + date);
    }
}

2 Trigger (触发器)

Trigger 用于定义 Job 的执行调度规则,它决定了 Job 何时 以及 如何 被执行。

Quartz 提供了两种主要的 Trigger:

  1. SimpleTrigger:用于简单的调度,在指定的时间点执行一次,或者在指定的时间间隔内重复执行 N 次。

    Java Quartz定时器如何配置与使用?-图3
    (图片来源网络,侵删)
    • startTime: 开始时间
    • endTime: 结束时间
    • repeatInterval: 重复间隔(毫秒)
    • repeatCount: 重复次数
  2. CronTrigger:功能更加强大,使用 Cron 表达式来定义复杂的调度规则,这是最常用的一种 Trigger。

    • cronExpression: Cron 表达式,如 "0/5 * * * * ?" 表示每 5 秒执行一次。

3 Scheduler (调度器)

Scheduler 是 Quartz 的核心控制器,它负责将 JobTrigger 绑定在一起,并根据 Trigger 的定义来执行 Job。

你可以将 Scheduler 想象成一个工厂车间,Job 是需要完成的工作,Trigger 是工作排班表,而 Scheduler 就是根据排班表来指挥和执行工作的车间主任。


快速入门:第一个 Quartz 程序

下面我们通过一个简单的例子,来演示如何创建一个 Job,并用 SimpleTrigger 来调度它。

步骤:

  1. 添加 Maven 依赖

    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.3.2</version> <!-- 建议使用较新稳定版本 -->
    </dependency>
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz-jobs</artifactId>
        <version>2.3.2</version>
    </dependency>
  2. 创建 Job 类

    就是上面提到的 HelloJob.java

  3. 编写主程序来调度 Job

    import org.quartz.*;
    import org.quartz.impl.StdSchedulerFactory;
    public class QuartzSimpleDemo {
        public static void main(String[] args) throws SchedulerException {
            // 1. 创建一个 SchedulerFactory(调度器工厂)
            SchedulerFactory schedulerFactory = new StdSchedulerFactory();
            // 2. 从工厂中获取一个 Scheduler 实例(调度器)
            Scheduler scheduler = schedulerFactory.getScheduler();
            // 3. 创建 JobDetail(任务详情)
            // JobDetail 是 Job 的详细信息,包括 Job 的类名、组名等
            // 注意:这里使用 JobBuilder.newJob(HelloJob.class) 创建,Quartz 会在执行时通过反射创建 HelloJob 的实例
            JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
                    .withIdentity("job1", "group1") // 设置 Job 的名称和组名
                    .build();
            // 4. 创建 Trigger(触发器)
            // 设置立即开始,然后每隔 2 秒执行一次,重复 5 次
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1", "group1") // 设置 Trigger 的名称和组名
                    .startNow() // 立即开始
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                            .withIntervalInSeconds(2) // 每 2 秒执行一次
                            .withRepeatCount(5)) // 重复 5 次 (总共执行 6 次)
                    .build();
            // 5. 将 Job 和 Trigger 绑定到 Scheduler 中,并启动调度器
            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.start();
            // 主线程休眠,让调度器有足够的时间执行任务
            try {
                Thread.sleep(20000); // 休眠 20 秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 关闭调度器
            scheduler.shutdown();
        }
    }

运行结果:

你会看到控制台每隔 2 秒打印一次 "Hello World!",共打印 6 次(包括立即执行的那一次),然后程序结束。


进阶:使用 Cron 表达式

Cron 表达式是 Quartz 最强大的功能,它定义了一套时间匹配规则。

1 Cron 表达式格式

一个标准的 Cron 表达式由 6-7 个子表达式组成,中间用空格隔开:

[秒] [分] [小时] [日] [月] [周] [年]

字段说明:

字段 允许值 允许的特殊字符
0-59
0-59
小时 0-23
1-31 , - * ? / L W
1-12 或 JAN-DEC
1-7 或 SUN-SAT , - * ? / L #
年 (可选) 1970-2099

特殊字符含义:

  • (星号):代表所有可能的值。 在“小时”字段表示“每小时”。
  • (逗号):用于列出多个值。MON, WED, FRI 在“周”字段表示“周一、周三、周五”。
  • (连字符):用于定义一个范围。10-12 在“小时”字段表示“10点到12点”。
  • (斜杠):用于指定一个步长。0/15 在“分”字段表示“从0分开始,每15分钟一次”;*/5 在“秒”字段表示“每5秒一次”。
  • (问号):仅用于“日”和“周”字段,表示“不指定值”,这两个字段互斥,必须有一个为 。
  • L (Last):表示“。L 在“日”字段表示“当月的最后一天”;L 在“周”字段表示“周六(一周的最后一天)”。
  • W (Weekday):表示“最近的工作日”。15W 在“日”字段表示“离15号最近的工作日”。
  • (Hash):表示“第几个”。6#3 在“周”字段表示“每个月的第三个周五”。

2 Cron 表达式示例

表达式 说明
0 0 12 * * ? 每天中午12点触发
0 15 10 ? * * 每天10:15触发
0 15 10 * * ? 每天10:15触发
0 15 10 * * ? * 每天10:15触发 (带年)
0 15 10 * * ? 2025 2025年每天10:15触发
0 * 14 * * ? 每天下午2点到2点59分之间的每分钟触发
0 0/5 14 * * ? 每天下午2点到2点55分之间的每5分钟触发
0 0/5 14,18 * * ? 每天下午2点到2点55分和下午6点到6点55分之间的每5分钟触发
0 0-5 14 * * ? 每天下午2点到2点05分之间的每分钟触发
0 10,44 14 ? 3 WED 每年三月的周三下午2:10和2:44触发
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 15 10 15 * ? 每月15日上午10:15触发
0 15 10 L * ? 每月最后一天的上午10:15触发
0 15 10 ? * 6L 每月最后一个周五的上午10:15触发
0 15 10 ? * 6L 2025-2025 2025年至2025年每月最后一个周五的上午10:15触发
0 15 10 ? * 6#3 每月第三个周五的上午10:15触发

3 使用 CronTrigger

将之前的 Quick Start 代码中的 SimpleTrigger 替换为 CronTrigger

// ... (前面的 JobDetail 创建代码相同)
// 4. 创建 CronTrigger
// 设置 cron 表达式:每 5 秒执行一次
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("cronTrigger1", "group1")
        .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
        .build();
// ... (后面的绑定和启动代码相同)

JobDataMap:传递数据给 Job

有时候我们需要在调度任务时,向 Job 传递一些参数,Quartz 提供了 JobDataMap 来实现这个功能。

JobDataMapjava.util.Map 的一个实现,它可以在 SchedulerJobDetailTrigger 之间共享数据。

通过 JobDetail 传递

// 在主程序中
JobDetail jobDetail = JobBuilder.newJob(DumbJob.class)
        .withIdentity("dumbJob", "group1")
        .usingJobData("jobSays", "Hello World!") // 传递键值对
        .usingJobData("myFloatValue", 3.14f)      // 传递另一个键值对
        .build();
// 在 Job 类中接收
public class DumbJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 从 JobExecutionContext 中获取 JobDataMap
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
        String jobSays = dataMap.getString("jobSays");
        float myFloatValue = dataMap.getFloat("myFloatValue");
        System.out.println("Job Says: " + jobSays);
        System.out.println("myFloatValue: " + myFloatValue);
    }
}

通过 Trigger 传递

JobDetailTrigger 中有同名的 key,Trigger 中的值会覆盖 JobDetail 中的值。

// 在主程序中
// JobDetail 中的数据
JobDetail jobDetail = JobBuilder.newJob(DumbJob.class)
        .withIdentity("dumbJob", "group1")
        .usingJobData("jobSays", "Hello from JobDetail")
        .build();
// Trigger 中的数据
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("myTrigger", "group1")
        .usingJobData("jobSays", "Hello from Trigger!") // 同名 key
        .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
        .build();
// 在 Job 类中执行时,会获取到 Trigger 中的值
// 输出: Job Says: Hello from Trigger!

Job 的持久化与集群

默认情况下,Quartz 将 Job 和 Trigger 的信息保存在内存中,这种方式简单高效,但存在一个严重问题:如果应用程序重启,所有的定时任务都会丢失

为了解决这个问题,Quartz 支持将数据持久化到数据库中。

1 持久化步骤

  1. 创建 Quartz 数据库表 Quartz 官方提供了各种数据库的 SQL 脚本,你可以在其官方仓库中找到:Quartz Database Tables 你需要根据你使用的数据库(如 MySQL, Oracle, PostgreSQL 等)选择对应的脚本并执行。

  2. 配置 Scheduler 在创建 SchedulerFactory 时,需要指定一个配置文件,告诉 Quartz 使用数据库作为 JobStore。

    quartz.properties 文件内容示例:

    # 实例化 Scheduler 时使用的属性
    org.quartz.scheduler.instanceName = MyScheduler
    org.quartz.scheduler.instanceId = AUTO
    # 使用 JobStoreTX,表示使用 JDBC 事务
    org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
    # 数据库驱动
    org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    # 数据库连接 URL
    org.quartz.jobStore.dataSource = myDS
    # 表名前缀,防止与现有表冲突
    org.quartz.jobStore.tablePrefix = QRTZ_
    # 是否是集群模式
    org.quartz.jobStore.isClustered = true
    # 定义 DataSource
    org.quartz.dataSource.myDS.connectionProvider.driver = com.mysql.cj.jdbc.Driver
    org.quartz.dataSource.myDS.connectionProvider.URL = jdbc:mysql://localhost:3306/quartz_db?useSSL=false&serverTimezone=UTC
    org.quartz.dataSource.myDS.connectionProvider.user = root
    org.quartz.dataSource.myDS.connectionProvider.password = your_password
  3. 使用配置文件 在 Java 代码中,StdSchedulerFactory 会自动在 classpath 下查找 quartz.properties 文件。

    // 不再需要手动配置,SchedulerFactory 会加载 quartz.properties
    SchedulerFactory schedulerFactory = new StdSchedulerFactory();
    Scheduler scheduler = schedulerFactory.getScheduler();

2 集群原理

isClustered = true 时,Quartz 会启动集群模式,其工作原理如下:

  1. 数据库锁:每个节点(JVM)在执行 Job 之前,都需要在数据库的 LOCKS 表中获取一个锁。
  2. 非阻塞获取:获取锁的过程是非阻塞的,一个节点成功获取锁后,其他节点会跳过这个 Job 的本次执行。
  3. 故障转移:如果一个节点宕机,它持有的锁会在超时后自动释放,其他健康的节点可以获取到锁,从而继续执行任务,实现了故障转移。

集群注意事项:

  • 必须共享数据库:所有节点必须连接到同一个数据库实例。
  • 时钟同步:集群中所有服务器的时钟必须尽量同步,否则可能导致调度混乱。
  • 不要使用 @DisallowConcurrentExecution:在集群模式下,如果一个 Job 的执行时间很长,超过了其下次执行时间,集群中的其他节点可能会再次触发同一个 Job,导致并发执行。@DisallowConcurrentExecution 只能保证单个 JVM 内不并发,无法保证集群环境。

Spring/Spring Boot 集成

在现代应用中,最常见的方式是通过 Spring 或 Spring Boot 来集成 Quartz。

1 Spring Boot 集成 (推荐)

Spring Boot 提供了 spring-boot-starter-quartz,使得集成变得异常简单。

步骤:

  1. 添加依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
  2. 创建 Job 类

    直接创建一个 Bean 即可,Spring Boot 会自动检测并注册它。

    import org.quartz.Job;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.springframework.stereotype.Component;
    @Component // 标记为 Spring Bean
    public class MySpringJob implements Job {
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            System.out.println("Spring Boot Quartz Job is running at: " + new Date());
            // 你可以在这里注入任何 Spring Bean
            //  @Autowired private SomeService someService;
        }
    }
  3. 配置定时任务

    你可以通过配置类来动态创建和调度任务,也可以直接在 application.properties 中配置简单的任务。

    通过配置类 (动态)

    import org.quartz.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    @Configuration
    public class QuartzConfig {
        @Bean
        public JobDetail myJobDetail() {
            // 使用 JobBuilder 创建 JobDetail
            return JobBuilder.newJob(MySpringJob.class)
                    .withIdentity("myJob") // 任务名
                    .storeDurably() // 即使没有关联的 Trigger 也保留
                    .build();
        }
        @Bean
        public Trigger myJobTrigger() {
            // 使用 TriggerBuilder 创建 Trigger
            return TriggerBuilder.newTrigger()
                    .forJob(myJobDetail()) // 关联 Job
                    .withIdentity("myTrigger") // 触发器名
                    .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?")) // 每10秒执行一次
                    .build();
        }
    }

    通过 application.properties (静态)

    # 简单任务示例:每5秒执行一次名为 'simpleJob' 的任务
    spring.quartz.job-name=simpleJob
    spring.quartz.job-group=myGroup
    spring.quartz.cron-expression=0/5 * * * * ?

这种方式非常适合简单的、固定的定时任务。


总结与最佳实践

场景 推荐方案 原因
简单、一次性、非关键任务 java.util.TimerScheduledExecutorService 轻量级,无需额外依赖,但功能简单,不支持复杂调度和持久化。
复杂调度、需要持久化、但无集群需求 Quartz (单机模式) 功能强大,支持 Cron 表达式,可持久化到数据库,重启后任务不丢失。
高可用、高并发的定时任务 Quartz (集群模式) 通过数据库锁和故障转移机制,确保任务在集群环境下稳定、不重复地执行。
Spring/Spring Boot 应用 Spring/Spring Boot 集成 Quartz 与 Spring 生态无缝集成,可以方便地注入 Spring Bean,配置简单,是现代 Java 应用的首选。

最佳实践:

  1. 保持 Job 的无状态:尽量避免在 Job 类中使用成员变量,如果必须使用,要确保线程安全,或者在 execute 方法内部使用局部变量,因为 Quartz 在集群模式下可能会复用同一个 Job 实例。
  2. 合理使用 @DisallowConcurrentExecution:如果你的 Job 是非线程安全的,使用此注解可以防止在同一个 JVM 内并发执行,但在集群模式下要格外小心。
  3. Job 执行时间不宜过长:Job 执行时间过长,可能会影响后续任务的调度,或者在集群环境下导致任务被其他节点“抢走”。
  4. 监控与告警:为 Quartz 集群配置监控和告警机制,例如监控数据库连接池、节点存活状态、任务执行失败情况等。
  5. 优雅关闭:在应用关闭时,调用 scheduler.shutdown(true),等待正在执行的任务完成,而不是直接 kill 进程。

希望这份详细的教程能帮助你全面掌握 Java Quartz 定时器!

分享:
扫描分享到社交APP
上一篇
下一篇