相关文章推荐
直爽的小摩托  ·  sockets - How can I ...·  1 年前    · 
阳光的香槟  ·  How to: Create and ...·  1 年前    · 
痴情的松鼠  ·  基于 SVG Sprites ...·  1 年前    · 
精彩文章免费看

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支持
我们可以看到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提供的时间访问器
下面看看各个函数式接口的解析

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,# 的特殊逻辑
  • 回溯重置逻辑变为固定的列表,即比当前时间维度小的所有时间维度,只要当前大的时间维度变化了,那么小的时间维度都要从最小值重新计算
  • 计算结果逻辑为两次计算结果相同就返回,保证从大的时间维度到小的维度循环计算命中到可选值为止。
  •