spring cron表达式源码分析

spring cron表达式源码分析

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

spring cron表达式源码分析

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

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

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

spring cron表达式源码分析

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

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

spring cron表达式源码分析

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

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

下面看看它的主要实现

spring cron表达式源码分析

这里我们一般常用的可能就是 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个部分,每个部分代表的含义如下:

spring cron表达式源码分析

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

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

spring cron表达式源码分析

先看下它的构造方法

spring cron表达式源码分析

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

代码的如下图

spring cron表达式源码分析

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

  1. 将我们传入的 cron字符串分割成数组。
  2. 分别解析每个部分,创建 CronExpression对象。 解析每个部分都调用的是 CronField.parsexxx 这样的静态方法。所有的解析基本是一致的,分别创建 BitsCronField对象。所以我们就只看 CronField.parseSeconds方法。
  3. CronField.parseSeconds源码分析 这个方法会调到 BitsCronField.parseSeconds(value);方法,继续调用到 BitsCronField的静态方法 parseField(value, Type.SECOND),下面我们主要看看这个方法的代码。 spring cron表达式源码分析 上面就是这个方法的全部代码了,从上面我标注的地方就能看到一个 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方法

    spring cron表达式源码分析 如果传入的 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、下面我们看下包含 /分支的部分

spring cron表达式源码分析
  • 首先也还是拆分 /前后,前面的作为 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的时刻执行。

spring cron表达式源码分析

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

spring cron表达式源码分析

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

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

举个例子:
比如我们的定时任务很快,在0纳秒后就返回了 ,由于我们的定时任务设置了只在0纳秒执行,那这时候计算出来的下次执行任务时间和上一次任务是同一时间,就又会去执行一遍定时任务。
这里也能看到我们的定时任务最快也是每秒执行一次。加1纳秒就是为了确保当前任务和下次任务不会在同一秒执行。
下次任务执行时间是在本次任务执行完就就算出来的

spring cron表达式源码分析

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

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

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

spring cron表达式源码分析

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

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

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

具体的做法就是用将全F左移 current位,与对应字段的 bits做与运算。然后返回最低位为1 bit位的索引。就表示下次任务可以执行的对应的时间字段的值。
当前有可能与运算后结果是0。那就没有1的 bit位。这时就会返回-1。在下面标注3的地方就会对这种场景进行处理。

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

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

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

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

下面是 elapseUntil方法的代码。

spring cron表达式源码分析

从图上看,主要分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表达式的各种写法,应该是个简单事情了。

Original: https://www.cnblogs.com/wbo112/p/16747311.html
Author: wang03
Title: spring cron表达式源码分析

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/801587/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球