我们为什么选择Quartz?

虽然 Spring Boot 自带的 @Scheduled 注解对于简单的、单机、内存中的定时任务非常方便,但 Quartz 提供了几个 @Scheduled 无法比拟的关键优势,这些优势对于构建一个健壮、可管理、生产就绪的定时任务系统至关重要。

任务持久化 (Persistence):

  • @Scheduled 的限制:任务信息(何时执行、执行什么)仅存在于内存中。如果应用重启,所有任务的调度状态都会丢失。你需要手动重新配置和启动它们。这对于需要长期运行或不能中断的任务来说是不可接受的。
  • Quartz 的优势: Quartz 可以将任务(JobDetail)、触发器(Trigger)和调度器状态持久化到数据库(如我们项目中使用的 MySQL)。这意味着:
  1. 重启恢复: 应用重启后,Quartz 会从数据库中读取之前存储的任务和触发器信息,自动恢复调度。那些在应用关闭期间“错过”的执行,可以根据配置策略(如 MISFIRE_INSTRUCTION)进行处理。
  2. 状态一致性: 任务的状态(下次执行时间、是否暂停等)是持久化的,不会因为应用生命周期而丢失。

动态任务管理:

  • @Scheduled 的限制: 任务的执行计划(Cron表达式等)通常在代码中通过注解硬编码,或者通过配置文件定义。要在运行时动态地创建、修改、暂停、恢复或删除一个任务是非常困难甚至不可能的。
  • Quartz 的优势: 提供了丰富的 API (Scheduler 接口) 来实现任务的全生命周期管理。正如我们的项目所展示的:
  1. 可以通过 REST API (OrderCleanupJobController) 动态地创建 (scheduleJob) 一个带有特定 Cron 表达式和参数(如超时时间)的任务。
  2. 可以随时暂停 (pauseJob)、恢复 (resumeJob) 或删除 (deleteJob) 一个正在运行或已存在的任务。
  3. 这种灵活性对于运营、运维或业务配置至关重要,无需停机即可调整任务策略。

丰富的任务和触发器模型:

  • Quartz 提供了比 @Scheduled 更精细和强大的任务(Job)和触发器(Trigger)模型。支持多种触发器类型(CronTrigger, SimpleTrigger 等),以及更复杂的调度需求。

代码实操

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>quartz-order-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>quartz-order-demo</name>
    <description>Demo project for Spring Boot, Quartz, and Scheduled Order Cleanup using MySQL</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- MySQL Driver -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

spring:
  datasource:
    url:jdbc:mysql://localhost:3306/quartz_order_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username:root
    password:123456
    driver-class-name:com.mysql.cj.jdbc.Driver
jpa:
    database-platform:org.hibernate.dialect.MySQLDialect
    hibernate:
      ddl-auto:update# Hibernate会根据实体自动创建/更新表结构
    show-sql:true# 显示执行的SQL语句,方便调试
    properties:
      hibernate:
        format_sql:true# 格式化SQL输出

logging:
level:
    root:INFO
    com.example.quartzorder:DEBUG
    org.springframework.scheduling.quartz:INFO
    org.hibernate.SQL:DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder:TRACE# 显示SQL参数值

# Quartz 使用数据库存储任务信息
spring:
quartz:
    job-store-type:jdbc# 使用 JDBC 存储
    jdbc:
      initialize-schema:always# 启动时总是初始化Quartz表 (生产环境慎用)
    properties:
      org:
        quartz:
          scheduler:
            instanceName:OrderCleanupScheduler
            instanceId:AUTO
          jobStore:
            class:org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix:QRTZ_
            isClustered:false
          threadPool:
            class:org.quartz.simpl.SimpleThreadPool
            threadCount:5

logback-spring.xml 日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>

    <logger name="com.example.quartzorder" level="DEBUG"/>
    <logger name="org.springframework.scheduling.quartz" level="INFO"/>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>

</configuration>

Order 实体类

package com.example.quartzorder.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "orders")
publicclass Order {

    publicenum Status {
        PENDING_PAYMENT, PAID, SHIPPED, CANCELLED
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String orderNumber;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Status status;

    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;

    // Constructors
    public Order() {}

    public Order(String orderNumber, Status status, LocalDateTime createdAt) {
        this.orderNumber = orderNumber;
        this.status = status;
        this.createdAt = createdAt;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOrderNumber() {
        return orderNumber;
    }

    public void setOrderNumber(String orderNumber) {
        this.orderNumber = orderNumber;
    }

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        this.status = status;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    @Override
    public String toString() {
        return"Order{" +
                "id=" + id +
                ", orderNumber='" + orderNumber + ''' +
                ", status=" + status +
                ", createdAt=" + createdAt +
                '}';
    }
}

Order Repository

package com.example.quartzorder.repository;

import com.example.quartzorder.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Repository
publicinterface OrderRepository extends JpaRepository<Order, Long> {

    /**
     * 查找状态为 PENDING_PAYMENT 且创建时间早于指定时间的订单
     * @param beforeTime 指定的时间点
     * @return 订单列表
     */
    @Query("SELECT o FROM Order o WHERE o.status = 'PENDING_PAYMENT' AND o.createdAt < :beforeTime")
    List<Order> findPendingPaymentOrdersBefore(@Param("beforeTime") LocalDateTime beforeTime);

    /**
     * 批量更新订单状态为 CANCELLED
     * @param orderIds 要更新的订单ID列表
     * @return 更新的行数
     */
    @Modifying
    @Transactional
    @Query("UPDATE Order o SET o.status = 'CANCELLED' WHERE o.id IN :orderIds")
    int cancelOrdersByIds(@Param("orderIds") List<Long> orderIds);
}

JobDataMapUtil 工具类

package com.example.quartzorder.util;

import org.quartz.JobDataMap;

publicclass JobDataMapUtil {

    publicstaticfinal String TIMEOUT_MINUTES_KEY = "timeoutMinutes";

    public static int getTimeoutMinutes(JobDataMap jobDataMap) {
        return jobDataMap.getInt(TIMEOUT_MINUTES_KEY);
    }

    public static void setTimeoutMinutes(JobDataMap jobDataMap, int timeoutMinutes) {
        jobDataMap.put(TIMEOUT_MINUTES_KEY, timeoutMinutes);
    }
}

订单业务逻辑服务

package com.example.quartzorder.service;

import com.example.quartzorder.entity.Order;
import com.example.quartzorder.repository.OrderRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Service
publicclass OrderService {

    privatestaticfinal Logger logger = LoggerFactory.getLogger(OrderService.class);

    @Autowired
    private OrderRepository orderRepository;

    /**
     * 查找并取消超时的未支付订单
     * @param timeoutMinutes 超时分钟数
     */
    public void cancelUnpaidOrders(int timeoutMinutes) {
        LocalDateTime beforeTime = LocalDateTime.now().minusMinutes(timeoutMinutes);
        logger.info("Searching for PENDING_PAYMENT orders created before {}", beforeTime);

        List<Order> ordersToCancel = orderRepository.findPendingPaymentOrdersBefore(beforeTime);

        if (ordersToCancel.isEmpty()) {
            logger.info("No PENDING_PAYMENT orders found to cancel.");
            return;
        }

        List<Long> orderIds = ordersToCancel.stream().map(Order::getId).collect(Collectors.toList());
        logger.info("Found {} orders to cancel: {}", ordersToCancel.size(), orderIds);

        // 执行批量更新
        int updatedCount = orderRepository.cancelOrdersByIds(orderIds);
        logger.info("Cancelled {} orders.", updatedCount);
        // 模拟:打印被取消的订单号
        ordersToCancel.forEach(order -> System.out.println(">>> Order Cancelled: " + order.getOrderNumber()));
    }
}

Quartz Job类

package com.example.quartzorder.job;

import com.example.quartzorder.service.OrderService;
import com.example.quartzorder.util.JobDataMapUtil;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// 为了让 @Autowired 生效,需要配置 SpringBeanJobFactory
@Component
publicclass CancelUnpaidOrdersJob implements Job {

    privatestaticfinal Logger logger = LoggerFactory.getLogger(CancelUnpaidOrdersJob.class);

    // 需要配合自定义的 SpringBeanJobFactory 使用
    @Autowired
    private OrderService orderService;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        int timeoutMinutes = JobDataMapUtil.getTimeoutMinutes(jobDataMap);
        String jobName = context.getJobDetail().getKey().getName();

        logger.info("Executing job [{}] with timeout [{}] minutes", jobName, timeoutMinutes);

        if (orderService != null) {
            orderService.cancelUnpaidOrders(timeoutMinutes);
        } else {
            logger.error("OrderService is not injected. Cannot execute job [{}]", jobName);
            // 在实际项目中,应确保 JobFactory 配置正确
        }
    }
}

配置正确的 JobFactory

package com.example.quartzorder.config;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
import org.springframework.stereotype.Component;

@Component
publicclass AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {

    privatetransient AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
}

application.yml

spring:
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: always
    job-factory: com.example.quartzorder.config.AutowiringSpringBeanJobFactory

DTO

package com.example.quartzorder.model;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

publicclass OrderCleanupJobRequest {

    @NotBlank(message = "Job name cannot be blank")
    private String jobName;

    @NotBlank(message = "Cron expression cannot be blank")
    private String cronExpression;

    @Min(value = 1, message = "Timeout minutes must be at least 1")
    privateint timeoutMinutes = 10; // 默认10分钟

    // Getters and Setters
    public String getJobName() {
        return jobName;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public void setCronExpression(String cronExpression) {
        this.cronExpression = cronExpression;
    }

    public int getTimeoutMinutes() {
        return timeoutMinutes;
    }

    public void setTimeoutMinutes(int timeoutMinutes) {
        this.timeoutMinutes = timeoutMinutes;
    }
}

Service

package com.example.quartzorder.service;

import com.example.quartzorder.job.CancelUnpaidOrdersJob;
import com.example.quartzorder.util.JobDataMapUtil;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
publicclass OrderCleanupJobService {

    privatestaticfinal Logger logger = LoggerFactory.getLogger(OrderCleanupJobService.class);

    @Autowired
    private Scheduler scheduler;

    public void addJob(String jobName, String cronExpression, int timeoutMinutes) throws SchedulerException {
        if (scheduler.checkExists(JobKey.jobKey(jobName))) {
            logger.warn("Job [{}] already exists.", jobName);
            thrownew SchedulerException("Job already exists: " + jobName);
        }

        JobDataMap jobDataMap = new JobDataMap();
        JobDataMapUtil.setTimeoutMinutes(jobDataMap, timeoutMinutes);

        JobDetail jobDetail = JobBuilder.newJob(CancelUnpaidOrdersJob.class)
                .withIdentity(jobName)
                .usingJobData(jobDataMap)
                .build();

        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(jobName + "_trigger")
                .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
                .build();

        scheduler.scheduleJob(jobDetail, cronTrigger);
        logger.info("Scheduled order cleanup job [{}] with cron [{}] and timeout [{}] minutes", jobName, cronExpression, timeoutMinutes);
    }

    public void deleteJob(String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName);
        if (!scheduler.checkExists(jobKey)) {
            logger.warn("Job [{}] does not exist.", jobName);
            thrownew SchedulerException("Job does not exist: " + jobName);
        }
        scheduler.deleteJob(jobKey);
        logger.info("Deleted order cleanup job [{}]", jobName);
    }

    public void pauseJob(String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName);
        if (!scheduler.checkExists(jobKey)) {
            logger.warn("Job [{}] does not exist.", jobName);
            thrownew SchedulerException("Job does not exist: " + jobName);
        }
        scheduler.pauseJob(jobKey);
        logger.info("Paused order cleanup job [{}]", jobName);
    }

    public void resumeJob(String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName);
        if (!scheduler.checkExists(jobKey)) {
            logger.warn("Job [{}] does not exist.", jobName);
            thrownew SchedulerException("Job does not exist: " + jobName);
        }
        scheduler.resumeJob(jobKey);
        logger.info("Resumed order cleanup job [{}]", jobName);
    }
}

Controller

package com.example.quartzorder.controller;

import com.example.quartzorder.model.OrderCleanupJobRequest;
import com.example.quartzorder.service.OrderCleanupJobService;
import jakarta.validation.Valid;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/order-cleanup-jobs")
publicclass OrderCleanupJobController {

    @Autowired
    private OrderCleanupJobService jobService;

    @PostMapping("/schedule")
    public ResponseEntity<Map<String, Object>> scheduleJob(@Valid@RequestBody OrderCleanupJobRequest request) {
        Map<String, Object> response = new HashMap<>();
        try {
            jobService.addJob(request.getJobName(), request.getCronExpression(), request.getTimeoutMinutes());
            response.put("status", "success");
            response.put("message", "Order cleanup job '" + request.getJobName() + "' scheduled successfully.");
            return ResponseEntity.status(HttpStatus.CREATED).body(response);
        } catch (SchedulerException e) {
            response.put("status", "error");
            response.put("message", "Failed to schedule job: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        } catch (Exception e) {
             response.put("status", "error");
             response.put("message", "Invalid request data: " + e.getMessage());
             return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
        }
    }

    @DeleteMapping("/delete/{jobName}")
    public ResponseEntity<Map<String, Object>> deleteJob(@PathVariable String jobName) {
        Map<String, Object> response = new HashMap<>();
        try {
            jobService.deleteJob(jobName);
            response.put("status", "success");
            response.put("message", "Order cleanup job '" + jobName + "' deleted successfully.");
            return ResponseEntity.ok(response);
        } catch (SchedulerException e) {
            response.put("status", "error");
            response.put("message", "Failed to delete job: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }

    @PostMapping("/pause/{jobName}")
    public ResponseEntity<Map<String, Object>> pauseJob(@PathVariable String jobName) {
        Map<String, Object> response = new HashMap<>();
        try {
            jobService.pauseJob(jobName);
            response.put("status", "success");
            response.put("message", "Order cleanup job '" + jobName + "' paused successfully.");
            return ResponseEntity.ok(response);
        } catch (SchedulerException e) {
            response.put("status", "error");
            response.put("message", "Failed to pause job: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }

    @PostMapping("/resume/{jobName}")
    public ResponseEntity<Map<String, Object>> resumeJob(@PathVariable String jobName) {
        Map<String, Object> response = new HashMap<>();
        try {
            jobService.resumeJob(jobName);
            response.put("status", "success");
            response.put("message", "Order cleanup job '" + jobName + "' resumed successfully.");
            return ResponseEntity.ok(response);
        } catch (SchedulerException e) {
            response.put("status", "error");
            response.put("message", "Failed to resume job: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }
}

Application

package com.example.quartzorder;

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

@SpringBootApplication
public class QuartzOrderApplication {

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

}
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]