zoukankan      html  css  js  c++  java
  • Springcloud学习笔记37任务调度框架Quartz 使用(Cron表达式)与@scheduled注解定时任务

    1.Quartz简介

    Quartz是一款Java编写的开源任务调度框架,同时它也是Spring默认的任务调度框架。基于定时、定期的策略来执行任务是它的核心功能,比如x年x月的每个星期五上午8点到9点,每隔10分钟执行1次。

    它的作用其实类似于Java中的Timer定时器以及JUC中的ScheduledExecutorService调度线程池,当然Quartz作为一个独立的任务调度框架无疑在这方面表现的更为出色,功能更强大,能够定义更为复杂的执行规则。

    Quartz中主要用到了:Builder建造者模式、Factory工厂模式以及组件模式。

    Quartz有3个核心要素:任务(Job)、触发器(Trigger)、调度器(Scheduler)。

    任务(Job,你需要做什么事?我们需要将具体的业务逻辑写到实现了Job接口的实现类中)

    触发器Trigger,你什么时候去做?它定义了任务的执行规则,什么时候开始执行,什么时候结束执行)

    调度器Scheduler,你什么时候需要去做什么事?通过传入的任务Job和触发器Trigger,以指定的规则执行任务)。

    Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠自己做所有的工作,而是依赖框架内一些非常重要的部件。Quartz不仅仅是线程和线程池管理。为确保可伸缩性,Quartz采用了基于多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度器用来执行预定的作业。这就是Quartz能并发运行多个作业的原理。Quartz依赖一套松耦合的线程池管理部件来管理线程环境。

    2.Quartz的体系结构

    JobDetail:quartz每次都会直接创建一个JobDetail,同时创建一个Job实例,它不直接接受一个Job的实例,但是它接受一个Job的实现类,通过new instance()的反射方式来实例一个Job,在这里Job是一个接口,我们需要自己编写类去实现这个接口。

    Trigger : 它由SimpleTrigger和CronTrigger组成,SimpleTrigger实现类似Timer的定时调度任务,CronTrigger可以通过cron表达式实现更复杂的调度逻辑·。

    SimpleTrigger很方便,如果你需要一次性执行(只是在一个给定时刻执行job),或者如果你需要一个job在一个给定的时间,并让它重复N次,并在执行之间延迟T。
    CronTrigger是有用的,如果你想拥有引发基于当前日历时间表,如每个星期五,中午或在每个月的第十天 10:15。
    Scheduler:调度器,JobDetail和Trigger可以通过Scheduler绑定到一起。

    3. Quartz重要组成部分

    3.1 Job接口

    Job接口很简单,只有一个execute方法,这是我们自己的具体业务逻辑的入口。

    要创建一个任务,我们需要编写一个实现该接口的具体任务类:

    public class HelloJob implements Job{
    
        public void execute(JobExecutionContext context) throws JobExecutionException {
        //编写我们自己的业务逻辑
        }
    }

    3.2 JobDetail

    JobDetail描述了Job对象的基本信息,主要包含四个重要的属性:name(Job的名称)、group(Job的组名称)、jobClass(Job对应的类)以及jobDataMap(存储一些用户自定义的信息或对象)。在SchedulerJob的名称name和组group组合必须是唯一的。

    quartz每次都会直接创建一个JobDetail,同时创建一个Job实例,它不直接接受一个Job的实例,但是它接受一个Job的实现类,通过new instance()的反射方式来实例一个Job.可以通过下面的方式将一个Job实现类绑定到JobDetail中

    // 指明job的名称,所在组的名称,以及绑定job类
    JobDetail job = JobBuilder.newJob(HelloJob.class) //绑定job类
        .withIdentity("JobName", "JobGroupName") //指明job的名称为JobName,所在组的名称为JobGroupName
        .usingJobData(jobDataMap) //传递job执行时需要的数据
        .build();

    3.3 JobBuiler

    主要是用来创建jobDeatil实例

    3.4 JobDataMap数据存储类

    通过查看JobDetailTriggerJobExecutionContext的源码可以发现,他们中都存在JobDataMap这个类型,它是以Map的形式存储我们的一些自定义数据的。当Job对象的execute方法被调用时,JobDataMap会通过JobExecutionContext传递给execute方法,它可以用来装载任何可序列化的数据对象。JobDataMap实现了Java中的Map接口,提供了一些自己的方法来存储数据。

    这是JobDataMap的继承树:

    可以看到JobDataMapDirtyFlagMap的子类,而DirtyFlagMap实际实现了Java中的java.util.Map类型:

    // DirtyFlagMap是java.util.Map接口的子类
    public class DirtyFlagMap<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { }

    一句话:把它当Java中的map来用就对了!

    3.5 trigger

    前文讲到它主要用来执行Job实现类的业务逻辑的,我们可以通过下面的代码来创建一个Trigger实例

    CronTrigger trigger = (CronTrigger) TriggerBuilder
                    .newTrigger()
                    .withIdentity("myTrigger", "group1")    //创建一个标识符
                    .startAt(date)//什么时候开始触发
                    //每秒钟触发一次任务
                    .withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *"))
                    .build();

    3.6 Scheduler

    创建Scheduler有两种方式
    通过StdSchedulerFactory来创建

    SchedulerFactory sfact=new StdSchedulerFactory();
    Scheduler scheduler=sfact.getScheduler();

    通过DirectSchedulerFactory来创建

    DiredtSchedulerFactory factory=DirectSchedulerFactory.getInstance();
    Scheduler scheduler=factory.getScheduler();

    Scheduler 配置参数一般存储在quartz.properties中,我们可以修改参数来配置相应的参数。通过调用getScheduler()方法就能创建和初始化调度对象。

    Scheduler的主要函数介绍:

    Date schedulerJob(JobDetail,Trigger trigger);返回最近触发的一次时间
    void standby()暂时挂起
    void shutdown()完全关闭,不能重新启动了
    shutdown(true)表示等待所有正在执行的job执行完毕之后,再关闭scheduler
    shutdown(false)即直接关闭scheduler

    在这里我们不得不提一下quartz.properties这个资源文件,在org.quartz这个包下,当我们程序启动的时候,它首先会到我们的根目录下查看是否配置了该资源文件,如果没有就会到该包下读取相应信息,当我们咋实现更复杂的逻辑时,需要自己指定参数的时候,可以自己配置参数来实现。下面我们简单看一下这个资源文件:

    org.quartz.scheduler.instanceName: DefaultQuartzScheduler
    org.quartz.scheduler.rmi.export: false
    org.quartz.scheduler.rmi.proxy: false
    org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
    
    org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
    org.quartz.threadPool.threadCount: 10
    org.quartz.threadPool.threadPriority: 5
    org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
    
    org.quartz.jobStore.misfireThreshold: 60000
    
    org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore

    该资源文件主要组成部分:
    ①调度器属性
    ②线程池属性
    ③作业存储设置
    ④插件设置

    <1>调度器属性:
    org.quartz.scheduler.instanceName属性用来区分特定的调度器实例,可以按照功能用途来给调度器起名。
    org.quartz.scheduler.instanceId属性和前者一样,也允许任何字符串,但这个值必须是在所有调度器实例中是唯一的,尤其是在一个集群当中,作为集群的唯一key,假如你想quartz帮你生成这个值的话,可以设置我Auto

    <2>线程池属性:
    threadCount设置线程的数量

    threadPriority设置线程的优先级

    org.quartz.threadPool.class 线程池的实现

    <3>作业存储设置:
    描述了在调度器实例的声明周期中,job和trigger信息是怎么样存储的

    <4>插件配置:
    满足特定需求用到的quartz插件的配置

    4. Cron表达式 

    在这里,我们着重讲解一下cron表达式,quartz之所以能够实现更加复杂的业务逻辑,主要在依赖于cron表达式。 
    cron表达式编写的顺序一次是”秒 分 时 日 月 周 年”。 

    在线Cron生成表达式:http://cron.qqe2.com/

    cron 一定有七位数,最后一位是年,SpringBoot 定时方案只需要设置六位即可:

    • 第一位, 表示秒, 取值是0 ~ 59,允许的字符为,- * /
    • 第二位, 表示分. 取值是0 ~ 59,允许的字符为 ,- * /
    • 第三位, 表示小时, 取值是0 ~ 23,允许的字符为,- * /
    • 第四位, 表示天/日, 取值是0 ~ 31,允许的字符为,- * ? / L W C
    • 第五位, 表示月份, 取值是1 ~ 12,允许的字符为 ,- * /
    • 第六位, 表示星期, 取值是1 ~ 7, 星期一,星期二…, 还有 1 表示星期日,允许的字符为,- * ? / L W C
    • 第七位, 年份, 可以留空, 取值是1970 ~ 2099

    cron中,还有一些特殊的符号,含义如下:

    • (*) 星号,可以理解为每的意思,每秒、每分、每天、每月、每年…。
    • (?) 问号,问号只能出现在日期和星期这两个位置,表示这个位置的值不确定,每天 3 点执行,因此第六位星期的位置,是不需要关注的,就是不确定的值;同时,日期和星期是两个相互排斥的元素,通过问号来表明不指定值,比如 1 月 10 日是星期一,如果在星期的位置另指定星期二,就前后冲突矛盾了。
    • (-) 减号,表达一个范围,如在小时字段中使用“10 - 12”,则表示从 10 到 12 点,即 10、11、12。
    • (,) 逗号,表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一、星期二、星期四。
    • (/) 斜杠,如 x/y,x 是开始值,y 是步长,比如在第一位(秒),0/15 就是从 0 秒开始,每隔 15 秒执行一次,最后就是 0、15、30、45、60,另 */y,等同于 0/y。

    举几个例子熟悉一下:

    表达式   意义   
    "0 0 12 * * ?"    每天中午12点触发   
    "0 15 10 ? * *"    每天上午10:15触发   
    "0 15 10 * * ?"    每天上午10:15触发   
    "0 15 10 * * ? *"    每天上午10:15触发   
    "0 15 10 * * ? 2005"    2005年的每天上午10:15触发   
    "0 * 14 * * ?"    在每天下午2点到下午2:59期间的每1分钟触发   
    "0 0/5 14 * * ?"    在每天下午2点到下午2:55期间的每5分钟触发    
    "0 0/5 14,18 * * ?"    在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发    
    "0 0-5 14 * * ?"    在每天下午2点到下午2:05期间的每1分钟触发   
    "0 10,44 14 ? 3 WED"    每年三月的星期三的下午2:10和2:44触发   
    "0 15 10 ? * MON-FRI"    周一至周五的上午10:15触发   
    "0 15 10 15 * ?"    每月15日上午10:15触发   
    "0 15 10 L * ?"    每月最后一日的上午10:15触发   
    "0 15 10 ? * 6L"    每月的最后一个星期五上午10:15触发     
    "0 15 10 ? * 6L 2002-2005"    2002年至2005年的每月的最后一个星期五上午10:15触发   
    "0 15 10 ? * 6#3"    每月的第三个星期五上午10:15触发    
      
    特殊字符   意义   
    *    表示所有值;   
    ?    表示未说明的值,即不关心它为何值;   
    -    表示一个指定的范围;   
    ,    表示附加一个可能值;   
    /    符号前表示开始时间,符号后表示每次递增的值;   
    L("last")    ("last") "L" 用在day-of-month字段意思是 "这个月最后一天";用在 day-of-week字段, 它简单意思是 "7" or "SAT"。 如果在day-of-week字段里和数字联合使用,它的意思就是 "这个月的最后一个星期几" – 例如: "6L" means "这个月的最后一个星期五". 当我们用“L”时,不指明一个列表值或者范围是很重要的,不然的话,我们会得到一些意想不到的结果。   
    W("weekday")    只能用在day-of-month字段。用来描叙最接近指定天的工作日(周一到周五)。例如:在day-of-month字段用“15W”指“最接近这个月第15天的工作日”,即如果这个月第15天是周六,那么触发器将会在这个月第14天即周五触发;如果这个月第15天是周日,那么触发器将会在这个月第16天即周一触发;如果这个月第15天是周二,那么就在触发器这天触发。注意一点:这个用法只会在当前月计算值,不会越过当前月。“W”字符仅能在day-of-month指明一天,不能是一个范围或列表。也可以用“LW”来指定这个月的最后一个工作日。    
    #    只能用在day-of-week字段。用来指定这个月的第几个周几。例:在day-of-week字段用"6#3"指这个月第3个周五(6指周五,3指第3个)。如果指定的日期不存在,触发器就不会触发。    
    C    指和calendar联系后计算过的值。例:在day-of-month 字段用“5C”指在这个月第5天或之后包括calendar的第一天;在day-of-week字段用“1C”指在这周日或之后包括calendar的第一天  

    5.Quartz框架实战

    5.1 maven依赖

    添加quartz的依赖:

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.0</version>
        </dependency>

    核心类:

    Scheduler :调度器,所有Job的调度都是由它控制;

    JobDetail :生成Job对象的实例,存储Job对象需要的参数;

    Job :执行业务逻辑;

    Trigger :定义触发的条件;

    帮助类

    SimpleScheduleBuilder:用于构建Scheduler:

    JobBuilder :用于构建JobDetail:

    TriggerBuilder :用于构建Trigger;

    5.2 创建job

    首先创建一个Quartz任务,任务中从JobExecutionContext中获取到了JobDetailTrigger中的JobDataMap,并从中取到了客户端QuartzScheduler中传入的数据:

    /**
     * @Author lucky
     * @Date 2021/12/27 9:18
     */
    public class HelloJob implements Job {
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            JobDetail detail = jobExecutionContext.getJobDetail();
            String name = detail.getJobDataMap().getString("name");
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //显示的格式
            String date = sdf.format(new Date());
            System.out.println(date+":  say hello " + name );
        }
    }

    5.3 任务调度测试案例

    创建Quartz客户端,构建JobDetailTrigger并使用Scheduler开始任务调度(这里要注意的是Scheduler实例创建后处于“待机”状态,所以别忘了调用start方法启动调度器,否则任务是不会执行的!):

    /**
     * @Author lucky
     * @Date 2021/12/27 9:23
     */
    public class JobTest {
        public static void main(String[] args) throws InterruptedException {
    
            // 创建工厂
            SchedulerFactory schedulerfactory = new StdSchedulerFactory();
            Scheduler scheduler = null;
            try {
                // 通过schedulerFactory获取一个调度器
                scheduler = schedulerfactory.getScheduler();
    
                JobDataMap jobDataMap=new JobDataMap();
                jobDataMap.put("name","quartz" );
    
    
                // 创建一个JobDetail实例,指明job的名称,所在组的名称,以及绑定job类
                JobDetail job = JobBuilder.newJob(HelloJob.class) //绑定job类
                        .withIdentity("JobName", "JobGroupName") //指定JobDetail的名称和组名称
                        .usingJobData(jobDataMap) //使用jobDataMap存储用户数据, jobDataMap为JobDetail传递的文本数据
                        .build();
                // 构建一个Trigger(定义触发的条件),指定Trigger名称和组,规定该Job立即执行,且3秒钟重复执行一次
                Trigger trigger = TriggerBuilder.newTrigger()
                        .withIdentity("CronTrigger1", "CronTriggerGroup") //指定Trigger名称和组
                        .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever()) // 设置运行规则,每隔3秒执行一次,一直重复下去
                        .startNow() // 执行的时机,立即执行
                        .build();
    
                //绑定JobDetail和Trigger
                scheduler.scheduleJob(job, trigger);
    
                //开始任务调度
                scheduler.start();
    
                Thread.sleep(30000);
    
                // 停止任务调度
                scheduler.shutdown();
    
            } catch (SchedulerException e) {
                e.printStackTrace();
            }
    
        }
    }

    控制台输出:

    6. @scheduled注解定时任务

    在 Spring Boot 中使用 @Scheduled 注解创建定时任务非常简单,只需要两步操作就可以创建一个定时任务:

    (1)在定时任务类上增加 @EnableScheduling 注解

    (2)在要执行任务的方法上增加 @Scheduled 注解

    (3)ShedLock的作用,确保任务在同一时刻最多执行一次。如果一个任务正在一个节点上执行,则它将获得一个锁,该锁将阻止从另一个节点(或线程)执行同一任务。如果一个任务已经在一个节点上执行,则在其他节点上的执行不会等待,只需跳过它即可

    package com.ttbank.flep.core.job;
    
    import org.springframework.beans.factory.annotation.Configurable;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * @Author lucky
     * @Date 2022/1/27 10:37
     */
    @Component
    @Configurable
    @EnableScheduling
    public class ScheduledTasks {
    
        /**
         * 每6秒执行一次
         **/
        @Scheduled(cron = "*/6 * *  * * * ")
        public void reportCurrentByCron(){
            SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println ("Scheduling Tasks Examples By Cron: The time is now " + sdf.format (new Date()));
        }
    
    }

    控制台输出:

    参考文献:

    https://zhuanlan.zhihu.com/p/133208221---非常好

    https://zhuanlan.zhihu.com/p/133211946

    https://blog.csdn.net/chengqiuming/article/details/84187419---非常好

    https://blog.csdn.net/cyan20115/article/details/106550915

    https://blog.csdn.net/java_hanyu_tel/article/details/79697161----非常好

    https://www.cnblogs.com/haw2106/p/9950826.html-----非常好

    https://www.cnblogs.com/loong-hon/p/10912741.html-----------非常好

    https://www.cnblogs.com/niceyoo/p/10917461.html

  • 相关阅读:
    php中常见的大坑,开发中应尽量避免
    ElasticSearch快速入门【建议收藏】
    OWASP Secure Headers Project
    专科是文科,是否可以报考理科的本科
    一网通办 下载发票
    “次幂”和“次方”有什么区别?
    详讲口诀“奇变偶不变,符号看象限”
    成考数学知识点 高起专
    如何下载腾讯课堂的视频
    JDK发布版本的总结
  • 原文地址:https://www.cnblogs.com/luckyplj/p/15608853.html
Copyright © 2011-2022 走看看