spring cron表达式源码分析

在springboot中,我们一般是通过如下的做法添加一个定时任务

上面的new CronTrigger("0 * * * * *")中的参数0 * * * * *就是cron表达式了。

这里主要是对cron表达式的源码进行分析,其他内容不再展开了。

这能看到会创建一个CronTrigger对象,这个对象它主要就是用来包装解析后的cron表达式,获取任务下次执行的时间。

CronTrigger构造方法中会调用到this.expression = CronExpression.parse(expression);将我们传入的cron字符串解析成为CronExpression对象。

CronExpression主要有一个next方法,它会根据当前cron表达式解析出来的对象,以及传入的时间,返回一个时间值,也就是下次任务执行的时间。

这里的入参需要实现Temporal接口。这是在JDK8引入的一套全新的时间、日期。

能引入新的,至少说明之前的Date等等之类的时间处理是不能满足各方面需要的。

下面看看它的主要实现

这里我们一般常用的可能就是Instant,LocalDateTime,ZonedDateTime了。

从上面就可以看到cron表达式的处理,主要是分为两步:1、将cron表达式字符串解析为CronExpression对象;2、根据传入的时间计算下次任务的执行时间。

在分析源码之前,我们简单看几个java中的类

  1. ValueRange主要用来表示时间、日期字段的有效范围。当然它也可以不用来表示时间、日期。下面我们简单看下它的使用。

    它主要有4个字段,4个属性值从上到下是不小于的关系。

        private final long minSmallest;   //最小的最小值
    private final long minLargest; //较大的最小值
    private final long maxSmallest; //较小的最大值
    private final long maxLargest; //最大的最大值
		//定义1个(1-10)的范围指定minSmallest和minLargest都是1,maxSmallest和maxLargest都是10
ValueRange valueRange=ValueRange.of(1,10);
//判断5是不是在上面定义的minSmallest和maxLargest(1-10)的范围内,在的话返回true
boolean validValue = valueRange.isValidValue(5);
System.out.println(validValue);
  1. ChronoField是一个枚举类,就是用来表示时间、日期的字段。

    下面我们简单看它的几个实例

    //用纳秒来表示表,1秒==1000000000纳秒,所以它的范围是0-999999999
NANO_OF_SECOND("NanoOfSecond", NANOS, SECONDS, ValueRange.of(0, 999_999_999)),
//用纳秒来表示一天,1天==86400秒,再转成纳秒就是86400L*1000000000
NANO_OF_DAY("NanoOfDay", NANOS, DAYS, ValueRange.of(0, 86400L * 1000_000_000L - 1)),
......其他基本类似,就不继续说了
  1. ChronoUnit也是一个枚举类,表示一个时间单元。有一个addTo方法表示给时间加上一个对应的时间单元。

    		//下面的代码就是给当前时间加上1天
    ZonedDateTime now = ZonedDateTime.now();
    ZonedDateTime zonedDateTime = ChronoUnit.DAYS.addTo(now, 1);

我们先看第一步:

1、将cron表达式字符串解析为CronExpression对象

我们传入的表达式用空格分成6个部分,每个部分代表的含义如下:

CronField类中有一个内部枚举类Type,它就是用来表示cron表达式中的字段(

在cron表达式中没有纳秒字段,其他都跟cron表达式是一一对应的

先看下它的构造方法

从上面也可以看到这个枚举类有两个字段,第一个是表示当前时间、日期的字段,后面是一个用来表示小于它的时间、日期字段的数组。

代码的如下图

主要代码就是上面框出来的:

  1. 将我们传入的cron字符串分割成数组。

  2. 分别解析每个部分,创建CronExpression对象。

    解析每个部分都调用的是CronField.parsexxx 这样的静态方法。所有的解析基本是一致的,分别创建BitsCronField对象。所以我们就只看CronField.parseSeconds方法。

    • CronField.parseSeconds源码分析

      这个方法会调到BitsCronField.parseSeconds(value);方法,继续调用到BitsCronField的静态方法 parseField(value, Type.SECOND),下面我们主要看看这个方法的代码。

    上面就是这个方法的全部代码了,从上面我标注的地方就能看到一个cron字段可以包含的其他符号,分别是/-这3种额外的符号。

    1、在标号1的地方首先把字段用号拆分成数组,后面在for循环中对每个部分进行处理。

    2、在for循环中,首先判断是否包含/,如果不包含,就调用parseRange返回一个ValueRange


    2.1、 下面我们先看下不包含/if分支

    parseRange方法比较简单,这里简单说下:

    如果当前的rangeStr==*,那就返回type对应的默认ValueRange

    这里的type就是前面看到的CronField的内部枚举类Type

    如果rangeStr不包含-,那就表示一个固定的值,用ValueRange.of(result, result)返回;

    如果rangeStr包含-,那就表示一个范围-前面的表示最小值、-之后的表示最大值。组装成一个ValueRange返回。


    parseRange返回之后,再调用result.setBits(range)方法。

    我们先看看BitsCronField这个类,它有一个属性private static final long MASK = 0xFFFFFFFFFFFFFFFFL;表示掩码,还有一个属性private long bits;我们最终计算出来的执行时间都会体现在这个字段上。

    由于对于cron表达式中的6个部分,最大的也就是比如表示分钟、秒钟的一共60个。由于long是64位,所以这是按照bit来设置对应的可用的值。

    举个例子比如cron表达式计算出来秒的部分是第50秒执行,那就会将对应的bits字段的第50位设置为1

    下面我们看看setBits方法

    如果传入的ValueRange只表示一个值,那就把对应的bit位置1;

    否则就将将最小值与最大值之间的bit位置1。

    这里使用|是由于外面我们可能会是,分割的多个字段,会出现多次赋值,要确保本次赋值不会将之前赋值1的bit位清空。

    这里需要注意的是右移是一个负数,这是由于MASKlong类型,也就是64位,所以右移负数其实也就是移动(64-(range.getMaximum() + 1))位。后面的+1主要是由于我们最小值是从0开始的。

    举个例子,如果范围的最小值是0,最大值是1。如果没有+1,最大值掩码就会右移64-1=63,最终只有第0位是1,这明显就是错误的。

    2.2、下面我们看下包含/分支的部分

  • 首先也还是拆分/前后,前面的作为ValueRange,后面的作为delta

    这里需要注意的是如果/前面不包含-,那/前面的只是作为最小值,最大值还是用type对应的最大值。

  • 标注2的地方就是设置对应的bit位了,这里主要是按照delta的增量在最小值和最大值之间分别设置对应bit位。


上面就是cron表达式中一个字段的解析了,创建一个BitsCronField对象,设置对应的bits属性对应的bit位为1,下面我们简单看看各种设置。

下面我们看看cron表达式秒字段的各种情况

* 表示将bitsbit位从0-59位都设置成1.

2,6,8表示将bitsbit位第2、6、8位都设置成1.

2/20,8表示将bitsbit位第2、22、42、8位都设置成1.

在解析完cron表达式的每个部分之后,就会创建一个CronExpression对象,这类会添加一个CronField.zeroNanos()字段,用来表示纳秒字段,同时将bits设置为0,表示我们的定时任务希望在纳秒为0的时刻执行。


上面已经创建好了CronExpression对象,下面我们看看如果计算下次执行时间。

这里就是根据传入的时间去计算下次任务的执行时间了 。

首先给入参时间加上1纳秒,这个主要是避免在1个时间点任务执行多次。

举个例子:

比如我们的定时任务很快,在0纳秒后就返回了 ,由于我们的定时任务设置了只在0纳秒执行,那这时候计算出来的下次执行任务时间和上一次任务是同一时间,就又会去执行一遍定时任务。

这里也能看到我们的定时任务最快也是每秒执行一次。加1纳秒就是为了确保当前任务和下次任务不会在同一秒执行。

下次任务执行时间是在本次任务执行完就就算出来的

在这里能看到,最多会尝试MAX_ATTEMPTS次,查看计算出来的时间不再变化,那这个时间就是我们计算出来的下次任务执行的时间。如果尝试MAX_ATTEMPTS次每次的时间都和上次的不一样,那就返回null

nextOrSameInternal方法中会分别对每个field进行处理。这些field有额外添加的纳秒(设置了bits=0,表示在0纳秒执行),其他6个就分别是cron表达式对应的部分,分别是秒、分、小时、日、月、星期

下面我们看下对单个字段的处理。

在标号1的地方首先获取传入的时间对应当前时间单位的值。

比如现在传入的是2022-10-01 12:00:15,对应秒的单位的值就是15.


在标号2的位置会根据当前时间单位的值去计算下次值

具体的做法就是用将全F左移current位,与对应字段的bits做与运算。然后返回最低位为1bit位的索引。就表示下次任务可以执行的对应的时间字段的值。

当前有可能与运算后结果是0。那就没有1的bit位。这时就会返回-1。在下面标注3的地方就会对这种场景进行处理。


在标注3的地方主要是对时间已经过去的情况进行处理。比如我们cron表达式计划在秒数为00的时刻进行执行,由于现在已经是15秒了。那只能在它的上一级时间单位(分钟)+1,同时将本时间单位置为0。

在这里已经对我们的时间进行了+1处理,所以时间值和传入的值已经有变化了 ,这时在外层就会进入下次循环。


在标注4的地方会重新计算对应时间单位最早执行的最小值。


能走到标注5的地方说明对应下次任务执行时间对应时间单位的值已经有变化了,在这里主要也还是调整时间,将时间调整成符合下次执行任务的时间。主要的代码是elapseUntil方法。

下面是elapseUntil方法的代码。

从图上看,主要分3种情况:

  • 下次执行时间在合法范围内,那就直接讲字段的值进行设置。

    这里需要注意的是这个范围不一定是固定的。如日,在1月范围有效范围就是在1-31。2月就是在1-28或1-29。

  • 如果时间不在有效范围内,那就在当前的时间单位上加上一个差值。

    这个加操作会使上一级时间单位变化。如当前时间单位是日,执行加操作,可能会使月单位的值也有所变化。

  • 如果下次执行时间小于当前时间单位的值,那就只能进行加。

    比如当前是1月15号,下次任务是10号执行。那就只有给日时间单位加(下次任务时间10+最大值31-当前值15+1-最小值1)=26

    将时间变成下个月对应的时间单位进行重新计算(将时间变成2月10号)。


在标注6的地方,这里是由于已经将当前的时间单位进行至少+1的调整。那这时就需要将它对应的所有下一级时间单位统一调整成最小值,以便下次重新计算。


上面就是整个下次任务执行时间的计算了 。

总结下,就是设定下次任务执行的纳秒单位为0,分别在秒,分,小时,日,月,星期单位上进行计算。至少时间不再调整。

上面只是分析了cron表达式的解析处理,关于cron表达式的各种写法并没有列出。不过相信大家根据源码反推cron表达式的各种写法,应该是个简单事情了。

spring cron表达式源码分析的更多相关文章

  1. Spring Developer Tools 源码分析:二、类路径监控

    在 Spring Developer Tools 源码分析一中介绍了 devtools 提供的文件监控实现,在第二部分中,我们将会使用第一部分提供的目录监控功能,实现对开发环境中 classpath ...

  2. Spring IOC 容器源码分析 - 余下的初始化工作

    1. 简介 本篇文章是"Spring IOC 容器源码分析"系列文章的最后一篇文章,本篇文章所分析的对象是 initializeBean 方法,该方法用于对已完成属性填充的 bea ...

  3. Spring IOC 容器源码分析 - 填充属性到 bean 原始对象

    1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的.我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反 ...

  4. Spring IOC 容器源码分析 - 循环依赖的解决办法

    1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的.在本篇文章中,我会首先向大家介绍一下什么是循环依赖.然后,进入源码分析阶段.为了更好的说明 Spring 解决循环依赖的办法,我 ...

  5. Spring IOC 容器源码分析 - 创建原始 bean 对象

    1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续.在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程.本篇文章,我们就从战术的层面上,详细分析doCreat ...

  6. Spring IOC 容器源码分析 - 创建单例 bean 的过程

    1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...

  7. Spring IOC 容器源码分析 - 获取单例 bean

    1. 简介 为了写 Spring IOC 容器源码分析系列的文章,我特地写了一篇 Spring IOC 容器的导读文章.在导读一文中,我介绍了 Spring 的一些特性以及阅读 Spring 源码的一 ...

  8. Spring IOC 容器源码分析系列文章导读

    1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...

  9. 十、Spring之BeanFactory源码分析(二)

    Spring之BeanFactory源码分析(二) 前言 在前面我们简单的分析了BeanFactory的结构,ListableBeanFactory,HierarchicalBeanFactory,A ...

随机推荐

  1. 2022-7-13 第五组 pan小堂 java基础

    ###java基础 1.java语言发展史和概述平台(了解) 詹姆斯·高斯林(James Gosling)1977年获得了加拿大卡尔加里大学计算机科学学士学位,1983年获得了美国卡内基梅隆大学计算机 ...

  2. 丽泽普及2022交流赛day20 1/4社论

    目录 T1 正方形 T2 玩蛇 T3 嗷呜 T4 开车 T1 正方形 略 T2 玩蛇 略 T3 嗷呜 (插一个删一个?) 找出相同的,丢掉循环节 . 感觉非常离谱,,, 正确性存疑 正确性问 SoyT ...

  3. 结束语句之 break

    C 语言自学之 break Dome1: 找出0-50之间的所有素数,所谓素数就是只能被1和它本身整除的数字,比如:7,13,23等.                运行结果: 2  3  5  7 ...

  4. Luogu3402【模板】可持久化并查集 (主席树)

    用\(depth\)按秩合并,不能直接启发,数组开40倍左右 #include <iostream> #include <cstdio> #include <cstrin ...

  5. SPI:Java的高可扩展利器

    摘要:JAVA SPI,基于接口的编程+策略模式+配置文件的动态加载机制. 本文分享自华为云社区<一文讲透Java核心技术之高可扩展利器SPI>,作者: 冰 河. SPI的概念 JAVA ...

  6. 若依 | 点击顶部 tag 标签不自动刷新

    需求场景 之前:只要点击若依顶部的标签,页面都会自动刷新. 问题:A 页面有查询结果,切换到 B 页面查看信息,再切回 A 页面,则 A 页面的查询结果不会保留. 需求:点击标签,页面不自动刷新,或者 ...

  7. 使用Docker搭建Nextcloud私有网盘

    一.准备材料 安装环境:linux 工具:docker 软件:MySql.Nextcloud 二.安装Docker 安装Docker:https://www.cnblogs.com/jzcn/p/15 ...

  8. 第三十二篇:vue的响应式原理

    好家伙 什么是响应式?比较官方的回答: Vue.js 的核心包括一套"响应式系统". "响应式",是指当数据改变后,Vue 会通知到使用该数据的代码. 例如,视 ...

  9. KingbaseES 行列转换函数

    关键字:    行专列,列转行, pivot, unpivot 行列转换是在数据分析中经常用到的一项功能,KingbaseES从V8R6C3B0071版本开始通过扩展插件(kdb_utils_func ...

  10. React版/Vue版都齐了,开源一套【特别】的后台管理系统...

    本项目主要基于Elux+Antd构建,包含React版本和Vue版本,旨在提供给大家一个简单基础.开箱即用的后台管理系统通用模版,主要包含运行环境.脚手架.代码风格.基本Layout.状态管理.路由管 ...