SpringBoot整合Quartz

一、Quartz 基础概念

1. 什么是 Quartz

Quartz 是一款开源的 Java 任务调度框架,支持基于 Cron 表达式、固定间隔、固定时间点等多种调度规则,能实现任务的全生命周期管理(创建、暂停、恢复、删除等),且支持集群部署和任务持久化,是 SpringBoot 项目中定时任务的首选方案。

2. 核心作用

  • 定时执行任务:如定时同步数据、发送通知、生成报表等
  • 动态管理任务:无需重启应用即可调整任务状态
  • 高可用支持:集群模式避免单点故障,持久化防止任务丢失
  • 灵活调度策略:支持 Cron 表达式、简单间隔等多种触发方式

3. 核心组件

  • Job:任务执行逻辑类(实现 org.quartz.Job 接口)
  • JobDetail:任务元数据(名称、分组、参数等)
  • Trigger:调度规则(CronTrigger 基于 Cron 表达式)
  • Scheduler:调度器(绑定 Job 和 Trigger 并执行任务)

二、技术栈与环境准备

  • 基础框架:Spring Boot 2.7.x
  • 任务调度:Quartz 2.3.x(Spring Boot 默认集成)
  • 数据库:MySQL
  • ORM 框架:MyBatis-Plus 3.5.x
  • 开发工具:IDEA、Maven
  • JDK

三、项目搭建与配置

image-20251029214434166

1. 创建 Maven 项目

新建 Spring Boot 项目,项目名为 SpringBootQuartz-Demo,包名设置为 com.yqd

2. 添加依赖(pom.xml)

<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 http://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>2.7.4</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.yqd</groupId>
  <artifactId>SpringBootQuartz-Demo</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>SpringBootQuartz-Demo</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!-- SpringBoot Web -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Quartz -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>

    <!-- MyBatis-Plus -->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.5.3.1</version>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
      <version>5.1.37</version>
    </dependency>

    <!-- Lombok(简化代码) -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
  </dependencies>

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

3. 配置文件(application.yml)

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/quartz_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

  quartz:
    # 持久化到数据库
    job-store-type: JDBC
    jdbc:
      # 初始化表结构(首次启动需设置为ALWAYS,后续改为NEVER)
      initialize-schema: ALWAYS
    properties:
      org:
        quartz:
          scheduler:
            instanceName: quartzScheduler
            instanceId: AUTO # 自动生成实例ID
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_ # 表前缀(Quartz默认)
            isClustered: true # 集群模式(可选)
            clusterCheckinInterval: 10000
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10 # 线程池大小
            threadPriority: 5

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.yqd.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志

四、数据库初始化

1. 创建数据库

执行 SQL 创建数据库:

CREATE DATABASE IF NOT EXISTS quartz_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

2. 自动生成 Quartz 内置表

启动项目后,Quartz 会自动在 quartz_demo 库中创建 11 张内置表(前缀 QRTZ_),无需手动干预。

3. 创建业务任务表(sys_job)

执行 SQL 创建自定义任务管理表:

CREATE TABLE `sys_job` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `job_name` varchar(100) NOT NULL COMMENT '任务名称',
  `job_group` varchar(100) NOT NULL COMMENT '任务组',
  `cron_expression` varchar(50) NOT NULL COMMENT 'Cron表达式',
  `class_name` varchar(255) NOT NULL COMMENT '任务执行类全路径',
  `status` int NOT NULL DEFAULT 1 COMMENT '状态(0-暂停,1-运行)',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_job_name_group` (`job_name`,`job_group`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定时任务管理表';

五、核心代码实现(包名:com.yqd)

1. 实体类(JobEntity)

package com.yqd.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 定时任务实体类
 */
@Data
@TableName("sys_job")
public class JobEntity {
    @TableId(type = IdType.AUTO)
    private Long id;

    private String jobName;  // 任务名称

    private String jobGroup;  // 任务组

    private String cronExpression;  // Cron表达式

    private String className;  // 任务执行类全路径

    private Integer status;  // 状态:0-暂停,1-运行

    private String remark;  // 备注

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;  // 创建时间

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;  // 更新时间
}

2. MyBatis-Plus 自动填充配置

package com.yqd.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

/**
 * 自动填充创建时间和更新时间
 */
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        // 插入时填充
        this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新时填充
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
    }
}

3. 任务执行类(TestJob)

package com.yqd.job;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;

/**
 * 示例任务:定时打印日志
 */
public class TestJob implements Job {
    private static final Logger log = LoggerFactory.getLogger(TestJob.class);

    /**
     * 任务执行方法
     */
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 获取传递的参数
        Long jobId = context.getJobDetail().getJobDataMap().getLong("jobId");

        // 业务逻辑
        log.info("===== TestJob 执行成功 =====");
        log.info("任务ID:{}", jobId);
        log.info("执行时间:{}", LocalDateTime.now());
        log.info("==========================\n");
    }
}

4. Quartz 工具类

package com.yqd.util;

import com.yqd.entity.JobEntity;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Quartz工具类:封装任务操作方法
 */
@Component
public class QuartzUtils {

    @Autowired
    private Scheduler scheduler;  // 注入调度器

    /**
     * 创建定时任务
     */
    public void createJob(JobEntity job) throws Exception {
        // 1. 获取任务执行类
        Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(job.getClassName());

        // 2. 构建JobDetail
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                .withIdentity(job.getJobName(), job.getJobGroup())  // 唯一标识
                .storeDurably()  // 持久化
                .build();

        // 3. 设置任务参数
        JobDataMap dataMap = jobDetail.getJobDataMap();
        dataMap.put("jobId", job.getId());

        // 4. 构建触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(job.getJobName() + "_trigger", job.getJobGroup())
                .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
                .build();

        // 5. 绑定任务和触发器
        scheduler.scheduleJob(jobDetail, trigger);

        // 6. 若状态为暂停,初始化为暂停状态
        if (job.getStatus() == 0) {
            pauseJob(job);
        }
    }

    /**
     * 暂停任务
     */
    public void pauseJob(JobEntity job) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        scheduler.pauseJob(jobKey);
    }

    /**
     * 恢复任务
     */
    public void resumeJob(JobEntity job) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        scheduler.resumeJob(jobKey);
    }

    /**
     * 删除任务
     */
    public void deleteJob(JobEntity job) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        scheduler.deleteJob(jobKey);
    }

    /**
     * 更新Cron表达式
     */
    public void updateCron(JobEntity job) throws Exception {
        TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName() + "_trigger", job.getJobGroup());
        CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);

        if (oldTrigger == null) {
            throw new RuntimeException("触发器不存在");
        }

        // 创建新触发器
        CronTrigger newTrigger = oldTrigger.getTriggerBuilder()
                .withIdentity(triggerKey)
                .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
                .build();

        // 替换触发器
        scheduler.rescheduleJob(triggerKey, newTrigger);
    }
}

5. Mapper 接口

package com.yqd.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yqd.entity.JobEntity;

/**
 * 任务Mapper接口
 */
public interface JobMapper extends BaseMapper<JobEntity> {
}

6. Service 层

6.1 Service 接口

package com.yqd.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.yqd.entity.JobEntity;

/**
 * 任务服务接口
 */
public interface JobService extends IService<JobEntity> {
    void addJob(JobEntity job) throws Exception;       // 添加任务
    void updateJob(JobEntity job) throws Exception;    // 更新任务
    void deleteJob(Long id) throws Exception;          // 删除任务
    void pauseJob(Long id) throws Exception;           // 暂停任务
    void resumeJob(Long id) throws Exception;          // 恢复任务
}

6.2 Service 实现类

package com.yqd.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yqd.entity.JobEntity;
import com.yqd.mapper.JobMapper;
import com.yqd.service.JobService;
import com.yqd.util.QuartzUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 任务服务实现类
 */
@Service
public class JobServiceImpl extends ServiceImpl<JobMapper, JobEntity> implements JobService {

    @Autowired
    private QuartzUtils quartzUtils;

    /**
     * 添加任务
     */
    @Override
    @Transactional
    public void addJob(JobEntity job) throws Exception {
        // 校验任务是否存在
        QueryWrapper<JobEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("job_name", job.getJobName())
                .eq("job_group", job.getJobGroup());
        if (baseMapper.exists(queryWrapper)) {
            throw new RuntimeException("任务已存在:" + job.getJobName() + "-" + job.getJobGroup());
        }

        // 保存到数据库
        save(job);
        // 创建Quartz任务
        quartzUtils.createJob(job);
    }

    /**
     * 更新任务
     */
    @Override
    @Transactional
    public void updateJob(JobEntity job) throws Exception {
        if (!existsById(job.getId())) {
            throw new RuntimeException("任务不存在");
        }
        updateById(job);
        quartzUtils.updateCron(job);
    }

    private boolean existsById(Long id) {
        // 校验任务是否存在
        QueryWrapper<JobEntity> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id", id);
        return baseMapper.exists(queryWrapper);
    }

    /**
     * 删除任务
     */
    @Override
    @Transactional
    public void deleteJob(Long id) throws Exception {
        JobEntity job = getById(id);
        if (job == null) {
            throw new RuntimeException("任务不存在");
        }
        quartzUtils.deleteJob(job);
        removeById(id);
    }

    /**
     * 暂停任务
     */
    @Override
    @Transactional
    public void pauseJob(Long id) throws Exception {
        JobEntity job = getById(id);
        if (job == null) {
            throw new RuntimeException("任务不存在");
        }
        quartzUtils.pauseJob(job);
        job.setStatus(0);
        updateById(job);
    }

    /**
     * 恢复任务
     */
    @Override
    @Transactional
    public void resumeJob(Long id) throws Exception {
        JobEntity job = getById(id);
        if (job == null) {
            throw new RuntimeException("任务不存在");
        }
        quartzUtils.resumeJob(job);
        job.setStatus(1);
        updateById(job);
    }
}

7. Controller 层

package com.yqd.controller;

import com.yqd.entity.JobEntity;
import com.yqd.service.JobService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 任务管理控制器
 */
@RestController
@RequestMapping("/job")
public class JobController {

    @Autowired
    private JobService jobService;

    /**
     * 添加任务
     */
    @PostMapping("/add")
    public String addJob(@RequestBody JobEntity job) {
        try {
            jobService.addJob(job);
            return "添加成功:" + job.getJobName();
        } catch (Exception e) {
            return "添加失败:" + e.getMessage();
        }
    }

    /**
     * 更新任务
     */
    @PostMapping("/update")
    public String updateJob(@RequestBody JobEntity job) {
        try {
            jobService.updateJob(job);
            return "更新成功:ID=" + job.getId();
        } catch (Exception e) {
            return "更新失败:" + e.getMessage();
        }
    }

    /**
     * 删除任务
     */
    @DeleteMapping("/{id}")
    public String deleteJob(@PathVariable Long id) {
        try {
            jobService.deleteJob(id);
            return "删除成功:ID=" + id;
        } catch (Exception e) {
            return "删除失败:" + e.getMessage();
        }
    }

    /**
     * 暂停任务
     */
    @GetMapping("/pause/{id}")
    public String pauseJob(@PathVariable Long id) {
        try {
            jobService.pauseJob(id);
            return "暂停成功:ID=" + id;
        } catch (Exception e) {
            return "暂停失败:" + e.getMessage();
        }
    }

    /**
     * 恢复任务
     */
    @GetMapping("/resume/{id}")
    public String resumeJob(@PathVariable Long id) {
        try {
            jobService.resumeJob(id);
            return "恢复成功:ID=" + id;
        } catch (Exception e) {
            return "恢复失败:" + e.getMessage();
        }
    }
}

8.添加配置类QuartzConfig

package com.yqd.config;

import org.quartz.Scheduler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import javax.sql.DataSource;

@Configuration
public class QuartzConfig {

    // 注入Spring管理的数据源(确保数据源配置正确)
    private final DataSource dataSource;

    public QuartzConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 配置SchedulerFactoryBean,用于创建Scheduler
     */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        // 设置数据源(Quartz持久化需要)
        factory.setDataSource(dataSource);
        // 其他可选配置:如任务工厂(如需依赖注入)
        // factory.setJobFactory(new AutowiringSpringBeanJobFactory());
        return factory;
    }

    /**
     * 暴露Scheduler Bean,供其他类注入
     */
    @Bean
    public Scheduler scheduler() throws Exception {
        return schedulerFactoryBean().getScheduler();
    }
}

9. 启动类

package com.yqd;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 应用启动类
 */
@SpringBootApplication
@MapperScan("com.yqd.mapper")  // 扫描Mapper接口
public class SpringBootQuartzDemo {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootQuartzDemo.class, args);
    }
}

六、测试流程

1. 启动项目

运行 SpringBootQuartzDemo 主类。

2. 添加任务(使用 Postman)

  • 请求地址:POST http://localhost:8080/job/add
  • 请求体(JSON):
{
  "jobName": "testJob",
  "jobGroup": "testGroup",
  "cronExpression": "0/5 * * * * ?",  // 每5秒执行一次
  "className": "com.yqd.job.TestJob",
  "status": 1,
  "remark": "测试任务"
}
  • 响应:添加成功:testJob
  • 观察日志:控制台每 5 秒打印一次任务执行日志。

3. 测试其他接口

  • 暂停任务:GET http://localhost:8080/job/pause/{id}
  • 恢复任务:GET http://localhost:8080/job/resume/{id}
  • 更新 Cron:POST http://localhost:8080/job/update(修改 cronExpression)
  • 删除任务:DELETE http://localhost:8080/job/{id}

七、生产环境注意事项

  1. 配置调整
    • spring.quartz.jdbc.initialize-schema 改为 NEVER
    • 关闭 MyBatis-Plus SQL 日志(删除 log-impl 配置)
  2. 集群部署
    • 开启 isClustered: true
    • 所有节点连接同一数据库
    • 确保服务器时间同步
  3. 任务类规范
    • 必须有无参构造方法
    • 全路径在集群环境中保持一致
  4. 异常处理
    • 任务执行类中添加异常捕获
    • 关键操作记录日志
posted @ 2025-10-29 21:52  碧水云天4  阅读(30)  评论(0)    收藏  举报