java实现cron解析计算,spring5.3.x的实现
java实现对cron表达式解析,spring5.2.x的实现 - 简书 (jianshu.com)
上一篇文章分析了 spring5.2.x的版本对cron表达式的解析及计算通过 CronSequenceGenerator 计算,我们看到其使用Calendar类进行计算,那么并没有使用jdk8添加的Temporal类及其子类(包括LocalDateTime等),jdk8对java的日期相关类进行了重构升级,提供了线程安全的更方便的api,那么spring最新版本是不是也有了新的支持呢。
更详细的cron的符号参考 Cron表达式的详细用法 - 简书 (jianshu.com)
总结的流程图有点乱,大家可以结合源码来看
还是老思路从 CronExpression#parse开始
public static CronExpression parse(String expression) {
Assert.hasLength(expression, "Expression string must not be empty");
// 内置了 每年,每个月,每天,每小时,每分钟,每秒的逻辑,如果表达式符合则直接返回对应内置的 expression
expression = resolveMacros(expression);
// 使用StringTokenizer 分割字符串
String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
if (fields.length != 6) {
throw new IllegalArgumentException(String.format(
"Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
try {
// 新版本使用了一个抽象类CronField抽象了 不同的时间维度。并且提供了CompositeCronField利用CronField数组处理day维度,也提供了QuartzCronField来处理L W等新的cron特性 ,除了dayOfWeek和dayOfMonth都使用和旧版本类似的BitsCronField来处理
CronField seconds = CronField.parseSeconds(fields[0]);
CronField minutes = CronField.parseMinutes(fields[1]);
CronField hours = CronField.parseHours(fields[2]);
CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]);
CronField months = CronField.parseMonth(fields[4]);
CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]);
// 解析完毕,下面看看具体的不同的解析逻辑
return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
catch (IllegalArgumentException ex) {
String msg = ex.getMessage() + " in cron expression \"" + expression + "\"";
throw new IllegalArgumentException(msg, ex);
//--------BitsCronField#parseSeconds--------
public static BitsCronField parseSeconds(String value) {
// 调用通用的方法,不过传入时间field
return parseField(value, Type.SECOND);
// BitsCronField 中的解析方法,处理时分秒月四种时间field
private static BitsCronField parseField(String value, Type type) {
Assert.hasLength(value, "Value must not be empty");
Assert.notNull(type, "Type must not be null");
try {
// 根据field 类型初始化一个 cronField对象
BitsCronField result = new BitsCronField(type);
// 还是熟悉的味道,先使用 , 分割参考上一篇文章
String[] fields = StringUtils.delimitedListToStringArray(value, ",");
for (String field : fields) {
// 不同于 旧版本使用contains,使用indexOf判断是否是 增量模式
int slashPos = field.indexOf('/');
if (slashPos == -1) {
// 如果不是增量模式,则当作 fix已经分割好的,或者是 - 范围来解析,如果是不是范围返回的是一个范围左右相同的,不过使用的是jdk8新提供的ValueRange
ValueRange range = parseRange(field, type);
// 还是老配方放入 可选值, 但是这次不是使用bitSet,而是直接使用一个long的十进制数字表示可选值,并通过一个十六进制数字和移位操作来计算,具体计算分析最后学习
result.setBits(range);
else {
// 老配方 兼容 2-18/6的逻辑,范围+增量逻辑,先取范围
String rangeStr = field.substring(0, slashPos);
// 增量的值
String deltaStr = field.substring(slashPos + 1);
ValueRange range = parseRange(rangeStr, type);
if (rangeStr.indexOf('-') == -1) {
// 如果只是单单的增量,就取当前时间field的最小最大值作为范围
range = ValueRange.of(range.getMinimum(), type.range().getMaximum());
int delta = Integer.parseInt(deltaStr);
if (delta <= 0) {
throw new IllegalArgumentException("Incrementer delta must be 1 or higher");
// 直接通过范围和增量 设置可选值,具体计算逻辑最后学习
result.setBits(range, delta);
return result;
catch (DateTimeException | IllegalArgumentException ex) {
String msg = ex.getMessage() + " '" + value + "'";
throw new IllegalArgumentException(msg, ex);
// ---------dayOfMonth-----------
public static CronField parseDaysOfMonth(String value) {
if (!QuartzCronField.isQuartzDaysOfMonthField(value)) {
// 如果不包含 L W 这些特殊cron表达式 还是使用上面的BitsCronField 的实现
return BitsCronField.parseDaysOfMonth(value);
else {
// 如果包含 L W这些特殊表达式,会解析一个CronField数组,进行分段处理
return parseList(value, Type.DAY_OF_MONTH, (field, type) -> {
// 每一个小段也区分 L W和正常的
if (QuartzCronField.isQuartzDaysOfMonthField(field)) {
return QuartzCronField.parseDaysOfMonth(field);
else {
return BitsCronField.parseDaysOfMonth(field);
// ----------parseList 解析 L W 等特殊------------
private static CronField parseList(String value, Type type, BiFunction<String, Type, CronField> parseFieldFunction) {
Assert.hasLength(value, "Value must not be empty");
// 用 , 分割 有点眼熟,和之前一样先用 , 分割处理
String[] fields = StringUtils.delimitedListToStringArray(value, ",");
CronField[] cronFields = new CronField[fields.length];
for (int i = 0; i < fields.length; i++) {
// 然后单独处理 - / 的逻辑
cronFields[i] = parseFieldFunction.apply(fields[i], type);
return CompositeCronField.compose(cronFields, type, value);
// 返回一个 CompositeCronField专门处理 带有L W 并且是 , 固定值有多个的情况,通过Cron数组处理
public static CronField compose(CronField[] fields, Type type, String value) {
Assert.notEmpty(fields, "Fields must not be empty");
Assert.hasLength(value, "Value must not be empty");
if (fields.length == 1) {
return fields[0];
else {
return new CompositeCronField(type, fields, value);
// BitsCronField的parseDate也使用逻辑相同 BitsCronField#parseField
// 带有L W的 parseDaysOfMonth 是单独特殊处理的。返回的也是QuartzCronField对象
public static QuartzCronField parseDaysOfMonth(String value) {
int idx = value.lastIndexOf('L');
// 如果包含 L
if (idx != -1) {
//jdk8提供的一个 函数式接口,用于设置调整时间,也可以自定义实现
TemporalAdjuster adjuster;
if (idx != 0) {
// L 只可以出现在第一个
throw new IllegalArgumentException("Unrecognized characters before 'L' in '" + value + "'");
// "LW" 同时出现的情况,W 指当前日期最近的工作日LW就是最后一天最近的工作日
else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW"
// 返回一个函数式接口直接设置时间
adjuster = lastWeekdayOfMonth();
else {
// 只有一个L 情况
if (value.length() == 1) { // "L"
// 返回一个函数式接口直接设置时间
adjuster = lastDayOfMonth();
//L - 数字的逻辑。这是一个组合用法,L代表当前周期最后一个单元,目前L只用于day,那么就是当月有31号L-5就是26号,如果当月30号则结果就是25号,如果L-30那么只有31日的月才能是结果,例如当前是5月,表达式为L-30那么 5月1日,7月1日,8月1日,10月1日,如果是L-31则表达式报错计算不出结果
else { // "L-[0-9]+"
int offset = Integer.parseInt(value.substring(idx + 1));
if (offset >= 0) {
throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'");
// 返回一个函数式接口直接设置时间
adjuster = lastDayWithOffset(offset);
return new QuartzCronField(Type.DAY_OF_MONTH, adjuster, value);
// 只有 W 的情况
idx = value.lastIndexOf('W');
if (idx != -1) {
if (idx == 0) {
throw new IllegalArgumentException("No day-of-month before 'W' in '" + value + "'");
else if (idx != value.length() - 1) {
throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'");
else { // "[0-9]+W"
// 从上述校验看出,W的使用必须配合数字,使用例如8W 则表示8号最近的工作日,有可能前移或者后移。具体这个工作日是否只定义了周六周日,还包括什么国际节假日,是否也包括中国式窜休就不得而知了,这个小伙伴要使用这个功能时需要测试,并结合源码,实在不行可以重写weekdayNearestTo返回的函数式接口
int dayOfMonth = Integer.parseInt(value.substring(0, idx));
dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth);
TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth);
return new QuartzCronField(Type.DAY_OF_MONTH, adjuster, value);
throw new IllegalArgumentException("No 'L' or 'W' found in '" + value + "'");
// 先看 带有 L W # 等特殊逻辑的 week逻辑
public static QuartzCronField parseDaysOfWeek(String value) {
int idx = value.lastIndexOf('L');
if (idx != -1) {
if (idx != value.length() - 1) {
throw new IllegalArgumentException("Unrecognized characters after 'L' in '" + value + "'");
else {
TemporalAdjuster adjuster;
if (idx == 0) {
throw new IllegalArgumentException("No day-of-week before 'L' in '" + value + "'");
else { // "[0-7]L"
// week 也可以使用 L 并指定周几 表示当前月最后一个星期的星期几,计算时兼容0也作为周日,先获取周维度的可选值
DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx));
// 然后利用函数式接口计算 最后一周
adjuster = lastInMonth(dayOfWeek);
// 又看到熟悉的套路,传入了两个时间维度,第一个是当前计算的field维度,后面的应该是重置维度。后续看看
return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value);
idx = value.lastIndexOf('#');
if (idx != -1) {
if (idx == 0) {
throw new IllegalArgumentException("No day-of-week before '#' in '" + value + "'");
else if (idx == value.length() - 1) {
throw new IllegalArgumentException("No ordinal after '#' in '" + value + "'");
// "[0-7]#[0-9]+"
// 这是一个 周的特殊逻辑,6#2代表每个月的第二周的周5,0代表周六开始,1为周日,2为周一,依次推移到7又是周六,那么#后面的代表当月第几周,1 ~ 5周。理论上可能存在第五周
// 先计算出#左边的周内可选值
DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx));
int ordinal = Integer.parseInt(value.substring(idx + 1));
if (ordinal <= 0) {
throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value +
"' must be positive number ");
// 利用函数式接口计算第几周
TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek);
return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value);
throw new IllegalArgumentException("No 'L' or '#' found in '" + value + "'");
jdk提供的时间访问器
下面看看各个函数式接口的解析
lastWeekdayOfMonth方法,返回当月最后一个工作日的访问器
private static TemporalAdjuster lastWeekdayOfMonth() {
// 我们可以看到TemporalAdjusters这个类,jdk真贴心,提供了好多已经定义好的访问器,这个方法就是访问当月最后一天
TemporalAdjuster adjuster = TemporalAdjusters.lastDayOfMonth();
return temporal -> {
// 先移动到最后一天
Temporal lastDom = adjuster.adjustInto(temporal);
Temporal result;
int dow = lastDom.get(ChronoField.DAY_OF_WEEK);
if (dow == 6) { // Saturday
// 如果是周六 向前取一天
result = lastDom.minus(1, ChronoUnit.DAYS);
else if (dow == 7) { // Sunday
// 如果是周日 向后取一天。。真实简单粗暴啊,工作日。。。
result = lastDom.minus(2, ChronoUnit.DAYS);
else {
result = lastDom;
// 内部判断天是否移动了
return rollbackToMidnight(temporal, result);
// -------跨越边界的判断------------
private static Temporal rollbackToMidnight(Temporal current, Temporal result) {
// 因为只偏移1天,不会出现 几十几百天的跨越边界导致 dayOfMonth又相等,所以用dayOfMonth判断一定会
if (result.get(ChronoField.DAY_OF_MONTH) == current.get(ChronoField.DAY_OF_MONTH)) {
return current;
else {
// 如果 day偏移了,将时分秒毫秒纳秒重置为0
return atMidnight().adjustInto(result);
// ------- 如果发生了 day的变化,将时分秒毫秒纳秒重置为0-------
private static TemporalAdjuster atMidnight() {
return temporal -> {
if (temporal.isSupported(ChronoField.NANO_OF_DAY)) {
return temporal.with(ChronoField.NANO_OF_DAY, 0);
else {
return temporal;
lastDayOfMonth 最后一天比较简单直接使用了jdk提供的逻辑,并重置时分秒
private static TemporalAdjuster lastDayOfMonth() {
TemporalAdjuster adjuster = TemporalAdjusters.lastDayOfMonth();
return temporal -> {
Temporal result = adjuster.adjustInto(temporal);
return rollbackToMidnight(temporal, result);
lastDayWithOffset 最后一天并且带有偏移,支持L-15,最后一天再向前偏移15天的逻辑
private static TemporalAdjuster lastDayWithOffset(int offset) {
Assert.isTrue(offset < 0, "Offset should be < 0");
TemporalAdjuster adjuster = TemporalAdjusters.lastDayOfMonth();
return temporal -> {
// 逻辑也很简单,直接使用jdk提供的api
Temporal result = adjuster.adjustInto(temporal).plus(offset, ChronoUnit.DAYS);
return rollbackToMidnight(temporal, result);
weekdayNearestTo 返回某一个指定日的 最近的工作日
private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) {
return temporal -> {
// 当前是一个函数,也就是temporal是计算时传入的值,current是计算时实际当前dayOfMonth,方法的参数为指定的日
int current = Type.DAY_OF_MONTH.get(temporal);
DayOfWeek dayOfWeek = DayOfWeek.from(temporal);
// 如果当前值是工作日并且和指定的日期相等
if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday
// 如果当前是 周五 指定日向前偏移一位,那么就相当于指定的day是当前月的周六,然后向前偏移一天正好就是传入的计算时间并且是周五
(dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before
// 如果当前是 周一 指定日向后偏移一位,那么就相当于指定的day是当前月的周日,然后向后偏移一天正好就是传入的计算时间并且是周一
(dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after
// 特殊情况,如果当月1,2号是周六周日,存在,那么为当前传入是3号,并且是周一,那么如果指定的日期是1号,则直接返回
(dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd
return temporal;
int count = 0;
// 如果都循环一年了还没找到,那么就没有这个时间了
while (count++ < CronExpression.MAX_ATTEMPTS) {
// 如果当前传入计算时间 是指定日期,直接判断 向前或者向后偏移即可
if (current == dayOfMonth) {
dayOfWeek = DayOfWeek.from(temporal);
if (dayOfWeek == DayOfWeek.SATURDAY) {
if (dayOfMonth != 1) {
temporal = temporal.minus(1, ChronoUnit.DAYS);
else {
// exception for "1W" fields: execute on next Monday
temporal = temporal.plus(2, ChronoUnit.DAYS);
else if (dayOfWeek == DayOfWeek.SUNDAY) {
temporal = temporal.plus(1, ChronoUnit.DAYS);
return atMidnight().adjustInto(temporal);
else {
// 需要循环计算,知道命中到指定的日期,再进行判断工作日去偏移即可
temporal = Type.DAY_OF_MONTH.elapseUntil(cast(temporal), dayOfMonth);
current = Type.DAY_OF_MONTH.get(temporal);
return null;
elapseUntil 方法,通过给定的相对值,对Temporal对象进行偏移计算
public <T extends Temporal & Comparable<? super T>> T elapseUntil(T temporal, int goal) {
// 取当前相对值
int current = get(temporal);
// 取出当前field的相对值范围
ValueRange range = temporal.range(this.field);
if (current < goal) {
// 如果当前值比给定值小
if (range.isValidIntValue(goal)) {
// 如果给定的值在当前feild维度的范围内
return cast(temporal.with(this.field, goal));
else {
// goal is invalid, eg. 29th Feb, so roll forward
// 不在范围内,例如dayOfMonth,并且给定31号,那么当前为6月则没有,获取当前日期到最后一天的差值 再 + 1
long amount = range.getMaximum() - current + 1;
// 直接移动到下个 field维度的周期内第一个值
return this.field.getBaseUnit().addTo(temporal, amount);
else {
// 如果当前值大于指定值,通过差值直接取到下一个月的指定值
long amount = goal + range.getMaximum() - current + 1 - range.getMinimum();
return this.field.getBaseUnit().addTo(temporal, amount);
新版提供了判断cron表达式是否符合计算逻辑
CronExpression#isValidExpression 其实就是通过解析方法,catch内部抛出的异常return
新版本的计算入口CronExpression#next
@Nullable
public <T extends Temporal & Comparable<? super T>> T next(T temporal) {
// 添加 1毫秒计算,防止循环计算出来相同的值,因为和原来的一直向前偏移的算法不一样的是大量使用jdk8的TemporalAdjuster接口来指定值,可能会指定到一个值
return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
可以看到是一个继承自Temporal 的泛型
这些子类都可以通过 cron表达式直接计算
@Nullable
private <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
for (int i = 0; i < MAX_ATTEMPTS; i++) {
T result = nextOrSameInternal(temporal);
// 如果没有计算出结果,或者计算的结果和当前时间相同,直接返回结果。
if (result == null || result.equals(temporal)) {
return result;
// 每次结果赋值,重复计算,计算到前后两次计算结果相同才返回,说明两次结果都命中到同一个时间点,这个时间是要计算的时间返回
temporal = result;
return null;
@Nullable
private <T extends Temporal & Comparable<? super T>> T nextOrSameInternal(T temporal) {
// 对每个时间维度进行计算
for (CronField field : this.fields) {
temporal = field.nextOrSame(temporal);
if (temporal == null) {
// 有一个维度未计算出结果视为计算不出结果
return null;
return temporal;
// 时间维度的数组 计算顺序为 week -> month -> dayOfMonth -> hours -> minutes -> seconds -> nanos 为什么带有纳秒,这是为了做时间偏移用,也就是如果当前时间如果连续计算,会一直命中到同一个时间上,那么会用纳秒偏移,牵动second的偏移会计算到下一个可选值上
this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()};
nextOrSame这是一个重要的方法,作为CronField类的抽象方法,目前有三个实现BitsCronField:计算时分秒月,CompositeCronField计算关于day的复合规则例如L-2,5W 也就是使用了L 或者W 可以支持通过, 进行组合。QuartzCronField:专门用于计算dayOfWeek或者dayOfMonth的 L,W,#等特殊符号
@Nullable
public abstract <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal);
先看 QuartzCronField的实现
@Override
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
// 直接使用解析出来的 时间访问器定位时间
T result = adjust(temporal);
if (result != null) {
// 如果定位后的时间比传入的时间小
if (result.compareTo(temporal) < 0) {
// We ended up before the start, roll forward and try again
// 下一个field维度向前滚1一个单位,然后将当前时间维度时间设置为最小值,实际就是通过获取当前周期最大值减去当前值 + 1 就到了下个周期最小值
temporal = this.rollForwardType.rollForward(temporal);
//继续计算一次
result = adjust(temporal);
if (result != null) {
// 重置结果
result = type().reset(result);
return result;
rollForward 向前偏移到下一个field周期偏移1后并将当前field周期设置最小值
public <T extends Temporal & Comparable<? super T>> T rollForward(T temporal) {
int current = get(temporal);
ValueRange range = temporal.range(this.field);
// 核心计算逻辑
long amount = range.getMaximum() - current + 1;
T result = this.field.getBaseUnit().addTo(temporal, amount);
current = get(result);
range = result.range(this.field);
// adjust for daylight savings
if (current != range.getMinimum()) {
// 补偿设置为最小值
result = this.field.adjustInto(result, range.getMinimum());
return result;
public <T extends Temporal> T reset(T temporal) {
// 太熟悉的配方了,如果当前rollForward用了向前滚一个大周期的逻辑。会将当前周期以及比当前周期小的周期都重置为最小值,例如当前为 dayOfWeek,则如果发生了月向前偏移1个单位,则周,时,分,纳秒,都重置为最小值,现在这个逻辑是固定的。也就是可能重置的列表是固定的。每次发生高一个field周期偏移(执行了rollForward)一定重置,然后在外部会继续重新计算
for (ChronoField lowerOrder : this.lowerOrders) {
if (temporal.isSupported(lowerOrder)) {
temporal = lowerOrder.adjustInto(temporal, temporal.range(lowerOrder).getMinimum());
return temporal;
@Override
public String toString() {
return this.field.toString();
再看看CompositeCronField,这里的逻辑其实一定是一个field维度,只不过是处理表达式有 通过, 组合了 L W 等等逻辑
@Nullable
@Override
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
T result = null;
for (CronField field : this.fields) {
// 调用QuartzCronField 或者 BitsCronField 计算出结果
T candidate = field.nextOrSame(temporal);
if (result == null ||
candidate != null && candidate.compareTo(result) < 0) {
// 第一次计算出的结果直接赋值,之后计算出的结果不为null并且比上一次计算的结果小则替换,因为使用 , 组合的计算逻辑,需要取最小的满足逻辑的值
result = candidate;
return result;
然后看看BitsCronField 通过移位操作计算的逻辑
@Nullable
@Override
public <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
// 获取当前相对值
int current = type().get(temporal);
// 计算出下一个可选值
int next = nextSetBit(current);
if (next == -1) {
// 如果下一个可选值在当前field维度越界,则需要 下一个field周期维度向前偏移1个单位
temporal = type().rollForward(temporal);
// 将当前field维度周期设置为可选的最小值,这里如果会从下标0直接返回第一个true可选的下标,例如5,6之类
next = nextSetBit(0);
// 如果计算完的结果就是当前的值直接返回,即当前已经命中到可选值了
if (next == current) {
return temporal;
else {
// 如果当前相对值和期待可选值next不同需要用期待可选值next 对temporal对象计算
int count = 0;
current = type().get(temporal);
// 最多 366的循环次数,因为无论什么时间维度,366已经是最大跨度了
while (current != next && count++ < CronExpression.MAX_ATTEMPTS) {
// 通过计算出来的期待的相对值,计算到那个值,其实也可能偏移到下一个更大的field周期了,例如现在在计算时,可能通过这个计算到了明天的0点
temporal = type().elapseUntil(temporal, next);
// 再取当前相对值
current = type().get(temporal);
// 再计算下一个可选值,当前的current也是可选值会返回current也就是 current == next
next = nextSetBit(current);
if (next == -1) {
temporal = type().rollForward(temporal);
next = nextSetBit(0);
if (count >= CronExpression.MAX_ATTEMPTS) {
return null;
// 无论如何 当前时间维度虽然计算完成了,但是还是会重置为最小值,后续递归继续计算,目的是消除产生大一个field维度产生了变化后还需要重新计算,因为现在是从week -> month -> dayOfMonth -> hours -> minutes -> seconds -> nanos 计算,如果当前时间维度计算移动了,一定要从大的维度再计算一次。得到真正结果的那次计算一定是(next == current) 返回的
return type().reset(temporal);
再来看看 如何通过一个 long 和一个16进制变量替换了之前的bitSet
下面是所有操作方法
// 一个 2的64次方-1的数字,十六进制 小伙伴可以百度学习一下二进制,原码补码等知识,或者不需要了解也可以,需要简单了解& | ~ 等计算就是使用两个十进制数字的二进制状态下的下标0,1来进行boolean计算出结果0或者1,<< 等移位操作来设置对应下标的0,1,当前这个值0x表示16进制,而后面代表一个64个1组成的二进制数据
private static final long MASK = 0xFFFFFFFFFFFFFFFFL;
// we store at most 60 bits, for seconds and minutes, so a 64-bit long suffices
// 因为目前时间维度 field的计算 最大只有 60秒60分 到60,而day为31 month12,所以一个64位的数字足够了
// 用来存可选值的下标 0,1标记位转为10进制的数字
private long bits;
// 先从简单的看起
boolean getBit(int index) {
// 如何确定当前下标是否命中了可选值 用1 向左移位下标数量,那么这个数据只有对应下标位置为1,然后和可选值的下标进行&,那么其他位置肯定都是0,只有当输入参数的下标可选值bits中也是1会返回true
return (this.bits & (1L << index)) != 0;
// 设置下标为可选,使用| 则一定会设置为1,用1下标左移位命中
private void setBit(int index) {
this.bits |= (1L << index);
// 将对应下标取反 并且用 & 符号 保证置为0,这个操作永远会置为0
private void clearBit(int index) {
this.bits &= ~(1L << index);
// 通过范围值 或者固定值(1~1两边相等为固定值)设置下标
private void setBits(ValueRange range) {
if (range.getMinimum() == range.getMaximum()) {
setBit((int) range.getMinimum());
else {
// 通过最小值左移位,对应下标就是最小值
long minMask = MASK << range.getMinimum();
// 不太明白的操作,但是目的就是将从1 ~ 最大值的下标都设置为1
long maxMask = MASK >>> - (range.getMaximum() + 1);
// 两个值进行 & 那么1的交集只有 最小值到 最大值的下标区间
this.bits |= (minMask & maxMask);
private void setBits(ValueRange range, int delta) {
if (delta == 1) {
setBits(range);
else {
// 范围加 增量逻辑只能循环一个一个 设置下标位
for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) {
setBit(i);
// 取当前下标位的下一个可选值的下标位
private int nextSetBit(int fromIndex) {
// 先以64位都是1 的值进行左移位,那么一直到给定值下标位都变为0,然后&取两个变量的1的交集下标位置,实际就是取this.bits以传入值下标位开始包括传入的下标位为1的可选值下标位置
long result = this.bits & (MASK << fromIndex);
if (result != 0) {
// 将命中的(也就是第一个为1的下标位置的下标数值返回)
return Long.numberOfTrailingZeros(result);
else {
return -1;
总结及和旧版spring解析cron的区别
通过抽象CronField 类将时间field维度抽象出来,并提供了QuartzCronField来处理#,L,W等特殊字符,CompositeCronField来解析包含L,W的组合field,通过符号 , 类似固定值的组合,单独的 , 固定值不需要。BitsCronField新的可选值命中计算逻辑处理类。
BitsCronField使用一个 64位全是1的二进制变量,和一个 long类型变量进行位操作计算可选值。
换了Temporal接口的子类的计算
计算顺序变为了 week -> month -> dayOfMonth -> hours -> minutes -> seconds -> nanos
QuartzCronField结合jdk的TemporalAdjuster函数式接口来计算工作日,最后一天等L,W,# 的特殊逻辑
回溯重置逻辑变为固定的列表,即比当前时间维度小的所有时间维度,只要当前大的时间维度变化了,那么小的时间维度都要从最小值重新计算
计算结果逻辑为两次计算结果相同就返回,保证从大的时间维度到小的维度循环计算命中到可选值为止。