SpringBoot+Quartz实现动态定时任务(非数据库模式)

在日常工作中,经常会遇到一些定时任务,比如定时发邮件、异构数据库同步数据等。目前比较常用的是SpringBoot+Quartz实现动态定时任务的Web项目,这种模式通常需要建立定时任务相关的表。本例为SpringBoot+Quartz实现动态定时任务的非Web项目,可以根据项目启动的参数来决定执行配置文件中的某些任务,并且所有的任务都配置在配置文件中,从而避免建数据库的麻烦。

一、流程说明:

程序启动时注册QuartzConfig配置类(该配置类将读取quartz.properties中的配置、任务信息、初始化调度器、job实例工厂等),然后通过CommandLineRunner的实现类,重写其run()方法,该方法根据启动传入的参数判断哪些配置任务信息需要进行执行,并调用QuartzManager工具类启动相应任务。

二、代码实例:

1.引入相关依赖

   
        
            org.springframework.boot
            spring-boot-starter
        

        
        
            org.springframework.boot
            spring-boot-starter-quartz
        
        
        
            org.projectlombok
            lombok
            provided		
        
        
        
            cn.hutool
            hutool-all
            5.1.0
        
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

    
    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                
                    true
                
            
        
    

2.【必须】quartz框架配置文件quartz.properties,主要配置quartz线程池、任务列表等

说明:由于本例采用非Web项目,故application.yml配置文件可以不配。实际业务中可以配置些动态数据源,实现定时任务的业务逻辑

#[非必须]设置调度器实例名称,默认QuartzScheduler
org.quartz.scheduler.instanceName=quartz-scheduler
#[非必须]设置调度器实例ID,默认值NON_CLUSTERED
org.quartz.scheduler.instanceId=AUTO

#[必须]设置线程池实现类(一般使用SimpleThreadPool定长线程池)
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#[必须]设置线程数(无默认值,一般设置为1-100的整数)
org.quartz.threadPool.threadCount=5
#[非必须]设置线程的优先级(最大为9,最小为1,默认为5)
org.quartz.threadPool.threadPriority=5
#[非必须]设置是否为守护线程(默认为true)
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
#[非必须]线程前缀(默认为调度器实例名称_Worker)
org.quartz.threadPool.threadNamePrefix=quartz-scheduler-thread

#[非必须]设置schedule相关信息存储方法(默认存储在内存中)
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore
#[非必须]最大能忍受的触发超时时间(默认为60秒)
org.quartz.jobStore.misfireThreshold=60000

##################################配置定时任务##################################
#jobName:任务名称,建议英文或任务描述拼音码首拼[必须]
#jobDesc:任务中文描述[必须]
#classMethod:任务执行的方法[必须]
#jobCron:任务的Cron表达式[必须]
#第1个定时任务
springboot.quartz.scheduledJobs[0].jobName=job1
springboot.quartz.scheduledJobs[0].jobDesc=测试任务1
springboot.quartz.scheduledJobs[0].classMethod=com.wwu.jobs.FirstJob
springboot.quartz.scheduledJobs[0].jobCron=0/10 * * * * ?

#第2个定时任务
springboot.quartz.scheduledJobs[1].jobName=job2
springboot.quartz.scheduledJobs[1].jobDesc=测试任务2
springboot.quartz.scheduledJobs[1].classMethod=com.wwu.jobs.SecondJob
springboot.quartz.scheduledJobs[1].jobCron=0/15 * * * * ?

#第3个定时任务
springboot.quartz.scheduledJobs[2].jobName=job3
springboot.quartz.scheduledJobs[2].jobDesc=测试任务3
springboot.quartz.scheduledJobs[2].classMethod=com.wwu.jobs.ThirdJob
springboot.quartz.scheduledJobs[2].jobCron=0 05 19 ? * 2

3.【非必须】slf4j日志分级切割配置文件logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>


    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    
    
        
        
            ${sysLog.pattern}
        
    

    
    
    
        
        
            
            INFO
            
            ACCEPT
            
            DENY
        
        
        ${sysLog.logDir}/${sysLog.logName}_info.txt
        
        
            
            ${sysLog.logDir}/${sysLog.logName}_info_%d{yyyyMMdd}_%i.txt
            
            ${sysLog.maxHistory}
            
            
                ${sysLog.maxFileSize}
            
        
        
        
            ${sysLog.fileCharset}
            ${sysLog.pattern}
        
    

    
    
        
        
            
            WARN
        
        
        ${sysLog.logDir}/${sysLog.logName}_error.txt
        
        
            
            ${sysLog.logDir}/${sysLog.logName}_error_%d{yyyyMMdd}_%i.txt
            
            ${maxHistory}
            
            
                ${sysLog.maxFileSize}
            
        
        
        
            ${sysLog.fileCharset}
            ${sysLog.pattern}
        
    

    
    
        
        0
        
        ${sysLog.queueSize}
        
    
    
        0
        ${sysLog.queueSize}
        
    

    
    
    
    

    
    
        
        
        
    


















 

4.创建任务实体类ScheduledJob.java,在初始化任务列表时创建任务明细(JobDetail)及触发器(Trigger)

package com.wwu.entity;

import lombok.Data;

/**
 * @description: 任务实体类
 * @author 一蓑烟雨
 * @version 1.0.0
 * @date 2023-04-16 16:53
 */
@Data
public class ScheduledJob {
    /** 任务ID */
    private String jobId;
    /** 任务名称 */
    private String jobName;
    /** 任务描述 */
    private String jobDesc;
    /** 任务表达式 */
    private String jobCron;
    /** 任务表达式中文 */
    private String jobCronZh;
    /**  任务分组编码*/
    private String jobGroup;
    /** 任务分组名称 */
    private String jobGroupName;
    /** 执行方法 */
    private String classMethod;
    /** 任务执行状态(Y,执行成功;N,执行失败;空,未执行) */
    private String jobStatus;
    /** 任务可用状态(Y:可用;N:不可用) */
    private String enabledFlag;
    /** 下次执行时间 */
    private String nextExecTime;
}

5.创建quartz配置类quartzConfig.java,主要用来配置线程池、调度器、job实例工厂等

package com.wwu.config;

import com.wwu.entity.ScheduledJob;
import lombok.Data;
import org.quartz.Scheduler;
import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
import java.util.Properties;

/**
 * @description: quartz配置类,配置线程池、调度器、job实例工厂等
 * @author 一蓑烟雨
 * @version 1.0.0
 * @date 2023-04-16 18:08
 */
@Data
@Configuration
@PropertySource("classpath:quartz.properties")
@ConfigurationProperties(prefix = "springboot.quartz")
public class QuartzConfig {
    /** 注入quartz.properties配置文件中的任务 */
    private List scheduledJobs;
    @Resource
    private JobFactory jobFactory;

    /**
     * @description: SchedulerFactoryBean工厂
     * @return org.springframework.scheduling.quartz.SchedulerFactoryBean
     * @author 一蓑烟雨
     * @date 2023/4/16 18:10
    */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        //创建 SchedulerFactoryBean 实例
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        try {
            //设置是否覆盖已存在的job,为True表示有相同名称和分组的触发器和任务存在时则替换
            schedulerFactoryBean.setOverwriteExistingJobs(true);
            //设置是否自动启动Scheduler
            schedulerFactoryBean.setAutoStartup(true);
            //设置quartz属性
            schedulerFactoryBean.setQuartzProperties(quartzProperties());
            //设置任务调度器
            schedulerFactoryBean.setJobFactory(jobFactory);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return schedulerFactoryBean;
    }

    /**
     * @description: 读取quartz配置文件中配置相关属性
     * @return java.util.Properties
     * @author 一蓑烟雨
     * @date 2023/4/17 17:18
     */
    @Bean
    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        //在quartz.properties中的属性被读取并注入后再初始化对象
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }
    /**
     * @description: 初始化schedule任务调度器
     * @return org.quartz.Scheduler
     * @author 一蓑烟雨
     * @date 2023/4/16 18:12
    */
    @Bean
    public Scheduler scheduler() {
        return schedulerFactoryBean().getScheduler();
    }
}

6.创建job实例工厂JobFactory.java

package com.wwu.config;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

/**
 * @description: 创建job实例工厂
 * @author 一蓑烟雨
 * @version 1.0.0
 * @date 2023-04-16 18:31
 */
@Component
public class JobFactory extends AdaptableJobFactory {
    @Resource
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        //调用父类的方法
        Object jobInstance = super.createJobInstance(bundle);
        //进行注入
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

7.创建CommandLineRunner接口的实现类QuartzCommandRunner.java,作用为启动项目时预先加载配置文件中的所有定时任务,并根据启动参数启动相应任务

package com.wwu.config;

import com.wwu.entity.ScheduledJob;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @description: 启动项目时预先加载配置文件中的所有定时任务,并根据启动参数启动相应任务
 * CommandLineRunner中的Order表示在项目启动时预先加载的类
 * @author 一蓑烟雨
 * @version 1.0.0
 * @date 2023-04-16 18:13
 */
@Slf4j
@Component
@Order(1)
public class QuartzCommandRunner implements CommandLineRunner {
    @Resource
    private QuartzManager quartzManager;
    @Resource
    private QuartzConfig quartzConfig;

    /**
     * @description: 重写run方法
     * @param [args]
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/17 15:37
    */
    @Override
    public void run(String... args) throws Exception {
        try {
            //初始化需要启动的任务列表
            List jobList = initSchedule(args);
            for (ScheduledJob scheduledJob : jobList) {
                //执行任务
                quartzManager.startJob(scheduledJob);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @description: 初始化需要执行的任务列表
     * @param [args]
     * @return java.util.List
     * @author 一蓑烟雨
     * @date 2023/4/17 15:37
    */
    public List initSchedule(String... args) throws SchedulerException {
        List jobs = new ArrayList();
        try {
            log.info("...根据参数加载配置文件quartz.properties中任务列表开始...");
            //1.遍历启动程序时传入的接口类型参数
            String[] typeParam = null;
            for (String arg : args) {
                if (arg.startsWith("type")){
                    typeParam =  arg.substring(5).split(",");
                    break;
                }
            }
            //2.遍历quartz.properties配置文件中任务列表
            for (ScheduledJob scheduledJob : quartzConfig.getScheduledJobs()) {
                //如果启动传入了参数,则按照参数加载启动任务
                if (typeParam != null && typeParam.length > 0) {
                    //遍历参数值和任务列表,若相同则加入到启动任务列表中
                    Arrays.stream(typeParam).forEach(param -> {
                        if (param.trim().equals(scheduledJob.getJobName())) {
                            jobs.add(scheduledJob);
                            log.info("......" + scheduledJob);
                        }
                    });
                } else {
                    //没有启动参数,默认启动配置文件中的所有任务
                    jobs.add(scheduledJob);
                    log.info("......" + scheduledJob);
                }
            }
            log.info("......共加载{}条任务需要执行......", jobs.size());
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("...根据参数加载配置文件quartz.properties中任务列表结束...");
        return jobs;
    }
}

8.创建quartz定时任务工具类QuartzManager.java,对任务进行管理(创建、启动、修改、删除、暂停)

package com.wwu.config;

import com.wwu.entity.ScheduledJob;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * @description: quartz定时任务工具类,对任务进行管理(创建、启动、修改、删除、暂停)
 * @author 一蓑烟雨
 * @version 1.0.0
 * @date 2023-04-16 17:11
 */
@Component
public class QuartzManager {
    /** 注入QuartzConfig中定义的任务调度器scheduler */
    @Resource
    private Scheduler scheduler;

    /**
     * @description: 获取所有任务信息
     * @return java.util.List
     * @author 一蓑烟雨
     * @date 2023/4/16 18:59
    */
    public List getAllJobInfo() throws SchedulerException {
        List jobList = new ArrayList();
        GroupMatcher matcher = GroupMatcher.anyJobGroup();
        Set jobKeys = scheduler.getJobKeys(matcher);
        for(JobKey jobKey: jobKeys){
            List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
            for(Trigger trigger: triggers){
                ScheduledJob job = new ScheduledJob();
                job.setJobName(jobKey.getName());
                job.setJobGroup(jobKey.getGroup());
                job.setJobDesc(trigger.getDescription());
                Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                job.setJobStatus(triggerState.name());
                if(trigger instanceof  CronTrigger){
                    CronTrigger cronTrigger= (CronTrigger) trigger;
                    job.setJobCron(cronTrigger.getCronExpression());
                }
                jobList.add(job);
            }
        }
        return jobList;
    }

    /**
     * @description: 获取某个任务的信息
     * @param scheduledJob
     * @return java.lang.String
     * @author 一蓑烟雨
     * @date 2023/4/16 18:07
    */
    public String getJobInfo(ScheduledJob scheduledJob) throws SchedulerException {
        TriggerKey triggerKey = new TriggerKey(scheduledJob.getJobName(), scheduledJob.getJobGroup());
        System.out.println("triggerKey:"+triggerKey);
        CronTrigger cronTrigger= (CronTrigger) scheduler.getTrigger(triggerKey);
        return String.format("time:%s,state:%s",cronTrigger.getCronExpression(),
                scheduler.getTriggerState(triggerKey).name());
    }

    /**
     * @description: 获取任务数量
     * @return java.lang.String
     * @author 一蓑烟雨
     * @date 2023/4/16 18:07
     */
    public int getJobSize() throws SchedulerException {
        GroupMatcher matcher = GroupMatcher.anyJobGroup();
        Set jobKeys = scheduler.getJobKeys(matcher);
        return jobKeys.size();
    }

    /**
     * @description: 获取触发器状态
     * @param scheduledJob
     * @return NONE:不存在;NORMAL:正常;PAUSED:暂停;COMPLETE:完成;ERROR:错误;BLOCKED:阻塞
     * @author 一蓑烟雨
     * @date 2023/4/17 18:56
     */
    public String getTriggerState(ScheduledJob scheduledJob) throws SchedulerException {
        TriggerKey triggerKey = new TriggerKey(scheduledJob.getJobName(), scheduledJob.getJobGroup());
        Trigger.TriggerState triggerState = scheduler.getTriggerState(triggerKey);
        return triggerState.name();
    }

    /**
     * @description: 开启某个任务
     * @param scheduledJob
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/16 18:05
    */
    public void startJob(ScheduledJob scheduledJob) throws Exception {
        JobKey jobKey = JobKey.jobKey(scheduledJob.getJobName(), scheduledJob.getJobGroup());
        //不存在则添加任务
        if(!scheduler.checkExists(jobKey)){
            addJobTask(scheduledJob);
        }
        //启动
        scheduler.start();
    }

    /**
     * @description: 添加某个任务
     * @param scheduledJob
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/16 18:01
    */
    public boolean addJobTask(ScheduledJob scheduledJob) throws Exception {
        //利用反射机制获取任务执行类
        Class<? extends Job> jobClass = (Class<? extends Job>)(Class.forName(scheduledJob.getClassMethod()).newInstance().getClass());
        //设置任务明细,调用定义的任务逻辑
        JobDetail jobDetail = JobBuilder.newJob(jobClass)
                //添加认证信息(也可通过usingJobData传参数)
                .withIdentity(scheduledJob.getJobName(), scheduledJob.getJobGroup())
                //执行
                .build();
        //设置任务触发器,cornTrigger规则定义执行规则
        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                //通过键值对方式向job实现业务逻辑传参数
                .usingJobData("jobDesc",scheduledJob.getJobDesc())
                .usingJobData("jobCron",scheduledJob.getJobCron())
                //添加认证信息
                .withIdentity(scheduledJob.getJobName(), scheduledJob.getJobGroup())
                //程序启动后间隔多久开始执行任务
                .startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND))
                //执行Cron表达时
                .withSchedule(CronScheduleBuilder.cronSchedule(scheduledJob.getJobCron()))
                //执行
                .build();
        //把任务明细和触发器注册到任务调度中
        scheduler.scheduleJob(jobDetail, cronTrigger);
        return true;
    }

    /**
     * @description: 修改任务的Cron表达式
     * @param scheduledJob
     * @return boolean
     * @author 一蓑烟雨
     * @date 2023/4/16 17:46
    */
    public boolean modifyJob(ScheduledJob scheduledJob) throws SchedulerException{
        TriggerKey triggerKey = new TriggerKey(scheduledJob.getJobName(), scheduledJob.getJobGroup());
        CronTrigger cronTrigger= (CronTrigger) scheduler.getTrigger(triggerKey);
        String oldTime = cronTrigger.getCronExpression();
        if (!oldTime.equalsIgnoreCase(scheduledJob.getJobCron())){
            CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(scheduledJob.getJobCron());
            CronTrigger trigger=TriggerBuilder.newTrigger()
                    .withIdentity(scheduledJob.getJobName(), scheduledJob.getJobGroup())
                    .withSchedule(cronScheduleBuilder)
                    .build();
            scheduler.rescheduleJob(triggerKey,trigger);
            return true;
        }else{
            return false;
        }
    }

    /**
     * @description: 暂停所有任务
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/16 17:41
    */
    public void pauseAllJob()throws SchedulerException{
        scheduler.pauseAll();
    }

    /**
     * @description: 暂停某个任务
     * @param scheduledJob
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/16 17:41
    */
    public void pauseJob(ScheduledJob scheduledJob)throws SchedulerException{
        JobKey jobKey = JobKey.jobKey(scheduledJob.getJobName(), scheduledJob.getJobGroup());
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);
        if (jobDetail == null) {
            return;
        }
        scheduler.pauseJob(jobKey);
    }

    /**
     * @description: 恢复所有任务
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/16 17:38
    */
    public void resumeAllJob()throws SchedulerException{
        scheduler.resumeAll();
    }

    /**
     * @description: 恢复某个任务
     * @param scheduledJob
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/16 17:39
    */
    public void resumeJob(ScheduledJob scheduledJob)throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(scheduledJob.getJobName(), scheduledJob.getJobGroup());
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);
        if (jobDetail == null) {
            return;
        }
        scheduler.resumeJob(jobKey);
    }

    /**
     * @description: 删除任务
     * @param scheduledJob
     * @return void
     * @author 一蓑烟雨
     * @date 2023/4/16 17:32
    */
    public void deleteJob(ScheduledJob scheduledJob)throws SchedulerException{
        JobKey jobKey = JobKey.jobKey(scheduledJob.getJobName(), scheduledJob.getJobGroup());
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);
        if (jobDetail == null) {
            return;
        }
        scheduler.deleteJob(jobKey);
    }
}

8.依次创建quartz中对应任务的job业务类FirstJob.java、SecondJob.java和ThirdJob.java,这些job业务类需要实现Job接口

package com.wwu.jobs;

import lombok.extern.slf4j.Slf4j;
import org.quartz.*;

/**
 * @description: 定义任务逻辑,需要实现Job接口并重写execute()方法
 * DisallowConcurrentExecution注解用来避免同一个任务由于执行时间过长导致并发执行
 * @author 一蓑烟雨
 * @version 1.0.0
 * @date 2023-04-16 19:51
 */
@Slf4j
@DisallowConcurrentExecution
public class FirstJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        //获取触发器cronTrigger传递的参数
        JobDataMap dataMap = context.getTrigger().getJobDataMap();
        log.info("【{}】任务执行开始,执行频率为:{}",dataMap.get("jobDesc"),dataMap.get("jobCron"));
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("【{}】任务执行结束",dataMap.get("jobDesc"));
    }
}

9.在IDEA开发工具配置参数或将项目打包成jar并通过命令行传参执行(如type=job1,job2),即可看到传入参数对应的任务已开始执行



展开阅读全文

页面更新:2024-03-12

标签:触发器   初始化   线程   烟雨   实例   加载   参数   模式   数据库   方法   项目   动态   列表

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top