美文网首页
java实现cron解析计算,spring5.3.x的实现

java实现cron解析计算,spring5.3.x的实现

作者: 二哈_8fd0 | 来源:发表于2022-05-14 22:42 被阅读0次

    java实现对cron表达式解析,spring5.2.x的实现 - 简书 (jianshu.com)
    上一篇文章分析了 spring5.2.x的版本对cron表达式的解析及计算通过 CronSequenceGenerator 计算,我们看到其使用Calendar类进行计算,那么并没有使用jdk8添加的Temporal类及其子类(包括LocalDateTime等),jdk8对java的日期相关类进行了重构升级,提供了线程安全的更方便的api,那么spring最新版本是不是也有了新的支持呢。

    新的cron支持
    我们可以看到CronExpression是核心解析处理类,copy出来后发现还有上图的几个类的依赖,其他类是关于@Scheduled的其他逻辑实现,我们只要看cron的解析计算
    更详细的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提供的时间访问器
    TemporalAdjusters

    有week,后移前移的各种逻辑,真是方便多了。

    下面看看各个函数式接口的解析

    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 的泛型

    理论上 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的区别

    1. 通过抽象CronField 类将时间field维度抽象出来,并提供了QuartzCronField来处理#,L,W等特殊字符,CompositeCronField来解析包含L,W的组合field,通过符号 , 类似固定值的组合,单独的 , 固定值不需要。BitsCronField新的可选值命中计算逻辑处理类。
    2. BitsCronField使用一个 64位全是1的二进制变量,和一个 long类型变量进行位操作计算可选值。
    3. 换了Temporal接口的子类的计算
    4. 计算顺序变为了 week -> month -> dayOfMonth -> hours -> minutes -> seconds -> nanos
    5. QuartzCronField结合jdk的TemporalAdjuster函数式接口来计算工作日,最后一天等L,W,# 的特殊逻辑
    6. 回溯重置逻辑变为固定的列表,即比当前时间维度小的所有时间维度,只要当前大的时间维度变化了,那么小的时间维度都要从最小值重新计算
    7. 计算结果逻辑为两次计算结果相同就返回,保证从大的时间维度到小的维度循环计算命中到可选值为止。

    相关文章

      网友评论

          本文标题:java实现cron解析计算,spring5.3.x的实现

          本文链接:https://www.haomeiwen.com/subject/altmurtx.html