Skip to content

Commit 5b1cd83

Browse files
committed
Cron解析工具优化,解决day-of-month使用L时会跳过非31天的月份问题;
1 parent 905f8f0 commit 5b1cd83

2 files changed

Lines changed: 87 additions & 16 deletions

File tree

doc/XXL-JOB官方文档.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2814,7 +2814,7 @@ public void execute() {
28142814
- 13、【优化】执行器名称长度调整,最长支持64字符;
28152815
- 14、【优化】执行器注册表主键调整为long数据类型,防止大规模执行器集群注册数据溢出;
28162816
- 15、【优化】任务参数长度调整,最长支持2048字符;
2817-
2817+
- 16、【优化】Cron解析工具优化,解决day-of-month使用L时会跳过非31天的月份问题;
28182818
28192819
数据库升级脚本:
28202820
```

xxl-job-admin/src/main/java/com/xxl/job/admin/scheduler/cron/CronExpression.java

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
package com.xxl.job.admin.scheduler.cron;
2+
/*
3+
* All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
4+
* Copyright IBM Corp. 2024, 2025
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
7+
* use this file except in compliance with the License. You may obtain a copy
8+
* of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
* License for the specific language governing permissions and limitations
16+
* under the License.
17+
*
18+
* Borrowed from quartz v2.5.2
19+
*/
220

321
import java.io.Serializable;
422
import java.text.ParseException;
@@ -63,7 +81,7 @@
6381
* <tr>
6482
* <td><code>Month</code></td>
6583
* <td>&nbsp;</td>
66-
* <td><code>0-11 or JAN-DEC</code></td>
84+
* <td><code>1-12 or JAN-DEC</code></td>
6785
* <td>&nbsp;</td>
6886
* <td><code>, - * /</code></td>
6987
* </tr>
@@ -186,8 +204,6 @@
186204
* @author Sharada Jambula, James House
187205
* @author Contributions from Mads Henderson
188206
* @author Refactoring from CronTrigger to CronExpression by Aaron Craven
189-
*
190-
* Borrowed from quartz v2.5.0
191207
*/
192208
public final class CronExpression implements Serializable, Cloneable {
193209

@@ -649,6 +665,10 @@ protected int storeExpressionVals(int pos, String s, int type)
649665
addToSet(ALL_SPEC_INT, -1, incr, type);
650666
return i;
651667
} else if (c == 'L') {
668+
669+
if(type < DAY_OF_MONTH)
670+
throw new ParseException("'L' not expected in seconds, minutes or hours fields.", i);
671+
652672
i++;
653673
if (type == DAY_OF_WEEK) {
654674
addToSet(7, 7, 0, type);
@@ -705,15 +725,15 @@ protected int storeExpressionVals(int pos, String s, int type)
705725

706726
private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException {
707727
if (incr > 59 && (type == SECOND || type == MINUTE)) {
708-
throw new ParseException("Increment > 60 : " + incr, idxPos);
728+
throw new ParseException("Increment >= 60 : " + incr, idxPos);
709729
} else if (incr > 23 && (type == HOUR)) {
710-
throw new ParseException("Increment > 24 : " + incr, idxPos);
730+
throw new ParseException("Increment >= 24 : " + incr, idxPos);
711731
} else if (incr > 31 && (type == DAY_OF_MONTH)) {
712-
throw new ParseException("Increment > 31 : " + incr, idxPos);
732+
throw new ParseException("Increment >= 31 : " + incr, idxPos);
713733
} else if (incr > 7 && (type == DAY_OF_WEEK)) {
714-
throw new ParseException("Increment > 7 : " + incr, idxPos);
734+
throw new ParseException("Increment >= 7 : " + incr, idxPos);
715735
} else if (incr > 12 && (type == MONTH)) {
716-
throw new ParseException("Increment > 12 : " + incr, idxPos);
736+
throw new ParseException("Increment >= 12 : " + incr, idxPos);
717737
}
718738
}
719739

@@ -1293,15 +1313,20 @@ public Date getTimeAfter(Date afterTime) {
12931313
day = -1;
12941314
}
12951315
}
1316+
1317+
boolean needAdvance = false;
12961318
if (smallestDay.isPresent()) {
12971319
if (day == -1 || smallestDay.get() < day) {
12981320
day = smallestDay.get();
1321+
needAdvance = true;
12991322
}
13001323
} else if (day == -1) {
13011324
day = 1;
13021325
mon++;
1326+
needAdvance = true;
13031327
}
1304-
if (day != t || mon != tmon) {
1328+
1329+
if (needAdvance && (day != t || mon != tmon)) {
13051330
cl.set(Calendar.SECOND, 0);
13061331
cl.set(Calendar.MINUTE, 0);
13071332
cl.set(Calendar.HOUR_OF_DAY, 0);
@@ -1311,6 +1336,7 @@ public Date getTimeAfter(Date afterTime) {
13111336
// are 1-based
13121337
continue;
13131338
}
1339+
13141340
} else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
13151341
if (lastDayOfWeek) { // are we looking for the last XXX day of
13161342
// the month?
@@ -1523,12 +1549,53 @@ protected void setCalendarHour(Calendar cal, int hour) {
15231549
}
15241550

15251551
/**
1526-
* NOT YET IMPLEMENTED: Returns the time before the given time
1552+
* Returns the time before the given time
15271553
* that the <code>CronExpression</code> matches.
1554+
*
1555+
* @param endTime a time for which the previous
1556+
* matching time is returned
1557+
* @return the previous matching time before the given end time,
1558+
* or null if there are no previous matching times
15281559
*/
15291560
public Date getTimeBefore(Date endTime) {
1530-
// FUTURE_TODO: implement QUARTZ-423
1531-
return null;
1561+
// the current implementation is not a direct calculation, but rather
1562+
// uses getTimeAfter with a binary search to find the previous match time
1563+
long end = endTime.getTime();
1564+
long min = 0; // the epoch date is the minimum supported by this class
1565+
long max = end;
1566+
// check if it's satisfiable at all
1567+
Date date = new Date(min);
1568+
Date after = getTimeAfter(date);
1569+
if (after == null || after.getTime() >= end)
1570+
return null; // there are no after-times before end
1571+
// from this point forward min's time-after is always less than end,
1572+
// and max's time-after is always equal to or greater than end
1573+
// so we just need to shrink the interval until they meet.
1574+
// optimization - perform inverse binary search to find a tighter lower bound
1575+
long interval = 60 * 60 * 1000; // start with a reasonable interval
1576+
while (interval < max) {
1577+
date.setTime(max - interval);
1578+
after = getTimeAfter(date);
1579+
if (after != null && after.getTime() < max) {
1580+
min = date.getTime(); // found a closer min
1581+
break;
1582+
}
1583+
interval *= 2;
1584+
}
1585+
// perform a regular binary search to find the earliest moment
1586+
// whose time-after is equal to or greater than the end time -
1587+
// this moment is the previous match time itself
1588+
while (max - min > 1000) { // we can stop at 1 second resolution
1589+
long mid = (min + max) >>> 1;
1590+
date.setTime(mid);
1591+
after = getTimeAfter(date);
1592+
if (after != null && after.getTime() < end)
1593+
min = mid;
1594+
else
1595+
max = mid;
1596+
}
1597+
date.setTime(max - max % 1000); // round to second
1598+
return date;
15321599
}
15331600

15341601
/**
@@ -1585,18 +1652,22 @@ private Optional<Integer> findSmallestDay(int day, int mon, int year, TreeSet<In
15851652

15861653
final int lastDay = getLastDayOfMonth(mon, year);
15871654
// For "L", "L-1", etc.
1588-
int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day)))
1655+
final int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day)))
15891656
.map(d -> d - LAST_DAY_OFFSET_START + 1)
15901657
.orElse(Integer.MAX_VALUE);
15911658

15921659
// For "1", "2", etc.
15931660
SortedSet<Integer> st = set.subSet(day, LAST_DAY_OFFSET_START);
15941661
// make sure we don't over-run a short month, such as february
15951662
if (!st.isEmpty() && st.first() < smallestDay && st.first() <= lastDay) {
1596-
smallestDay = st.first();
1663+
return Optional.of(st.first());
15971664
}
15981665

1599-
return smallestDay == Integer.MAX_VALUE ? Optional.empty() : Optional.of(smallestDay);
1666+
if (smallestDay == Integer.MAX_VALUE) {
1667+
return Optional.empty();
1668+
} else {
1669+
return Optional.of(smallestDay + lastDay - LAST_DAY_OFFSET_START + 1);
1670+
}
16001671
}
16011672

16021673
private void readObject(java.io.ObjectInputStream stream)

0 commit comments

Comments
 (0)