Springcloud学习笔记37--任务调度框架Quartz 使用01(Cron表达式)与@scheduled注解定时任务

1.Quartz简介

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

基于定时、定期的策略来执行任务是它的核心功能,比如x年x月的每个星期五上午8点到9点,每隔10分钟执行1次。

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绑定到一起。任务调度器,是实际执行任务调度的控制器。在spring中通过SchedulerFactoryBean封装起来。

3. Quartz重要组成部分

3.1 Job接口

Job是一个接口,只有一个方法void execute(JobExecutionContext context) , 这是我们自己的具体业务逻辑的入口。作业类需要实现接口中的execute方法,JobExecutionContext提供了调度的上下文信息,每一次执行Job都会重新创建一个Job对象实例。

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

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

https://blog.csdn.net/qq_34279574/article/details/120776854---注解定时任务框架

https://blog.csdn.net/wangmx1993328/article/details/105057405  非常推荐

https://www.cnblogs.com/muxi0407/p/11969119.html 推荐

 

posted @ 2021-11-26 17:32  雨后观山色  阅读(497)  评论(0编辑  收藏  举报