Quartz任务调度,一张表实现任务管理

·  阅读 1468
Quartz任务调度,一张表实现任务管理

简介

java后端入门新手,对知识内容理解较浅,如文章内容有误,请各位大佬多多指点。本篇文章适用于对quartz有一定了解的入门新手,且并没有采用quartz官方的持久化方式,是在结合工作需求的基础上完成的quartz任务调度的任务添加与修改,以及对时间表达式的修改等功能


任务调度框架Quartz基本概念如下:
Job/JobDetail: 任务
任务指的是你要做的事,也是任务调度的核心,比如我想要每隔一个小时喝一杯水,那么任务就是“喝水”
Trigger: 触发器
触发器指的是任务触发的时间,也就是任务在什么时间开始执行。例如上一句喝水的例子,“每隔一个小时”,这就是一个触发器,触发器具有两种定时类型,一种简单定时,一种使用Cron时间表达式进行复杂定时。

image.png
Scheduler: 调度器
调度器是用来接收任务和触发器的,可以将对应的任务和触发器整合在一起,还可以实现远程和集群
Cron时间表达式: 常用于任务调度中,用来完成复杂的周期性时间表达,例如 (0 0/1 * * * ? )就代表每一分钟执行一次任务,建议使用cron表达式生成器,非常不建议手写

简单使用

一.传统SpringBoot配置使用

这个方法是使用了SpringBoot与Quartz的整合方式,并将任务和触发器用Bean组件的方式进行自动配置。

1.导入依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
复制代码

2.新建任务类

在SpringBoot2.x之前,使用Quartz建立任务类需要实现Job接口,重写excute()方法,而在SpringBoot中,一般是继承QuartzJobBean类,重写executeInternal() 方法,方法内就是任务具体执行的内容

@Slf4j
public class MyJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("MyJob开始运行啦!");

    }
}
复制代码

3.新建配置类

需要创建一个Job,关联对应的实体类,每个Job和Trigger都可以设置分组,Job/Trigger有组group和名字name的联合唯一标识,在一个触发器中不允许有两个相同标识的Job/Trigger

  • Job和trigger都可以进行分组,相对应的任务和触发器可以分到同一组
  • 可以使用Trigger的group和name来获取对应的触发器
@Configuration
public class QuartzConfig {
    
    @Bean
    public JobDetail jobDetail(){

        return JobBuilder.newJob(MyJob.class)//加载任务实体(必须)
                .withIdentity("myJob","group1")//设置分组
                .storeDurably(true)//true会使任务在没有对应触发器的情况下一直存在
                .build();
    }

    @Bean
    public Trigger trigger(){
        //设置Cron时间表达式,此处也可用SimpleScheduleBuilder
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/30 0/1 * * * ?");
        //建立触发器
        return TriggerBuilder.newTrigger()
                .forJob(jobDetail())//与Job进行绑定
                .withIdentity("trigger","group1")//分组
                .withSchedule(cronScheduleBuilder)//绑定触发时间
                .build();//构建
    }
    
}
复制代码

程序启动时会自动加载@Configration配置文件,以及被@Bean标记的方法,至此,任务调度启动。 程序启动时会实现以下内容,这是代表着Quartz的内容存储在内存中,没有进行持久化,并显示了线程数等信息

  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.
复制代码

二.复杂多任务调度

Gitee源码地址:quartz任务调度

1.简介

多任务调度自然不可能是并发,而是采用了多线程异步并行的方法来进行多个任务的调度。Quartz有自带的线程池,默认的核心线程数是10个,也就是说,最多支持10个任务的同时调度;当十个任务要进行切换时,线程池会释放空余的线程,并从第一个线程开始,循环往复。不过不建议在没有配置线程池的情况下运行10个任务,可能会造成线程堵塞的情况。

2.非默认持久化

本篇文章的多任务方法并没有使用默认的持久化方式,而是采用了自建表的方式来存储任务信息。如需要使用默认的持久化方式,请参考Quartz官方文档建表说明;如想简单使用自建表,主要需要如下内容:

image.png

3.创建数据表

每个任务类都继承于QuartzJobBean类,并重写对应的方法
总体需求:完成多个任务的定时调度,并且能够在数据表中添加任务和修改任务执行周期

image.png
任务类的创建需要与数据表中的数据互相对应,并自行配置任务类的结果
数据表内容如下:

image.png
其中实体类路径一定要存在对应的实体类,在表中添加任务之前一定要确保你想要添加的Job存在于你的程序中。

4.程序结构

程序结构如下:

image.png
其中,核心代码为控制层的两个类

(1)实体类:Job.java

import lombok.Data;

@Data
public class Job {

    private int id;   //id自增
    private String group;   //分组
    private String name;    //任务名
    private String className;  //任务类路径
    private String cron;  //时间表达式

}
复制代码

(2)任务类

MyJob/MyJob2
注意分开写。在数据库添加任务之前,记得建立对应的任务类。任务类中的executeInternal()方法中就是你的任务要执行的操作,这里以日志代替,进行了省略。

@Slf4j
public class MyJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("MyJob开始运行啦!");
		
    }
}


@Slf4j
public class MyJob2 extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("MyJob2开始运行啦!");
    }
}
复制代码

(3)核心类

QuartzJob.java
这个类是任务调度的核心类,主要包括四个方法,代码中会详细的介绍方法的作用

@Slf4j
@Component
public class QuartzJob {

    @Resource
    Scheduler scheduler;

    //这里是获取数据库表的数据Service
    @Resource
    JobService jobService;

    public void start() throws SchedulerException {
        scheduler.start();
    }

    @Scheduled(cron = "0 0/1 * * * ?")
    public void loadJob() throws Exception {
        //数据库遍历数据
        List<Job> list = jobService.findAllJob();

        for (Job job : list){

            //获取一个要修改的触发器的资料,身份,key
            TriggerKey triggerKey = new TriggerKey(job.getName(),job.getGroup());

            //根据key获取要更改的具体的CronTrigger触发器
            CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);

            //判定,如果没有对应的触发器,就建立任务和触发器
            if (null == cronTrigger){
                createJob(scheduler,job);
            }else {
                //如果已经有对应的触发器,那么就已存在任务,程序转入触发器Cron监测方法
                updateJob(job,job.getCron());
            }
        }
    }

    public void createJob(Scheduler scheduler,Job job) throws SchedulerException {

        Class<org.quartz.Job> clazz;
        try {
            //这就是调度器要执行的类
            clazz = (Class<org.quartz.Job>) Class.forName(job.getClassName());
        } catch (ClassNotFoundException e1) {
            throw new RuntimeException(e1);
        }

        //批量创建任务
        JobDetail jobDetail = JobBuilder.newJob(clazz)
                .withIdentity(job.getName(),job.getGroup())
                .build();
        //获取当前Cron时间
        String cron = job.getCron();
        //创建表达式,构建触发器
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing();
		//创建触发器
        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withSchedule(cronScheduleBuilder)
                .withIdentity(job.getName(),job.getGroup())
                .build();
        //调度器整合任务与对应的触发器
        scheduler.scheduleJob(jobDetail,cronTrigger);
        log.info("当前job创建成功:{}",job.getName());

    }

    /**
     * 1.此方法会对触发器进行更新,主要更新Cron表达式
     * 2.此方法会判定当前触发器的时间较上一分钟是否存在修改
     * 3.只有判定存在修改时,才会对表达式进行修改
     *
     * @param job   任务实体类
     * @param time  修改后的Cron表达式
     * @throws Exception
     */
    public void updateJob(Job job,String time) throws Exception{

        //创建一个要修改的触发器的资料,身份
        TriggerKey triggerKey = new TriggerKey(job.getName(),job.getGroup());

        //获取要更改的具体的CronTrigger触发器
        CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        //获取当前时间
        String oldTime = cronTrigger.getCronExpression();
        if (!oldTime.equals(time)){
            //用修改后的时间更新触发器
            CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(time).withMisfireHandlingInstructionDoNothing();
            CronTrigger cronTrigger1 = TriggerBuilder.newTrigger()
                    .withIdentity(job.getName(),job.getGroup())
                    .withSchedule(cronScheduleBuilder)
                    .build();
            //调度器整合新的触发器
            scheduler.rescheduleJob(triggerKey,cronTrigger1);
            log.info("监听到修改,任务“{}”发生修改,修改前执行时间为:{} ,修改后执行时间为: {}",job.getName(),oldTime,time);
        }

    }

}
复制代码

这里按照从底到外的顺序来说明,createJob/updateJob -> loadJob ->start
createJob(): 这里遍历任务,创建任务JobDetail以及对应的触发器Trigger
updateJob(): 这个方法用于更新触发器,当对时间表达式进行了修改时,用来更新对应的任务类的触发器。
loadJob (): 这个方法用于执行任务的创建和触发器的修改,方法执行时会开始检查对应的name和group,来查找是否建立了这个任务及其触发器。所以在数据表中添加任务时会发现队友对应的触发器,就会去新建任务以及触发器。如果已经存在,就会转入触发器的监听方法,只有当修改了时间表达式后,才会修改触发器。这个方法加入@Scheduled注解,让它能够在固定的时间运行一次,这里设置的是一分钟。
start(): 是任务调度的开启方法,里面启动了调度器,之所以单独分开写,是因为要确保调度器只启动一次,并且一直保持稳定运行


QuartzStart.java

@Slf4j
@Component
public class QuartzStart implements ApplicationListener<ApplicationEvent> {

    private static boolean loaded = false;

    @Resource
    QuartzJob quartzJob;

    //这个方法会在容器加载时执行一次
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        if(applicationEvent instanceof ContextRefreshedEvent){
            if(!loaded){//避免多次执行
                loaded = true;
                //定时任务启动
                try {
                    
                    //第一遍加载全部任务
                    quartzJob.loadJob();
                    //全部任务都开始执行
                    quartzJob.start();
                    System.out.println("任务已经启动...啦啦啦啦啦啦啦");

                } catch (SchedulerException se) {
                    se.printStackTrace();
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            }

        }
    }
}
复制代码

这个类的方法只会在服务器启动时加载执行一次,目的是加载第一遍任务以及启动任务调度

(4)执行结果

刚开始会创建所有任务,并按照任务表中的Cron表达式执行,初始都为10s执行一次

image.png
在修改表达式后,任务二改为15秒执行一次

image.png
这里要进一步说明的是,在时间修改监听,也就是@Scheduled注解的方法,时间如果设为整点(每分钟执行),并且与任务的执行重合(每十秒,第六次为一个整分钟),由于修改触发器与任务类的执行是分开的两个线程,所以当时间重合时,即使已经修改了时间,但是任务类还是会按照修改前的时间执行一次,第二次会按照新的时间执行,为了避免这样的情况,建议合理安排时间。
本篇文章中对任务与时间的修改在数据库中完成,也可结合前端,封装为接口,动态修改数据库中的数据,从而实现对定时任务的控制
本文完。

新手写文,希望文章能让大家有所收获,如有任何疑问或建议,欢迎在评论区讨论!

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改