springboot+quartz 单机和集群使用示例-【备份任务】

1.配置文件

java 复制代码
spring:
  datasource:
    name: mopsDruidDataSource   #数据源名字
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/itsm?useSSL=false&serverTimezone=UTC
    username: root
    password: root

    druid:
      #初始化时建立物理连接的个数
      initial-size: 5
      #最小连接池数量
      min-idle: 5
      #最大连接池数量 maxIdle已经不再使用
      max-active: 20
      #获取连接时最大等待时间,单位毫秒
      max-wait: 60000
      #用来检测连接是否有效的sql
      validation-query: SELECT 1

2.配置类

java 复制代码
import com.alibaba.druid.pool.DruidDataSource;
import org.quartz.utils.ConnectionProvider;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

@Component
public class DruidQuartzConnectionProvider2 implements ConnectionProvider {

    private final DruidDataSource dataSource;


    public DruidQuartzConnectionProvider2(DruidDataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Connection getConnection() throws SQLException {
        // 直接从 Spring 数据源获取连接(无需手动配置 URL 等)
        return dataSource.getConnection();
    }

    @Override
    public void shutdown() {
        // 关闭连接池(由 Spring 管理,通常无需手动调用)
    }

    @Override
    public void initialize() throws SQLException {
        // 移除 dataSource.init(),依赖 Spring 初始化
        // dataSource.init();
    }


    //  quartz:
    //    auto-startup: true
    //    # 指定使用数据库作为任务存储方式,这使得多个节点可以共享任务和触发器信息
    //    job-store-type: jdbc
    //    jdbc:
    //      # 在应用启动时初始化或更新 Quartz 的数据库表结构。对于生产环境,建议设置为 never 或者手动管理数据库初始化,以避免意外覆盖现有数据
    //      initialize-schema: never
    //    properties:
    //      org:
    //        quartz:
    //          scheduler:
    //            # 集群中所有节点共用的名字
    //            instanceName: MyClusteredScheduler
    //            # 设置为 AUTO 表示 Quartz 自动为每个实例生成一个唯一的实例ID
    //            instanceId: AUTO
    //          threadPool:
    //            # 定义了 Quartz 线程池中的线程数和优先级,用于执行作业
    //            threadCount: 25
    //            threadPriority: 5
    //          jobStore:
    //            # 作为事务管理的持久化类,并且指定了标准的 JDBC 代理类、数据源名称、表前缀和集群模式启用状态
    //            class: org.quartz.impl.jdbcjobstore.JobStoreTX
    //            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    //            tablePrefix: QRTZ_
    //            # 启用了集群支持,确保多节点间任务的协调
    //            isClustered: true
    //            clusterCheckinInterval: 20000
    //            useProperties: false
    //            #指定数据源名字
    //            dataSource: mopsDruidDataSource
}

3.配置类

java 复制代码
import com.alibaba.druid.pool.DruidDataSource;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
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;

@Configuration
@Order(1)
public class QuartzConfig {

    @Autowired
    private DruidDataSource dataSource;


    @Autowired
    private ApplicationContext applicationContext;  // 注入 Spring 上下文

    /**
     * 由于quartz报一直找不到数据源,不知道为什么?,所以需要手动添加数据源
     * @date 2025/5/12 9:46
     */
    @PostConstruct
    public void initDataSourceProvider() {
        DBConnectionManager.getInstance().addConnectionProvider("mopsDruidDataSource", new DruidQuartzConnectionProvider2(dataSource));
    }


    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);
        factory.setQuartzProperties(quartzProperties());
        factory.setAutoStartup(true);
        factory.setOverwriteExistingJobs(true);

        // 关键:将 ApplicationContext 转换为 ConfigurableApplicationContext
        ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
        AutowireCapableBeanFactory autowireCapableBeanFactory = configurableContext.getAutowireCapableBeanFactory();

        // 配置 JobFactory,让 Quartz 使用 Spring 的 Bean 工厂创建 Job 实例
        factory.setJobFactory(new AdaptableJobFactory() {
            @Override
            protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
                // 1. 创建 Job 实例(由 Quartz 默认工厂创建)
                Object jobInstance = super.createJobInstance(bundle);
                // 2. 让 Spring 的 AutowireCapableBeanFactory 自动注入依赖
                autowireCapableBeanFactory.autowireBean(jobInstance);
                return jobInstance;
            }
        });

        return factory;
    }

    @Bean
    public Scheduler scheduler(SchedulerFactoryBean factory) {
        return factory.getScheduler();
    }

    private Properties quartzProperties() {
        Properties properties = new Properties();
        properties.setProperty("org.quartz.scheduler.instanceName", "MyClusteredScheduler");
        properties.setProperty("org.quartz.scheduler.instanceId", "AUTO");
        properties.setProperty("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        properties.setProperty("org.quartz.threadPool.threadCount", "25");
        properties.setProperty("org.quartz.threadPool.threadPriority", "5");
        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;
    }
}

4.任务类

java 复制代码
import com.enterprise.itsm.util.LogUtil;
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.stereotype.Component;

@Component
public class DynamicBackupJob implements Job {

    private static final Logger log = LoggerFactory.getLogger(DynamicBackupJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        LogUtil.info(log, "DynamicBackupJob-run", "开始执行定时任务");
        JobDataMap dataMap = context.getMergedJobDataMap();
        String syncF5TaskJson = dataMap.getString("backupTask");
        // SyncF5DeviceCronConfigPO configPO = JSONUtil.toBean(syncF5TaskJson, SyncF5DeviceCronConfigPO.class);
        //备份任务真正执行起来的逻辑
    }
}

5.调度开启

java 复制代码
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;

/**
 * 动态备份任务调度服务
 * 负责在应用启动时加载数据库中的备份配置,并动态注册到 Quartz 调度器中
 */
@Slf4j
@Component
public class BackupSchedulerService implements CommandLineRunner {

    @Resource
    private Scheduler scheduler;

    @Override
    public void run(String... args) {
        try {
            log.info("【备份调度器】服务启动,开始初始化动态备份任务...");
            // 调度所有备份任务,你也可以查询所有的任务,然后循环调用
            scheduleAllBackupTasks();
            // 检查触发器状态(可选)
            checkTriggerStatus();
        } catch (SchedulerException e) {
            log.error("【备份调度器】调度器初始化失败", e);
        }
    }

      /**
     * 调度所有备份任务
     * 1. 获取所有唯一的备份任务组
     * 2. 遍历并逐个调度具体的设备备份任务
     */
    public void scheduleAllBackupTasks() throws SchedulerException {
        log.info("开始调度所有备份任务,当前时间: {}", System.currentTimeMillis());
        try {
            // 对单个备份任务进行调度
            scheduleSingleBackupTask(config);
        } catch (Exception e) {
            log.error("【任务调度失败】任务组: {}, 异常信息: {}", backupName, e.getMessage(), e);
        }
    }

    /**
     * 调度单个备份任务(含防重校验)
     */
    private void scheduleSingleBackupTask(BackupConfig config) throws SchedulerException {
        log.info("开始调度设备IP为: {} 的备份任务", config.getManagementIp());
        
        String jobId = "job-" + config.getId();
        String triggerId = "trigger-" + config.getId();
        JobKey jobKey = new JobKey(jobId);
        TriggerKey triggerKey = new TriggerKey(triggerId);

        try {
            // 1. 防止重复添加任务
            if (scheduler.checkExists(jobKey)) {
                log.info("【跳过】任务 {} 已存在,不再重复调度。", jobId);
                return;
            }

            // 2. 创建 JobDetail,并通过 JobDataMap 传递业务参数
            JobDataMap jobDataMap = new JobDataMap();
            jobDataMap.put("backupTask", JSONUtil.toJsonStr(config));
            
            JobDetail job = JobBuilder.newJob(DynamicBackupJob.class)
                    .withIdentity(jobKey)
                    .setJobData(jobDataMap)
                    .build();

            // 3. 创建 Trigger,绑定 Cron 表达式
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerKey)
                    .withSchedule(CronScheduleBuilder.cronSchedule(config.getCronExpression()))
                    .build();

            // 4. 注册到调度器
            scheduler.scheduleJob(job, trigger);
            log.info("【成功调度】任务ID: {}, Cron: {}", config.getId(), config.getCronExpression());
        } catch (Exception e) {
            log.error("【任务调度失败】任务ID: {}, 异常: {}", config.getId(), e.getMessage(), e);
            throw e;
        }
    }

    /**
     * 检查触发器状态(排查异常状态的触发器)
     */
    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);
                if (state != Trigger.TriggerState.NORMAL) {
                    log.warn("触发器 {} 状态异常: {}", triggerKey.getName(), state);
                }
            }
        }
    }

    /**
     * 清除所有已注册的备份任务
     * 彻底删除作业与触发器,防止服务重启后任务堆积
     */
    public void clearAllScheduledJobs() throws SchedulerException {
        // 1. 删除所有触发器
        for (String triggerGroupName : scheduler.getTriggerGroupNames()) {
            GroupMatcher<TriggerKey> triggerGroupMatcher = GroupMatcher.triggerGroupEquals(triggerGroupName);
            Set<TriggerKey> triggerKeys = scheduler.getTriggerKeys(triggerGroupMatcher);

            for (TriggerKey triggerKey : triggerKeys) {
                scheduler.pauseTrigger(triggerKey);
                boolean triggerDeleted = scheduler.unscheduleJob(triggerKey);
                if (triggerDeleted) {
                    log.info("已删除触发器: {}(组:{})", triggerKey.getName(), triggerKey.getGroup());
                }
            }
        }

        // 2. 删除所有作业
        for (String jobGroupName : scheduler.getJobGroupNames()) {
            GroupMatcher<JobKey> jobGroupMatcher = GroupMatcher.jobGroupEquals(jobGroupName);
            Set<JobKey> jobKeys = scheduler.getJobKeys(jobGroupMatcher);

            for (JobKey jobKey : jobKeys) {
                scheduler.pauseJob(jobKey);
                boolean jobDeleted = scheduler.deleteJob(jobKey);
                if (jobDeleted) {
                    log.info("已删除作业: {}(组:{})", jobKey.getName(), jobKey.getGroup());
                }
            }
        }
        log.info("所有备份任务清理完成");
    }
}

6.当然,也可以对任务进行增删改查,开放成api接口

比如: // 既然新增了备份任务配置,那么需要向Quartz,注册,并调度这个任务

java 复制代码
/**
     * 调度单个备份任务(防残留、防重复,确保新任务生效)
     */
    private void scheduleSingleBackupTask(DeviceBackupConfigPO config) throws SchedulerException {
        Long taskId = config.getId();
        String jobId = "job-" + taskId;
        String triggerId = "trigger-" + taskId;
        JobKey jobKey = new JobKey(jobId);
        TriggerKey triggerKey = new TriggerKey(triggerId);

        try {
            // 额外清理:防止极端情况下的残留(触发器/作业未删干净)
            if (scheduler.checkExists(triggerKey)) {
                scheduler.unscheduleJob(triggerKey);
                LogUtil.info(log, "scheduleSingleBackupTask", "【清理残留】触发器:{}(任务ID:{})", triggerId, taskId);
            }
            if (scheduler.checkExists(jobKey)) {
                scheduler.deleteJob(jobKey);
                LogUtil.info(log, "scheduleSingleBackupTask", "【清理残留】作业:{}(任务ID:{})", jobId, taskId);
            }

            // 1. 构建JobDetail(携带任务参数)
            JobDataMap jobDataMap = new JobDataMap();
            jobDataMap.put("backupTask", JSONUtil.toJsonStr(config)); // 任务参数转JSON(避免序列化问题)
            JobDetail jobDetail = JobBuilder.newJob(DynamicBackupJob.class) // 你的任务执行类
                    .withIdentity(jobKey)
                    .setJobData(jobDataMap)
                    .requestRecovery(true) // 服务重启后恢复任务
                    .build();

            // 2. 构建Trigger(基于新Cron)
            CronScheduleBuilder cronSchedule = CronScheduleBuilder.cronSchedule(config.getCronExpression())
                    .withMisfireHandlingInstructionDoNothing(); // 错过执行时不补执行(根据业务调整)
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerKey)
                    .withSchedule(cronSchedule)
                    .build();

            // 3. 调度任务
            scheduler.scheduleJob(jobDetail, trigger);
            LogUtil.info(log, "scheduleSingleBackupTask",
                    "【调度成功】任务ID:{},管理IP:{},Cron:{}",
                    taskId, config.getManagementIp(), config.getCronExpression());

        } catch (Exception e) {
            String errorMsg = String.format("【调度失败】任务ID:%s,异常:%s", taskId, e.getMessage());
            LogUtil.error(log, "scheduleSingleBackupTask", errorMsg, e);
            throw new SchedulerException(errorMsg, e);
        }
    }

如果删除了一个任务,

java 复制代码
 @Override
    public boolean deleteBackupConfig(List<Long> ids) {
        try {
            List<NetworkDeviceBackupConfigPO> list = networkDeviceBackupConfigService.listByIds(ids);
            for (NetworkDeviceBackupConfigPO po : list) {
                String jobId = "job-" + po.getId();
                String triggerId = "trigger-" + po.getId();
                JobKey jobKey = new JobKey(jobId);
                TriggerKey triggerKey = new TriggerKey(triggerId);
                // 检查并删除触发器
                if (scheduler.checkExists(triggerKey)) {
                    scheduler.unscheduleJob(triggerKey); // 删除触发器
                    LogUtil.info(log, "deleteBackupConfig", "【成功删除】触发器: {}", triggerId);
                } else {
                    LogUtil.info(log, "deleteBackupConfig", "【跳过删除】未找到对应的触发器ID: {}", triggerId);
                }

                // 检查并删除作业
                if (scheduler.checkExists(jobKey)) {
                    scheduler.deleteJob(jobKey); // 删除作业
                    LogUtil.info(log, "deleteBackupConfig", "【成功删除】作业: {}", jobId);
                } else {
                    LogUtil.info(log, "deleteBackupConfig", "【跳过删除】未找到对应的作业ID: {}", jobId);
                }
            }
            return networkDeviceBackupConfigService.removeBatchByIds(ids);

        } catch (Exception e) {
            LogUtil.error(log, "deleteBackupConfig", "deleteBackupConfig error:{}", e);
            throw new BusinessException("删除备份任务配置出现错误:" + e.getMessage());
        }
    }

如果是编辑某个任务,那就先删,后增

相关推荐
invicinble15 小时前
对于spring的bean应该有哪些领域的认识
java·后端·spring
梦想的旅途215 小时前
实现企微外部群主动发送接口:从 0 到 1 实现主动给客户发送的业务实战
java·开发语言·企业微信
是宇写的啊15 小时前
博客系统-小项目
java·数据库·spring boot·mybatis
he___H15 小时前
leetcode100-合并区间
java·数据结构·算法
nbsaas-boot15 小时前
Drools 规则引擎实战:原理、规则语法、数据库动态规则与企业级玩法
java·数据库·python
Hwang25216 小时前
Spring 框架 -01 -单例池的一些理解
java
AI人工智能+电脑小能手16 小时前
【大白话说Java面试题 第74题】【Mysql篇】第4题:InnoDB 和 MyISAM 的数据文件存储区别?
java·开发语言·mysql·面试
qq_25183645716 小时前
基于java Web快乐岛儿童网站设计与实现
java·开发语言·前端
憧憬成为java架构高手的小白16 小时前
苍穹外卖--day11数据统计-图形报表(管理端)
java·spring boot·echarts