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. 【每天学一点-02】创建Node.js的第一个应用

    1.引入require模块,使用createServer()创建服务器 [server.js]文件 var http = require('http'); http.createServer(func ...

  2. Nginx常用命令之启动与重启

    1.测试新的Nginx程序是否正确 [test@P-SH-Nginx-01 nginx]$ ./sbin/nginx -t nginx: the configuration file /usr/loc ...

  3. CF1705A Mark the Photographer 题解

    题意: 给定一队人的身高,将其分成两队,问能否实现前面的人均低于后面的人至少 \(x\) 个单位长度. 做法: 将这队人的身高进行排序,\(h_1 \sim h_n\) 即为第一队,\(h_{n+1} ...

  4. Windows快捷安装应用方法(此处以Virtualbox为例)

    1.执行已下载的virtualbox的安装exe文件,使用pywinauto模拟点击Windows安装的对应控件 1.1.启动exe文件 start *.exe 1.2.使用pywinauto(也适用 ...

  5. 20220727-Java中多态总结

    目录 方法的多态 对象的多态 多态的注意事项和细节 向下转型 Java动态绑定机制 多态polymorphism:方法或者对象具有多种形态 方法的多态 方法的重载可以体现多态 代码示例 // 通过方法 ...

  6. HTML基础标签学习

    HTML基础学习 前言 HTML基础学习会由HTML基础标签学习.HTML表单学习和一张思维导图总结HTML基础三篇文章构成,文章中博主会提取出重点常用的知识和经常出现的bug,提高学习的效率,后续会 ...

  7. PHP几个常见不常用的方法

    method_exists判断方法是否存在 <?php class F{ public function __construct(){ if(method_exists($this, 'son_ ...

  8. 使用Python3将word文档和pdf电子书进行格式互转(兼容Windows/Linux)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_96 一些重要文档格式之间的互转在目前显得尤为重要,pdf作为通用格式在现在各个平台上兼容性是最好的,所以写python脚本将这些w ...

  9. 从零开始Blazor Server(8)--增加菜单以及调整位置

    这篇干啥 这篇文章主要是把前面的一些东西稍微调整一下,使其更适合后面的内容. 主要是两个事,一个是把原来的PermissionEntity直接变成MenuEntity,直接让最后一级是菜单,这样后面就 ...

  10. ACM模式细节

    牛客网的ACM模式需要自己写输入输出,在这里简单记录一下: 基本答题框架: import java.util.*; public class Main{ public static void main ...