diff --git a/doc/XXL-JOB官方文档.md b/doc/XXL-JOB官方文档.md index 9a406e31..8bffb948 100644 --- a/doc/XXL-JOB官方文档.md +++ b/doc/XXL-JOB官方文档.md @@ -2814,7 +2814,7 @@ public void execute() { - 13、【优化】执行器名称长度调整,最长支持64字符; - 14、【优化】执行器注册表主键调整为long数据类型,防止大规模执行器集群注册数据溢出; - 15、【优化】任务参数长度调整,最长支持2048字符; - +- 16、【优化】Cron解析工具优化,解决day-of-month使用L时会跳过非31天的月份问题; 数据库升级脚本: ``` diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/cron/CronExpression.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/cron/CronExpression.java index 212178df..65e5b607 100644 --- a/xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/cron/CronExpression.java +++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/cron/CronExpression.java @@ -1,4 +1,22 @@ package com.xxl.job.admin.scheduler.cron; +/* + * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved. + * Copyright IBM Corp. 2024, 2025 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Borrowed from quartz v2.5.2 + */ import java.io.Serializable; import java.text.ParseException; @@ -63,7 +81,7 @@ import java.util.TreeSet; * * Month *   - * 0-11 or JAN-DEC + * 1-12 or JAN-DEC *   * , - * / * @@ -186,8 +204,6 @@ import java.util.TreeSet; * @author Sharada Jambula, James House * @author Contributions from Mads Henderson * @author Refactoring from CronTrigger to CronExpression by Aaron Craven - * - * Borrowed from quartz v2.5.0 */ public final class CronExpression implements Serializable, Cloneable { @@ -649,6 +665,10 @@ public final class CronExpression implements Serializable, Cloneable { addToSet(ALL_SPEC_INT, -1, incr, type); return i; } else if (c == 'L') { + + if(type < DAY_OF_MONTH) + throw new ParseException("'L' not expected in seconds, minutes or hours fields.", i); + i++; if (type == DAY_OF_WEEK) { addToSet(7, 7, 0, type); @@ -705,15 +725,15 @@ public final class CronExpression implements Serializable, Cloneable { private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { if (incr > 59 && (type == SECOND || type == MINUTE)) { - throw new ParseException("Increment > 60 : " + incr, idxPos); + throw new ParseException("Increment >= 60 : " + incr, idxPos); } else if (incr > 23 && (type == HOUR)) { - throw new ParseException("Increment > 24 : " + incr, idxPos); + throw new ParseException("Increment >= 24 : " + incr, idxPos); } else if (incr > 31 && (type == DAY_OF_MONTH)) { - throw new ParseException("Increment > 31 : " + incr, idxPos); + throw new ParseException("Increment >= 31 : " + incr, idxPos); } else if (incr > 7 && (type == DAY_OF_WEEK)) { - throw new ParseException("Increment > 7 : " + incr, idxPos); + throw new ParseException("Increment >= 7 : " + incr, idxPos); } else if (incr > 12 && (type == MONTH)) { - throw new ParseException("Increment > 12 : " + incr, idxPos); + throw new ParseException("Increment >= 12 : " + incr, idxPos); } } @@ -1293,15 +1313,20 @@ public final class CronExpression implements Serializable, Cloneable { day = -1; } } + + boolean needAdvance = false; if (smallestDay.isPresent()) { if (day == -1 || smallestDay.get() < day) { day = smallestDay.get(); + needAdvance = true; } } else if (day == -1) { day = 1; mon++; + needAdvance = true; } - if (day != t || mon != tmon) { + + if (needAdvance && (day != t || mon != tmon)) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); @@ -1311,6 +1336,7 @@ public final class CronExpression implements Serializable, Cloneable { // are 1-based continue; } + } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule if (lastDayOfWeek) { // are we looking for the last XXX day of // the month? @@ -1523,12 +1549,53 @@ public final class CronExpression implements Serializable, Cloneable { } /** - * NOT YET IMPLEMENTED: Returns the time before the given time + * Returns the time before the given time * that the CronExpression matches. + * + * @param endTime a time for which the previous + * matching time is returned + * @return the previous matching time before the given end time, + * or null if there are no previous matching times */ public Date getTimeBefore(Date endTime) { - // FUTURE_TODO: implement QUARTZ-423 - return null; + // the current implementation is not a direct calculation, but rather + // uses getTimeAfter with a binary search to find the previous match time + long end = endTime.getTime(); + long min = 0; // the epoch date is the minimum supported by this class + long max = end; + // check if it's satisfiable at all + Date date = new Date(min); + Date after = getTimeAfter(date); + if (after == null || after.getTime() >= end) + return null; // there are no after-times before end + // from this point forward min's time-after is always less than end, + // and max's time-after is always equal to or greater than end + // so we just need to shrink the interval until they meet. + // optimization - perform inverse binary search to find a tighter lower bound + long interval = 60 * 60 * 1000; // start with a reasonable interval + while (interval < max) { + date.setTime(max - interval); + after = getTimeAfter(date); + if (after != null && after.getTime() < max) { + min = date.getTime(); // found a closer min + break; + } + interval *= 2; + } + // perform a regular binary search to find the earliest moment + // whose time-after is equal to or greater than the end time - + // this moment is the previous match time itself + while (max - min > 1000) { // we can stop at 1 second resolution + long mid = (min + max) >>> 1; + date.setTime(mid); + after = getTimeAfter(date); + if (after != null && after.getTime() < end) + min = mid; + else + max = mid; + } + date.setTime(max - max % 1000); // round to second + return date; } /** @@ -1585,7 +1652,7 @@ public final class CronExpression implements Serializable, Cloneable { final int lastDay = getLastDayOfMonth(mon, year); // For "L", "L-1", etc. - int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day))) + final int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day))) .map(d -> d - LAST_DAY_OFFSET_START + 1) .orElse(Integer.MAX_VALUE); @@ -1593,10 +1660,14 @@ public final class CronExpression implements Serializable, Cloneable { SortedSet st = set.subSet(day, LAST_DAY_OFFSET_START); // make sure we don't over-run a short month, such as february if (!st.isEmpty() && st.first() < smallestDay && st.first() <= lastDay) { - smallestDay = st.first(); + return Optional.of(st.first()); } - return smallestDay == Integer.MAX_VALUE ? Optional.empty() : Optional.of(smallestDay); + if (smallestDay == Integer.MAX_VALUE) { + return Optional.empty(); + } else { + return Optional.of(smallestDay + lastDay - LAST_DAY_OFFSET_START + 1); + } } private void readObject(java.io.ObjectInputStream stream)