springboot2.7.11 + quartz2.3.2,单机,集群实战,增删改查任务,项目一启动就执行任务
在 IT 基础设施管理中,网络设备配置定时备份是保障运维安全的核心需求(避免配置丢失、支持故障回滚)。本文基于 Spring Boot 2.x 环境,详细讲解如何整合 Quartz 实现动态任务调度,解决数据源配置、任务增删改查、Spring Bean 注入等关键问题,附完整可复用代码。
一、环境准备与核心依赖
1. 技术栈选型
- 基础框架:Spring Boot 2.x
- 任务调度:Quartz 2.3.x(Spring Boot Starter 集成)
- 数据源:Druid 连接池 + MySQL 8.0
- 工具类:Hutool(JSON 序列化/反序列化)
- 其他:Spring WebSecurity(权限控制,可选)
手动执行 Quartz 建表脚本(路径:quartz-core-2.3.x.jar!/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql
)
2. Maven 核心依赖
在 pom.xml
中引入 Quartz Starter 及 Druid 依赖(已集成的可忽略):
<!-- Quartz 任务调度 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency><!-- Druid 数据源 -->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.16</version> <!-- 版本可按需调整 -->
</dependency><!-- MySQL 驱动 -->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency><!-- Hutool 工具类(JSON 处理) -->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.22</version>
</dependency>
二、Quartz 核心配置(解决数据源与 Bean 注入问题)
Quartz 默认不依赖 Spring 数据源,且 Job 实例无法直接注入 Spring Bean(如 Mapper/Service)。以下配置从 数据源绑定 和 Spring 上下文集成 两方面解决核心痛点。
1. 自定义 Quartz 数据源提供者
手动将 Druid 数据源注入 Quartz,避免 Quartz 找不到数据源的问题:
import com.alibaba.druid.pool.DruidDataSource;
import org.quartz.utils.ConnectionProvider;
import org.springframework.stereotype.Component;import java.sql.Connection;
import java.sql.SQLException;/*** Quartz 自定义数据源提供者(绑定 Druid 数据源)*/
@Component
public class DruidQuartzConnectionProvider implements ConnectionProvider {private final DruidDataSource dataSource;// 构造器注入 Spring 管理的 Druid 数据源public DruidQuartzConnectionProvider(DruidDataSource dataSource) {this.dataSource = dataSource;}@Overridepublic Connection getConnection() throws SQLException {// 直接从 Druid 连接池获取连接return dataSource.getConnection();}@Overridepublic void shutdown() {// 数据源由 Spring 管理,无需手动关闭}@Overridepublic void initialize() throws SQLException {// 避免重复初始化(依赖 Spring 初始化 Druid)}
}
2. Quartz 核心配置类
配置 Scheduler 工厂、任务工厂(支持 Spring Bean 注入)、Quartz 集群属性:
import com.alibaba.druid.pool.DruidDataSource;
import org.quartz.Scheduler;
import org.quartz.spi.TriggerFiredBundle;
import org.quartz.utils.DBConnectionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;import javax.annotation.PostConstruct;
import java.util.Properties;/*** Quartz 核心配置类(数据源绑定 + 任务工厂配置)*/
@Configuration
@Order(1) // 确保配置优先加载
public class QuartzConfig {@Autowiredprivate DruidDataSource dataSource;@Autowiredprivate ApplicationContext applicationContext;/*** 初始化 Quartz 数据源(解决 Quartz 找不到数据源问题)*/@PostConstructpublic void initDataSourceProvider() {DBConnectionManager.getInstance().addConnectionProvider("mopsDruidDataSource", new DruidQuartzConnectionProvider(dataSource));}/*** 配置 Scheduler 工厂(核心)*/@Beanpublic SchedulerFactoryBean schedulerFactoryBean() {SchedulerFactoryBean factory = new SchedulerFactoryBean();// 1. 绑定数据源factory.setDataSource(dataSource);// 2. 配置 Quartz 属性(集群 + 线程池)factory.setQuartzProperties(quartzProperties());// 3. 应用启动时自动启动调度器factory.setAutoStartup(true);// 4. 覆盖已存在的任务(避免重复)factory.setOverwriteExistingJobs(true);// 5. 关键:配置 Job 工厂,支持 Spring Bean 注入ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;AutowireCapableBeanFactory autowireBeanFactory = configurableContext.getAutowireCapableBeanFactory();factory.setJobFactory(new AdaptableJobFactory() {@Overrideprotected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {// 第一步:Quartz 默认创建 Job 实例Object jobInstance = super.createJobInstance(bundle);// 第二步:Spring 自动注入 Job 中的 Bean(如 Mapper/Service)autowireBeanFactory.autowireBean(jobInstance);return jobInstance;}});return factory;}/*** 注册 Scheduler 实例(供业务代码调用)*/@Beanpublic Scheduler scheduler(SchedulerFactoryBean factory) {return factory.getScheduler();}/*** Quartz 核心属性配置(集群模式 + 线程池)*/private Properties quartzProperties() {Properties properties = new Properties();// 调度器实例名(集群内统一)properties.setProperty("org.quartz.scheduler.instanceName", "NetDeviceBackupScheduler");// 实例 ID 自动生成(集群模式必备)properties.setProperty("org.quartz.scheduler.instanceId", "AUTO");// 线程池配置(25 个线程,优先级 5)properties.setProperty("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");properties.setProperty("org.quartz.threadPool.threadCount", "25");properties.setProperty("org.quartz.threadPool.threadPriority", "5");// 持久化配置(JDBC 存储,支持集群)properties.setProperty("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");properties.setProperty("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");properties.setProperty("org.quartz.jobStore.tablePrefix", "QRTZ_"); // 数据库表前缀properties.setProperty("org.quartz.jobStore.isClustered", "true"); // 启用集群properties.setProperty("org.quartz.jobStore.clusterCheckinInterval", "20000"); // 集群节点心跳间隔properties.setProperty("org.quartz.jobStore.useProperties", "false");properties.setProperty("org.quartz.jobStore.dataSource", "mopsDruidDataSource"); // 绑定自定义数据源properties.setProperty("org.quartz.scheduler.skipUpdateCheck", "true"); // 跳过版本检查(优化启动速度)return properties;}
}
3. application.yml 配置
配置数据源、Quartz 基础属性(与上述配置类互补):
spring:application:name: net-device-backup-servicedatasource:name: mopsDruidDataSource # 数据源名(需与 Quartz 配置一致)type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8.0 驱动url: 你的mysqlusername: 用户名password: 密码druid:initial-size: 5 # 初始化连接数min-idle: 5 # 最小空闲连接数max-active: 20 # 最大连接数max-wait: 60000 # 最大等待时间(毫秒)validation-query: SELECT 1 # 连接校验 SQLquartz:auto-startup: true # 自动启动(与配置类一致,冗余保障)job-store-type: jdbc # 任务存储方式(JDBC)jdbc:initialize-schema: never # 生产环境禁用自动建表(手动执行 Quartz 建表脚本)server:port: 38081servlet:context-path: /net-backup
三、业务代码实现(网络设备备份)
1. 备份任务实体类(PO)
存储备份任务配置(如设备 ID、Cron 表达式、备份路径等):
import lombok.Data;
import java.util.Date;/*** 网络设备备份任务配置 PO(对应数据库表)*/
@Data
public class BackupConfigPO {private Long id; // 任务 ID(主键)private String deviceId; // 网络设备 ID(如交换机/路由器 ID)private String deviceName; // 设备名称private String cronExpression; // Cron 表达式(定时规则)private String backupPath; // 备份文件存储路径(如 /backup/net/)private Integer status; // 任务状态(0-禁用,1-启用)private Date createTime; // 创建时间private Date updateTime; // 更新时间
}
2. 核心 Job 类(备份逻辑)
Quartz 任务执行入口,支持注入 Spring Bean(如备份服务、Redis 模板):
import cn.hutool.json.JSONUtil;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import javax.annotation.Resource;/*** 网络设备动态备份 Job(Quartz 任务执行类)*/
@Component
public class DynamicBackupJob implements Job {private static final Logger log = LoggerFactory.getLogger(DynamicBackupJob.class);// 注入 Spring Bean(依赖 QuartzConfig 中的 JobFactory 实现注入)@Resourceprivate BackupService backupService; // 备份业务服务(自定义)@Resourceprivate RedisTemplate<String, String> redisTemplate; // Redis 模板(可选,用于分布式锁)@Overridepublic void execute(JobExecutionContext context) {try {// 1. 获取任务参数(从 JobDataMap 中获取备份配置)JobDataMap dataMap = context.getMergedJobDataMap();String backupTaskJson = dataMap.getString("backupTask");BackupConfigPO backupTask = JSONUtil.toBean(backupTaskJson, BackupConfigPO.class);log.info("开始执行网络设备备份任务:设备ID={}, 设备名称={}", backupTask.getDeviceId(), backupTask.getDeviceName());// 2. 执行备份逻辑(调用业务服务)// 注:可加分布式锁避免集群环境下重复执行try {backupService.executeBackup(backupTask); // 核心备份方法(见下文)log.info("设备备份任务执行成功:设备ID={}", backupTask.getDeviceId());} finally {redisTemplate.delete(lockKey); // 释放锁}} catch (Exception e) {log.error("网络设备备份任务执行失败", e);// 可选:任务失败告警(如调用 RocketMQ 发送告警消息)}}
}
3. 备份业务服务(BackupService)
封装备份核心逻辑(如 SSH 连接设备、获取配置、存储文件):
import org.springframework.stereotype.Service;/*** 网络设备备份业务服务*/
@Service
public class BackupService {/*** 执行网络设备备份* @param config 备份任务配置*/public void executeBackup(BackupConfigPO config) {try {// 1. 连接网络设备(示例:SSH 连接交换机/路由器)// 实际场景:使用 JSch 或 Apache Commons Net 库建立 SSH 连接// String deviceIp = getDeviceIpByDeviceId(config.getDeviceId()); // 从 CMDB 获取设备 IP// SSHClient sshClient = new SSHClient();// sshClient.connect(deviceIp, 22);// sshClient.authPassword("username", "password"); // 从密码管理平台获取账号密码// 2. 执行备份命令(如 Cisco 设备:show running-config)// String backupCmd = getBackupCmdByDeviceType(config.getDeviceType());// String deviceConfig = sshClient.exec(backupCmd).getInputStreamAsString();// 3. 存储备份文件(本地磁盘或 OSS)// String fileName = config.getDeviceName() + "_" + System.currentTimeMillis() + ".txt";// String fullPath = config.getBackupPath() + fileName;// FileUtil.writeString(deviceConfig, fullPath, StandardCharsets.UTF_8);// 4. 记录备份日志(更新数据库状态)// backupLogMapper.insert(new BackupLogPO(config.getId(), "SUCCESS", fullPath));// 注:以上为伪代码,实际需根据设备类型(华为/华三/Cisco)适配命令} catch (Exception e) {// 5. 备份失败处理(记录日志 + 告警)// backupLogMapper.insert(new BackupLogPO(config.getId(), "FAILED", e.getMessage()));throw new RuntimeException("设备备份失败:" + e.getMessage(), e);}}
}
4. 任务调度服务(启动/增删改任务)
负责 Quartz 任务的生命周期管理(应用启动加载任务、新增/修改/删除任务):
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Set;/*** 网络设备备份任务调度服务(管理任务生命周期)*/
@Component
public class BackupSchedulerService implements CommandLineRunner {private static final Logger log = LoggerFactory.getLogger(BackupSchedulerService.class);@Resourceprivate Scheduler scheduler;@Resourceprivate BackupConfigMapper backupConfigMapper; // 备份配置 Mapper(自定义,查询任务列表)/*** 应用启动时执行:加载所有启用的备份任务*/@Overridepublic void run(String... args) {try {log.info("【备份任务调度服务】开始初始化...");// 1. 查询所有启用的备份任务(status=1)List<BackupConfigPO> enableTasks = backupConfigMapper.selectEnableTasks();if (enableTasks.isEmpty()) {log.info("【备份任务调度服务】无启用的备份任务");return;}// 2. 逐个调度任务for (BackupConfigPO task : enableTasks) {scheduleSingleBackupTask(task);}// 3. 检查任务状态(可选,调试用)checkTriggerStatus();log.info("【备份任务调度服务】初始化完成,共加载 {} 个任务", enableTasks.size());} catch (SchedulerException e) {log.error("【备份任务调度服务】初始化失败", e);throw new RuntimeException("Quartz 调度器初始化失败", e);}}/*** 调度单个备份任务(支持防重)* @param config 备份任务配置*/public void scheduleSingleBackupTask(BackupConfigPO config) throws SchedulerException {// 1. 构建 JobKey 和 TriggerKey(唯一标识任务和触发器)String jobId = "backup-job-" + config.getId();String triggerId = "backup-trigger-" + config.getId();JobKey jobKey = new JobKey(jobId);TriggerKey triggerKey = new TriggerKey(triggerId);// 2. 防重:任务已存在则跳过if (scheduler.checkExists(jobKey)) {log.info("【任务调度】任务已存在,跳过:jobId={}", jobId);return;}// 3. 校验 Cron 表达式(避免无效表达式)if (!CronExpression.isValidExpression(config.getCronExpression())) {throw new IllegalArgumentException("Cron 表达式无效:" + config.getCronExpression());}// 4. 构建 JobDetail(绑定任务执行类 + 参数)JobDataMap jobDataMap = new JobDataMap();jobDataMap.put("backupTask", JSONUtil.toJsonStr(config)); // 传递任务配置JobDetail jobDetail = JobBuilder.newJob(DynamicBackupJob.class).withIdentity(jobKey).setJobData(jobDataMap).requestRecovery(true) // 服务重启后恢复未完成的任务.build();// 5. 构建 Trigger(绑定 Cron 表达式)Trigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(CronScheduleBuilder.cronSchedule(config.getCronExpression())).startNow() // 立即生效.build();// 6. 提交任务到调度器scheduler.scheduleJob(jobDetail, trigger);log.info("【任务调度】任务提交成功:jobId={}, Cron={}", jobId, config.getCronExpression());}/*** 修改备份任务(先删除旧任务,再提交新任务)* @param config 新的任务配置*/public void updateBackupTask(BackupConfigPO config) throws SchedulerException {// 1. 删除旧任务deleteBackupTask(config.getId());// 2. 提交新任务scheduleSingleBackupTask(config);log.info("【任务修改】任务更新成功:jobId=backup-job-{}", config.getId());}/*** 删除备份任务* @param taskId 任务 ID*/public void deleteBackupTask(Long taskId) throws SchedulerException {String jobId = "backup-job-" + taskId;String triggerId = "backup-trigger-" + taskId;JobKey jobKey = new JobKey(jobId);TriggerKey triggerKey = new TriggerKey(triggerId);// 1. 删除触发器if (scheduler.checkExists(triggerKey)) {scheduler.unscheduleJob(triggerKey);log.info("【任务删除】触发器已删除:triggerId={}", triggerId);}// 2. 删除任务if (scheduler.checkExists(jobKey)) {scheduler.deleteJob(jobKey);log.info("【任务删除】任务已删除:jobId={}", jobId);}}/*** 检查所有触发器状态(调试用)*/public void checkTriggerStatus() throws SchedulerException {List<String> triggerGroups = scheduler.getTriggerGroupNames();for (String group : triggerGroups) {Collection<TriggerKey> triggerKeys = scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(group));for (TriggerKey triggerKey : triggerKeys) {Trigger.TriggerState state = scheduler.getTriggerState(triggerKey);// 正常状态:NORMAL(启用),异常状态:PAUSED(暂停)、ERROR(错误)if (state != Trigger.TriggerState.NORMAL) {log.warn("【触发器状态】状态异常:triggerId={}, state={}", triggerKey.getName(), state);}}}}
}
四、关键问题与解决方案
1. 问题:Quartz 找不到数据源
- 原因:Quartz 默认使用自带的数据源配置,未关联 Spring 管理的 Druid 数据源。
- 解决方案:自定义
ConnectionProvider
绑定 Druid 数据源,并在QuartzConfig
中通过DBConnectionManager
注册。
2. 问题:Job 中无法注入 Spring Bean
- 原因:Quartz 自行创建 Job 实例,未经过 Spring 容器,导致
@Resource
/@Autowired
失效。 - 解决方案:自定义
AdaptableJobFactory
,在创建 Job 实例后调用AutowireCapableBeanFactory.autowireBean()
注入 Spring Bean。
3. 问题:集群环境下任务重复执行
- 原因:Quartz 集群模式依赖数据库锁保证任务唯一性,但需确保
jobStore.isClustered=true
且数据库支持行锁。 - 解决方案:
- 配置文件中启用集群模式(
isClustered=true
); - 任务执行前加分布式锁(如 Redis 锁),双重保障。
- 配置文件中启用集群模式(
4. 问题:Cron 表达式无效导致任务不执行
- 原因:Cron 表达式格式错误(如少写字段、无效字符)。
- 解决方案:
- 调用
CronExpression.isValidExpression()
校验表达式; - 使用在线 Cron 工具(如 Cron 表达式生成器)生成正确表达式。
- 调用
五、生产环境注意事项
- Quartz 数据库表:手动执行 Quartz 建表脚本(路径:
quartz-core-2.3.x.jar!/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql
),禁用initialize-schema: always
(避免重复建表)。 - 任务日志:记录任务执行日志(成功/失败、备份路径、耗时),便于问题排查。
- 失败重试:通过 Quartz 触发器配置重试策略(如
withSchedule(CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionFireAndProceed())
)。 - 资源隔离:为 Quartz 线程池配置独立线程,避免与业务线程池抢占资源。
- 密码安全:设备登录密码不硬编码,通过密码管理平台(如齐治 OPSEC)动态获取。
六、总结
本文基于 Spring Boot 2.x + Quartz 实现了网络设备定时备份功能,核心解决了 数据源绑定、Bean 注入、任务生命周期管理 三大痛点,代码可直接复用至其他定时任务场景(如服务器巡检、日志清理)。
后续可扩展方向:
- 集成监控平台(如 Prometheus),监控任务执行成功率;
- 开发前端页面,支持任务可视化配置(Cron 表达式生成、任务状态查看);
- 接入消息队列(如 RocketMQ),实现备份结果异步通知。
七 Spring Boot + Quartz 任务增删改查接口实现(附完整代码)
在实际项目中,我们往往需要通过接口动态管理Quartz任务(新增、修改、删除、暂停/恢复),而不是仅在应用启动时加载。本文基于前文的Quartz基础配置,封装一套标准化的任务管理接口,支持RESTful风格调用,可直接集成到生产环境。
一、核心接口设计
我们将任务管理操作封装为QuartzTaskService
服务类,并通过Controller暴露REST接口,支持以下功能:
- 新增定时任务
- 修改定时任务(含Cron表达式变更)
- 删除定时任务
- 暂停/恢复任务
- 查询任务状态
先定义一个任务DTO(数据传输对象),统一接口入参格式:
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;/*** 定时任务操作DTO*/
@Data
public class QuartzTaskDTO {/** 任务ID(新增时可不传,修改/删除时必传) */private Long taskId;/** 任务名称(用于标识,非唯一) */@NotBlank(message = "任务名称不能为空")private String taskName;/** Cron表达式(定时规则) */@NotBlank(message = "Cron表达式不能为空")private String cronExpression;/** 任务参数(JSON格式,将传递给Job) */private String taskParams;/** 任务执行类(全类名,需实现Job接口) */@NotBlank(message = "任务执行类不能为空")private String jobClassName;
}
二、任务管理核心服务
封装QuartzTaskService
,实现任务增删改查的核心逻辑,注意处理并发和异常情况:
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;/*** Quartz任务管理核心服务*/
@Service
public class QuartzTaskService {private static final Logger log = LoggerFactory.getLogger(QuartzTaskService.class);@Resourceprivate Scheduler scheduler;/*** 新增定时任务* @param dto 任务信息DTO*/@Transactional(rollbackFor = Exception.class)public void addTask(QuartzTaskDTO dto) throws SchedulerException, ClassNotFoundException {// 1. 参数校验if (!CronExpression.isValidExpression(dto.getCronExpression())) {throw new IllegalArgumentException("Cron表达式格式错误: " + dto.getCronExpression());}// 2. 校验Job类是否存在且实现Job接口Class<?> jobClass = ClassUtil.loadClass(dto.getJobClassName());if (jobClass == null || !Job.class.isAssignableFrom(jobClass)) {throw new IllegalArgumentException("任务执行类不存在或未实现Job接口: " + dto.getJobClassName());}// 3. 生成唯一JobKey和TriggerKey(建议使用业务ID作为前缀)String jobId = "job-" + System.currentTimeMillis(); // 实际项目建议使用业务主键String triggerId = "trigger-" + System.currentTimeMillis();JobKey jobKey = new JobKey(jobId);TriggerKey triggerKey = new TriggerKey(triggerId);// 4. 防重检查if (scheduler.checkExists(jobKey)) {throw new RuntimeException("任务已存在: " + jobId);}// 5. 封装任务参数JobDataMap jobDataMap = new JobDataMap();// 传递基础参数(任务ID、名称等)Map<String, Object> baseParams = new HashMap<>();baseParams.put("taskId", dto.getTaskId());baseParams.put("taskName", dto.getTaskName());// 传递业务参数(如果有)if (StrUtil.isNotBlank(dto.getTaskParams())) {baseParams.put("bizParams", JSONUtil.parseObj(dto.getTaskParams()));}jobDataMap.put("taskParams", JSONUtil.toJsonStr(baseParams));// 6. 创建JobDetailJobDetail jobDetail = JobBuilder.newJob((Class<? extends Job>) jobClass).withIdentity(jobKey).setJobData(jobDataMap).requestRecovery(true) // 服务重启后恢复任务.build();// 7. 创建TriggerTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(CronScheduleBuilder.cronSchedule(dto.getCronExpression())).startNow() // 立即生效.build();// 8. 提交任务scheduler.scheduleJob(jobDetail, trigger);log.info("新增定时任务成功: jobId={}, cron={}", jobId, dto.getCronExpression());}/*** 修改定时任务(支持修改Cron表达式和参数)* @param dto 任务信息DTO(必须包含taskId)*/@Transactional(rollbackFor = Exception.class)public void updateTask(QuartzTaskDTO dto) throws SchedulerException, ClassNotFoundException {if (dto.getTaskId() == null) {throw new IllegalArgumentException("任务ID不能为空");}// 1. 先删除旧任务deleteTask(dto.getTaskId());// 2. 再新增新任务(复用新增逻辑)addTask(dto);log.info("修改定时任务成功: taskId={}", dto.getTaskId());}/*** 删除定时任务* @param taskId 业务任务ID*/@Transactional(rollbackFor = Exception.class)public void deleteTask(Long taskId) throws SchedulerException {// 注意:此处JobKey生成规则必须与新增时一致String jobId = "job-" + taskId;String triggerId = "trigger-" + taskId;JobKey jobKey = new JobKey(jobId);TriggerKey triggerKey = new TriggerKey(triggerId);// 1. 先删除触发器if (scheduler.checkExists(triggerKey)) {boolean unscheduleResult = scheduler.unscheduleJob(triggerKey);log.info("删除触发器: triggerId={}, 结果: {}", triggerId, unscheduleResult);} else {log.warn("删除触发器失败: 未找到triggerId={}", triggerId);}// 2. 再删除Jobif (scheduler.checkExists(jobKey)) {boolean deleteResult = scheduler.deleteJob(jobKey);log.info("删除任务: jobId={}, 结果: {}", jobId, deleteResult);} else {log.warn("删除任务失败: 未找到jobId={}", jobId);}}/*** 暂停任务* @param taskId 业务任务ID*/public void pauseTask(Long taskId) throws SchedulerException {String jobId = "job-" + taskId;JobKey jobKey = new JobKey(jobId);if (scheduler.checkExists(jobKey)) {scheduler.pauseJob(jobKey);log.info("暂停任务成功: jobId={}", jobId);} else {throw new RuntimeException("任务不存在: jobId=" + jobId);}}/*** 恢复任务* @param taskId 业务任务ID*/public void resumeTask(Long taskId) throws SchedulerException {String jobId = "job-" + taskId;JobKey jobKey = new JobKey(jobId);if (scheduler.checkExists(jobKey)) {scheduler.resumeJob(jobKey);log.info("恢复任务成功: jobId={}", jobId);} else {throw new RuntimeException("任务不存在: jobId=" + jobId);}}/*** 查询任务状态* @param taskId 业务任务ID* @return 状态描述(NORMAL-正常, PAUSED-暂停, COMPLETE-完成, ERROR-错误, NONE-不存在)*/public String getTaskStatus(Long taskId) throws SchedulerException {String triggerId = "trigger-" + taskId;TriggerKey triggerKey = new TriggerKey(triggerId);if (!scheduler.checkExists(triggerKey)) {return "NONE";}Trigger.TriggerState state = scheduler.getTriggerState(triggerKey);return state.name();}
}
三、REST接口暴露
通过Controller将服务方法暴露为HTTP接口,支持前端调用:
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.quartz.SchedulerException;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import javax.validation.Valid;/*** 定时任务管理接口*/
@RestController
@RequestMapping("/api/quartz/task")
@Api(tags = "定时任务管理")
public class QuartzTaskController {@Resourceprivate QuartzTaskService quartzTaskService;@PostMapping("/add")@ApiOperation("新增定时任务")public Result<String> addTask(@Valid @RequestBody QuartzTaskDTO dto) {try {quartzTaskService.addTask(dto);return Result.success("新增任务成功");} catch (Exception e) {return Result.fail("新增任务失败: " + e.getMessage());}}@PostMapping("/update")@ApiOperation("修改定时任务")public Result<String> updateTask(@Valid @RequestBody QuartzTaskDTO dto) {try {quartzTaskService.updateTask(dto);return Result.success("修改任务成功");} catch (Exception e) {return Result.fail("修改任务失败: " + e.getMessage());}}@DeleteMapping("/delete/{taskId}")@ApiOperation("删除定时任务")public Result<String> deleteTask(@PathVariable Long taskId) {try {quartzTaskService.deleteTask(taskId);return Result.success("删除任务成功");} catch (Exception e) {return Result.fail("删除任务失败: " + e.getMessage());}}@PostMapping("/pause/{taskId}")@ApiOperation("暂停定时任务")public Result<String> pauseTask(@PathVariable Long taskId) {try {quartzTaskService.pauseTask(taskId);return Result.success("暂停任务成功");} catch (Exception e) {return Result.fail("暂停任务失败: " + e.getMessage());}}@PostMapping("/resume/{taskId}")@ApiOperation("恢复定时任务")public Result<String> resumeTask(@PathVariable Long taskId) {try {quartzTaskService.resumeTask(taskId);return Result.success("恢复任务成功");} catch (Exception e) {return Result.fail("恢复任务失败: " + e.getMessage());}}@GetMapping("/status/{taskId}")@ApiOperation("查询任务状态")public Result<String> getTaskStatus(@PathVariable Long taskId) {try {String status = quartzTaskService.getTaskStatus(taskId);return Result.success(status);} catch (Exception e) {return Result.fail("查询任务状态失败: " + e.getMessage());}}
}
其中Result
是通用响应封装类:
import lombok.Data;/*** 通用响应结果*/
@Data
public class Result<T> {private int code;private String message;private T data;public static <T> Result<T> success(T data) {Result<T> result = new Result<>();result.setCode(200);result.setMessage("操作成功");result.setData(data);return result;}public static <T> Result<T> fail(String message) {Result<T> result = new Result<>();result.setCode(500);result.setMessage(message);return result;}
}
四、关键技术点说明
1. JobKey与TriggerKey设计
- 必须保证唯一性:建议使用
业务ID+固定前缀
生成(如job-${taskId}
),避免重复 - 新增/修改/删除操作需使用同一规则生成,否则会出现"找不到任务"的问题
2. 任务参数传递
- 通过
JobDataMap
传递参数,支持基本类型和JSON字符串 - 建议统一封装参数格式(如包含
taskId
、taskName
、bizParams
等),便于Job中解析
3. 异常处理
- 对Cron表达式进行预校验,避免无效表达式导致任务创建失败
- 对Job类进行校验,确保类存在且实现
Job
接口 - 所有操作建议加事务,确保任务状态一致性
4. 集群环境注意事项
- 任务ID必须全局唯一(可使用分布式ID生成器如Leaf)
- 避免在Job中执行耗时操作,如需处理可通过消息队列异步执行
- 敏感参数(如密码)建议加密存储,避免明文暴露在任务参数中
五、接口调用示例
1. 新增任务
POST /api/quartz/task/add
Content-Type: application/json{"taskName": "网络设备备份任务","cronExpression": "0 0 1 * * ?", // 每天凌晨1点执行"taskParams": "{\"deviceId\":1001,\"backupPath\":\"/backup/net/\"}","jobClassName": "com.example.backup.job.DynamicBackupJob"
}
2. 修改任务(变更Cron表达式)
POST /api/quartz/task/update
Content-Type: application/json{"taskId": 1001,"taskName": "网络设备备份任务","cronExpression": "0 0 2 * * ?", // 改为每天凌晨2点执行"taskParams": "{\"deviceId\":1001,\"backupPath\":\"/backup/net/v2/\"}","jobClassName": "com.example.backup.job.DynamicBackupJob"
}
3. 暂停任务
POST /api/quartz/task/pause/1001
4. 查询任务状态
GET /api/quartz/task/status/1001
# 响应:{"code":200,"message":"操作成功","data":"NORMAL"}
六、总结
本文实现了一套完整的Quartz任务管理接口,支持动态新增、修改、删除和状态查询,解决了实际项目中任务管理的核心需求。关键在于:
- 统一JobKey和TriggerKey生成规则,确保任务唯一标识
- 完善参数校验和异常处理,提高接口健壮性
- 支持事务管理,保证任务状态一致性
实际使用时,可根据业务需求扩展功能,如:
- 增加任务执行日志记录
- 支持任务执行历史查询
- 集成前端页面实现可视化管理
- 对接监控系统实现任务异常告警