spring cron表达式源码分析
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
中的类
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);
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)),
......其他基本类似,就不继续说了
ChronoUnit
也是一个枚举类,表示一个时间单元。有一个addTo
方法表示给时间加上一个对应的时间单元。//下面的代码就是给当前时间加上1天
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime zonedDateTime = ChronoUnit.DAYS.addTo(now, 1);
我们先看第一步:
1、将cron
表达式字符串解析为CronExpression
对象
我们传入的表达式用空格分成6个部分,每个部分代表的含义如下:
在CronField
类中有一个内部枚举类Type
,它就是用来表示cron
表达式中的字段(
在cron表达式中没有纳秒字段,其他都跟
cron
表达式是一一对应的
先看下它的构造方法
从上面也可以看到这个枚举类有两个字段,第一个是表示当前时间、日期的字段,后面是一个用来表示小于它的时间、日期字段的数组。
代码的如下图
主要代码就是上面框出来的:
将我们传入的
cron
字符串分割成数组。分别解析每个部分,创建
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
位清空。这里需要注意的是右移是一个负数,这是由于
MASK
是long
类型,也就是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
表达式秒字段的各种情况
*
表示将bits
中bit
位从0-59
位都设置成1.
2,6,8
表示将bits
中bit
位第2、6、8位都设置成1.
2/20,8
表示将bits
中bit
位第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表达式源码分析的更多相关文章
- Spring Developer Tools 源码分析:二、类路径监控
在 Spring Developer Tools 源码分析一中介绍了 devtools 提供的文件监控实现,在第二部分中,我们将会使用第一部分提供的目录监控功能,实现对开发环境中 classpath ...
- Spring IOC 容器源码分析 - 余下的初始化工作
1. 简介 本篇文章是"Spring IOC 容器源码分析"系列文章的最后一篇文章,本篇文章所分析的对象是 initializeBean 方法,该方法用于对已完成属性填充的 bea ...
- Spring IOC 容器源码分析 - 填充属性到 bean 原始对象
1. 简介 本篇文章,我们来一起了解一下 Spring 是如何将配置文件中的属性值填充到 bean 对象中的.我在前面几篇文章中介绍过 Spring 创建 bean 的流程,即 Spring 先通过反 ...
- Spring IOC 容器源码分析 - 循环依赖的解决办法
1. 简介 本文,我们来看一下 Spring 是如何解决循环依赖问题的.在本篇文章中,我会首先向大家介绍一下什么是循环依赖.然后,进入源码分析阶段.为了更好的说明 Spring 解决循环依赖的办法,我 ...
- Spring IOC 容器源码分析 - 创建原始 bean 对象
1. 简介 本篇文章是上一篇文章(创建单例 bean 的过程)的延续.在上一篇文章中,我们从战略层面上领略了doCreateBean方法的全过程.本篇文章,我们就从战术的层面上,详细分析doCreat ...
- Spring IOC 容器源码分析 - 创建单例 bean 的过程
1. 简介 在上一篇文章中,我比较详细的分析了获取 bean 的方法,也就是getBean(String)的实现逻辑.对于已实例化好的单例 bean,getBean(String) 方法并不会再一次去 ...
- Spring IOC 容器源码分析 - 获取单例 bean
1. 简介 为了写 Spring IOC 容器源码分析系列的文章,我特地写了一篇 Spring IOC 容器的导读文章.在导读一文中,我介绍了 Spring 的一些特性以及阅读 Spring 源码的一 ...
- Spring IOC 容器源码分析系列文章导读
1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...
- 十、Spring之BeanFactory源码分析(二)
Spring之BeanFactory源码分析(二) 前言 在前面我们简单的分析了BeanFactory的结构,ListableBeanFactory,HierarchicalBeanFactory,A ...
随机推荐
- 【每天学一点-02】创建Node.js的第一个应用
1.引入require模块,使用createServer()创建服务器 [server.js]文件 var http = require('http'); http.createServer(func ...
- Nginx常用命令之启动与重启
1.测试新的Nginx程序是否正确 [test@P-SH-Nginx-01 nginx]$ ./sbin/nginx -t nginx: the configuration file /usr/loc ...
- CF1705A Mark the Photographer 题解
题意: 给定一队人的身高,将其分成两队,问能否实现前面的人均低于后面的人至少 \(x\) 个单位长度. 做法: 将这队人的身高进行排序,\(h_1 \sim h_n\) 即为第一队,\(h_{n+1} ...
- Windows快捷安装应用方法(此处以Virtualbox为例)
1.执行已下载的virtualbox的安装exe文件,使用pywinauto模拟点击Windows安装的对应控件 1.1.启动exe文件 start *.exe 1.2.使用pywinauto(也适用 ...
- 20220727-Java中多态总结
目录 方法的多态 对象的多态 多态的注意事项和细节 向下转型 Java动态绑定机制 多态polymorphism:方法或者对象具有多种形态 方法的多态 方法的重载可以体现多态 代码示例 // 通过方法 ...
- HTML基础标签学习
HTML基础学习 前言 HTML基础学习会由HTML基础标签学习.HTML表单学习和一张思维导图总结HTML基础三篇文章构成,文章中博主会提取出重点常用的知识和经常出现的bug,提高学习的效率,后续会 ...
- PHP几个常见不常用的方法
method_exists判断方法是否存在 <?php class F{ public function __construct(){ if(method_exists($this, 'son_ ...
- 使用Python3将word文档和pdf电子书进行格式互转(兼容Windows/Linux)
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_96 一些重要文档格式之间的互转在目前显得尤为重要,pdf作为通用格式在现在各个平台上兼容性是最好的,所以写python脚本将这些w ...
- 从零开始Blazor Server(8)--增加菜单以及调整位置
这篇干啥 这篇文章主要是把前面的一些东西稍微调整一下,使其更适合后面的内容. 主要是两个事,一个是把原来的PermissionEntity直接变成MenuEntity,直接让最后一级是菜单,这样后面就 ...
- ACM模式细节
牛客网的ACM模式需要自己写输入输出,在这里简单记录一下: 基本答题框架: import java.util.*; public class Main{ public static void main ...