From f0d36458885107dd0a0d9d013bfe68ec16df34fe Mon Sep 17 00:00:00 2001
From: xuxueli <931591021@qq.com>
Date: Sun, 16 Feb 2025 14:25:34 +0800
Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E5=BA=A6=E4=B8=AD=E5=BF=83Cron?=
=?UTF-8?q?=E5=89=8D=E7=AB=AF=E7=BB=84=E4=BB=B6=E4=BC=98=E5=8C=96=EF=BC=8C?=
=?UTF-8?q?=E8=A7=A3=E5=86=B3week=E9=85=8D=E7=BD=AE=E4=B8=8E=E5=90=8E?=
=?UTF-8?q?=E7=AB=AF=E5=85=BC=E5=AE=B9=E6=80=A7=E9=97=AE=E9=A2=98(ISSUE-22?=
=?UTF-8?q?20)=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
doc/XXL-JOB官方文档.md | 5 +-
.../job/admin/core/cron/CronExpression.java | 648 ++++++++----------
.../static/plugins/cronGen/cronGen.js | 15 +-
.../static/plugins/cronGen/cronGen_en.js | 4 +-
.../admin/core/util/CronExpressionTest.java | 23 +
5 files changed, 338 insertions(+), 357 deletions(-)
create mode 100644 xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/CronExpressionTest.java
diff --git a/doc/XXL-JOB官方文档.md b/doc/XXL-JOB官方文档.md
index 9c310f8f..5290021d 100644
--- a/doc/XXL-JOB官方文档.md
+++ b/doc/XXL-JOB官方文档.md
@@ -2455,8 +2455,9 @@ public void execute() {
### 7.38 版本 v3.0.1 Release Notes[规划中]
- 1、【修复】任务操作逻辑优化,修复边界情况下逻辑中断问题(ISSUE-2081)。
-- 2、[规划中]登陆态Token生成逻辑优化,混淆登陆时间属性,降低token泄漏风险;
-- 3、[规划中]组件扫描改为BeanPostProcessor方式,避免小概率情况下提前初始化;底层组件移除单例写法,汇总factory统一管理;
+- 2、【修复】调度中心Cron前端组件优化,解决week配置与后端兼容性问题(ISSUE-2220)。
+- 3、[规划中]登陆态Token生成逻辑优化,混淆登陆时间属性,降低token泄漏风险;
+- 4、[规划中]组件扫描改为BeanPostProcessor方式,避免小概率情况下提前初始化;底层组件移除单例写法,汇总factory统一管理;
### TODO LIST
- 1、调度隔离:调度中心针对不同执行器,各自维护不同的调度和远程触发组件。
diff --git a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java b/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
index 42f9918e..ff246aa4 100644
--- a/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
+++ b/xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
@@ -1,20 +1,3 @@
-/*
- * All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
- *
- * 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.
- *
- */
-
package com.xxl.job.admin.core.cron;
import java.io.Serializable;
@@ -25,145 +8,153 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
/**
- * Provides a parser and evaluator for unix-like cron expressions. Cron
+ * Provides a parser and evaluator for unix-like cron expressions. Cron
* expressions provide the ability to specify complex time combinations such as
- * "At 8:00am every Monday through Friday" or "At 1:30am every
- * last Friday of the month".
- *
+ * "At 8:00am every Monday through Friday" or "At 1:30am every
+ * last Friday of the month".
+ *
* Cron expressions are comprised of 6 required fields and one optional field
* separated by white space. The fields respectively are described as follows:
- *
- *
+ *
+ *
+ *
Examples of cron expressions and their meanings.
*
- *
Field Name
- *
- *
Allowed Values
- *
- *
Allowed Special Characters
+ *
Field Name
+ *
+ *
Allowed Values
+ *
+ *
Allowed Special Characters
*
*
- *
Seconds
- *
- *
0-59
- *
- *
, - * /
+ *
Seconds
+ *
+ *
0-59
+ *
+ *
, - * /
*
*
- *
Minutes
- *
- *
0-59
- *
- *
, - * /
+ *
Minutes
+ *
+ *
0-59
+ *
+ *
, - * /
*
*
- *
Hours
- *
- *
0-23
- *
- *
, - * /
+ *
Hours
+ *
+ *
0-23
+ *
+ *
, - * /
*
*
- *
Day-of-month
- *
- *
1-31
- *
- *
, - * ? / L W
+ *
Day-of-month
+ *
+ *
1-31
+ *
+ *
, - * ? / L W
*
*
- *
Month
- *
- *
0-11 or JAN-DEC
- *
- *
, - * /
+ *
Month
+ *
+ *
0-11 or JAN-DEC
+ *
+ *
, - * /
*
*
- *
Day-of-Week
- *
- *
1-7 or SUN-SAT
- *
- *
, - * ? / L #
+ *
Day-of-Week
+ *
+ *
1-7 or SUN-SAT
+ *
+ *
, - * ? / L #
*
*
- *
Year (Optional)
- *
- *
empty, 1970-2199
- *
- *
, - * /
+ *
Year (Optional)
+ *
+ *
empty, 1970-2199
+ *
+ *
, - * /
*
*
- *
- * The '*' character is used to specify all values. For example, "*"
+ *
+ * The '*' character is used to specify all values. For example, "*"
* in the minute field means "every minute".
- *
+ *
+ *
* The '?' character is allowed for the day-of-month and day-of-week fields. It
* is used to specify 'no specific value'. This is useful when you need to
* specify something in one of the two fields, but not the other.
- *
+ *
* The '-' character is used to specify ranges For example "10-12" in
* the hour field means "the hours 10, 11 and 12".
- *
+ *
* The ',' character is used to specify additional values. For example
* "MON,WED,FRI" in the day-of-week field means "the days Monday,
* Wednesday, and Friday".
- *
+ *
+ *
* The '/' character is used to specify increments. For example "0/15"
- * in the seconds field means "the seconds 0, 15, 30, and 45". And
+ * in the seconds field means "the seconds 0, 15, 30, and 45". And
* "5/15" in the seconds field means "the seconds 5, 20, 35, and
* 50". Specifying '*' before the '/' is equivalent to specifying 0 is
* the value to start with. Essentially, for each field in the expression, there
- * is a set of numbers that can be turned on or off. For seconds and minutes,
+ * is a set of numbers that can be turned on or off. For seconds and minutes,
* the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
* 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn
* on every "nth" value in the given set. Thus "7/6" in the
- * month field only turns on month "7", it does NOT mean every 6th
- * month, please note that subtlety.
- *
+ * month field only turns on month "7", it does NOT mean every 6th
+ * month, please note that subtlety.
+ *
+ *
* The 'L' character is allowed for the day-of-month and day-of-week fields.
- * This character is short-hand for "last", but it has different
- * meaning in each of the two fields. For example, the value "L" in
- * the day-of-month field means "the last day of the month" - day 31
- * for January, day 28 for February on non-leap years. If used in the
- * day-of-week field by itself, it simply means "7" or
+ * This character is short-hand for "last", but it has different
+ * meaning in each of the two fields. For example, the value "L" in
+ * the day-of-month field means "the last day of the month" - day 31
+ * for January, day 28 for February on non-leap years. If used in the
+ * day-of-week field by itself, it simply means "7" or
* "SAT". But if used in the day-of-week field after another value, it
* means "the last xxx day of the month" - for example "6L"
- * means "the last friday of the month". You can also specify an offset
- * from the last day of the month, such as "L-3" which would mean the third-to-last
- * day of the calendar month. When using the 'L' option, it is important not to
+ * means "the last friday of the month". You can also specify an offset
+ * from the last day of the month, such as "L-3" which would mean the third-to-last
+ * day of the calendar month. When using the 'L' option, it is important not to
* specify lists, or ranges of values, as you'll get confusing/unexpected results.
- *
- * The 'W' character is allowed for the day-of-month field. This character
- * is used to specify the weekday (Monday-Friday) nearest the given day. As an
- * example, if you were to specify "15W" as the value for the
+ *
+ *
+ * The 'W' character is allowed for the day-of-month field. This character
+ * is used to specify the weekday (Monday-Friday) nearest the given day. As an
+ * example, if you were to specify "15W" as the value for the
* day-of-month field, the meaning is: "the nearest weekday to the 15th of
- * the month". So if the 15th is a Saturday, the trigger will fire on
+ * the month". So if the 15th is a Saturday, the trigger will fire on
* Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
- * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
+ * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
* However if you specify "1W" as the value for day-of-month, and the
- * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
- * 'jump' over the boundary of a month's days. The 'W' character can only be
+ * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
+ * 'jump' over the boundary of a month's days. The 'W' character can only be
* specified when the day-of-month is a single day, not a range or list of days.
- *
- * The 'L' and 'W' characters can also be combined for the day-of-month
- * expression to yield 'LW', which translates to "last weekday of the
+ *
+ *
+ * The 'L' and 'W' characters can also be combined for the day-of-month
+ * expression to yield 'LW', which translates to "last weekday of the
* month".
- *
+ *
+ *
* The '#' character is allowed for the day-of-week field. This character is
- * used to specify "the nth" XXX day of the month. For example, the
- * value of "6#3" in the day-of-week field means the third Friday of
- * the month (day 6 = Friday and "#3" = the 3rd one in the month).
- * Other examples: "2#1" = the first Monday of the month and
+ * used to specify "the nth" XXX day of the month. For example, the
+ * value of "6#3" in the day-of-week field means the third Friday of
+ * the month (day 6 = Friday and "#3" = the 3rd one in the month).
+ * Other examples: "2#1" = the first Monday of the month and
* "4#5" = the fifth Wednesday of the month. Note that if you specify
* "#5" and there is not 5 of the given day-of-week in the month, then
* no firing will occur that month. If the '#' character is used, there can
- * only be one expression in the day-of-week field ("3#1,6#3" is
+ * only be one expression in the day-of-week field ("3#1,6#3" is
* not valid, since there are two expressions).
- *
+ *
*
- *
+ *
* The legal characters and the names of months and days of the week are not
* case sensitive.
- *
+ *
*
* NOTES:
+ *
*
*
Support for specifying both a day-of-week and a day-of-month value is
* not complete (you'll need to use the '?' character in one of these fields).
*
- *
Overflowing ranges is supported - that is, having a larger number on
- * the left hand side than the right. You might do 22-2 to catch 10 o'clock
- * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
- * very important to note that overuse of overflowing ranges creates ranges
- * that don't make sense and no effort has been made to determine which
- * interpretation CronExpression chooses. An example would be
+ *
Overflowing ranges is supported - that is, having a larger number on
+ * the left hand side than the right. You might do 22-2 to catch 10 o'clock
+ * at night until 2 o'clock in the morning, or you might have NOV-FEB. It is
+ * very important to note that overuse of overflowing ranges creates ranges
+ * that don't make sense and no effort has been made to determine which
+ * interpretation CronExpression chooses. An example would be
* "0 0 14-6 ? * FRI-MON".
*
- *
- *
- *
+ *
+ *
* @author Sharada Jambula, James House
* @author Contributions from Mads Henderson
* @author Refactoring from CronTrigger to CronExpression by Aaron Craven
*
- * Borrowed from quartz v2.3.1
- *
+ * Borrowed from quartz v2.5.0
*/
public final class CronExpression implements Serializable, Cloneable {
private static final long serialVersionUID = 12423409423L;
-
+
protected static final int SECOND = 0;
protected static final int MINUTE = 1;
protected static final int HOUR = 2;
@@ -212,11 +202,14 @@ public final class CronExpression implements Serializable, Cloneable {
protected static final int YEAR = 6;
protected static final int ALL_SPEC_INT = 99; // '*'
protected static final int NO_SPEC_INT = 98; // '?'
+ protected static final int MAX_LAST_DAY_OFFSET = 30;
+ protected static final int LAST_DAY_OFFSET_START = 32; // "L-30"
+ protected static final int LAST_DAY_OFFSET_END = LAST_DAY_OFFSET_START + MAX_LAST_DAY_OFFSET; // 'L'
protected static final Integer ALL_SPEC = ALL_SPEC_INT;
protected static final Integer NO_SPEC = NO_SPEC_INT;
-
- protected static final Map monthMap = new HashMap(20);
- protected static final Map dayMap = new HashMap(60);
+
+ protected static final Map monthMap = new HashMap<>(20);
+ protected static final Map dayMap = new HashMap<>(60);
static {
monthMap.put("JAN", 0);
monthMap.put("FEB", 1);
@@ -246,43 +239,41 @@ public final class CronExpression implements Serializable, Cloneable {
protected transient TreeSet minutes;
protected transient TreeSet hours;
protected transient TreeSet daysOfMonth;
+ protected transient TreeSet nearestWeekdays;
protected transient TreeSet months;
protected transient TreeSet daysOfWeek;
protected transient TreeSet years;
- protected transient boolean lastdayOfWeek = false;
- protected transient int nthdayOfWeek = 0;
- protected transient boolean lastdayOfMonth = false;
- protected transient boolean nearestWeekday = false;
- protected transient int lastdayOffset = 0;
+ protected transient boolean lastDayOfWeek = false;
+ protected transient int nthDayOfWeek = 0;
protected transient boolean expressionParsed = false;
-
+
public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100;
/**
- * Constructs a new CronExpression based on the specified
+ * Constructs a new CronExpression based on the specified
* parameter.
- *
+ *
* @param cronExpression String representation of the cron expression the
* new object should represent
* @throws java.text.ParseException
- * if the string expression cannot be parsed into a valid
+ * if the string expression cannot be parsed into a valid
* CronExpression
*/
public CronExpression(String cronExpression) throws ParseException {
if (cronExpression == null) {
throw new IllegalArgumentException("cronExpression cannot be null");
}
-
+
this.cronExpression = cronExpression.toUpperCase(Locale.US);
-
+
buildExpression(this.cronExpression);
}
-
+
/**
* Constructs a new {@code CronExpression} as a copy of an existing
* instance.
- *
+ *
* @param expression
* The existing cron expression to be copied
*/
@@ -296,7 +287,7 @@ public final class CronExpression implements Serializable, Cloneable {
try {
buildExpression(cronExpression);
} catch (ParseException ex) {
- throw new AssertionError();
+ throw new AssertionError("Could not parse expression!", ex);
}
if (expression.getTimeZone() != null) {
setTimeZone((TimeZone) expression.getTimeZone().clone());
@@ -307,7 +298,7 @@ public final class CronExpression implements Serializable, Cloneable {
* Indicates whether the given date satisfies the cron expression. Note that
* milliseconds are ignored, so two Dates falling on different milliseconds
* of the same second will always have the same result here.
- *
+ *
* @param date the date to evaluate
* @return a boolean indicating whether the given date satisfies the cron
* expression
@@ -317,18 +308,18 @@ public final class CronExpression implements Serializable, Cloneable {
testDateCal.setTime(date);
testDateCal.set(Calendar.MILLISECOND, 0);
Date originalDate = testDateCal.getTime();
-
+
testDateCal.add(Calendar.SECOND, -1);
-
+
Date timeAfter = getTimeAfter(testDateCal.getTime());
return ((timeAfter != null) && (timeAfter.equals(originalDate)));
}
-
+
/**
* Returns the next date/time after the given date/time which
* satisfies the cron expression.
- *
+ *
* @param date the date/time at which to begin the search for the next valid
* date/time
* @return the next valid date/time
@@ -336,48 +327,48 @@ public final class CronExpression implements Serializable, Cloneable {
public Date getNextValidTimeAfter(Date date) {
return getTimeAfter(date);
}
-
+
/**
* Returns the next date/time after the given date/time which does
* not satisfy the expression
- *
- * @param date the date/time at which to begin the search for the next
+ *
+ * @param date the date/time at which to begin the search for the next
* invalid date/time
* @return the next valid date/time
*/
public Date getNextInvalidTimeAfter(Date date) {
long difference = 1000;
-
+
//move back to the nearest second so differences will be accurate
Calendar adjustCal = Calendar.getInstance(getTimeZone());
adjustCal.setTime(date);
adjustCal.set(Calendar.MILLISECOND, 0);
Date lastDate = adjustCal.getTime();
-
+
Date newDate;
-
+
//FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution.
-
+
//keep getting the next included time until it's farther than one second
// apart. At that point, lastDate is the last valid fire time. We return
// the second immediately following it.
while (difference == 1000) {
newDate = getTimeAfter(lastDate);
- if(newDate == null) {
+ if(newDate == null)
break;
- }
+
difference = newDate.getTime() - lastDate.getTime();
-
+
if (difference == 1000) {
lastDate = newDate;
}
}
-
+
return new Date(lastDate.getTime() + 1000);
}
-
+
/**
- * Returns the time zone for which this CronExpression
+ * Returns the time zone for which this CronExpression
* will be resolved.
*/
public TimeZone getTimeZone() {
@@ -389,16 +380,16 @@ public final class CronExpression implements Serializable, Cloneable {
}
/**
- * Sets the time zone for which this CronExpression
+ * Sets the time zone for which this CronExpression
* will be resolved.
*/
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}
-
+
/**
* Returns the string representation of the CronExpression
- *
+ *
* @return a string representation of the CronExpression
*/
@Override
@@ -407,30 +398,30 @@ public final class CronExpression implements Serializable, Cloneable {
}
/**
- * Indicates whether the specified cron expression can be parsed into a
+ * Indicates whether the specified cron expression can be parsed into a
* valid cron expression
- *
+ *
* @param cronExpression the expression to evaluate
* @return a boolean indicating whether the given expression is a valid cron
* expression
*/
public static boolean isValidExpression(String cronExpression) {
-
+
try {
new CronExpression(cronExpression);
} catch (ParseException pe) {
return false;
}
-
+
return true;
}
public static void validateExpression(String cronExpression) throws ParseException {
-
+
new CronExpression(cronExpression);
}
-
-
+
+
////////////////////////////////////////////////////////////////////////////
//
// Expression Parsing Functions
@@ -443,25 +434,28 @@ public final class CronExpression implements Serializable, Cloneable {
try {
if (seconds == null) {
- seconds = new TreeSet();
+ seconds = new TreeSet<>();
}
if (minutes == null) {
- minutes = new TreeSet();
+ minutes = new TreeSet<>();
}
if (hours == null) {
- hours = new TreeSet();
+ hours = new TreeSet<>();
}
if (daysOfMonth == null) {
- daysOfMonth = new TreeSet();
+ daysOfMonth = new TreeSet<>();
+ }
+ if (nearestWeekdays == null) {
+ nearestWeekdays = new TreeSet<>();
}
if (months == null) {
- months = new TreeSet();
+ months = new TreeSet<>();
}
if (daysOfWeek == null) {
- daysOfWeek = new TreeSet();
+ daysOfWeek = new TreeSet<>();
}
if (years == null) {
- years = new TreeSet();
+ years = new TreeSet<>();
}
int exprOn = SECOND;
@@ -469,13 +463,13 @@ public final class CronExpression implements Serializable, Cloneable {
StringTokenizer exprsTok = new StringTokenizer(expression, " \t",
false);
+ if(exprsTok.countTokens() > 7) {
+ throw new ParseException("Invalid expression has too many terms: " + expression, -1);
+ }
+
while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
String expr = exprsTok.nextToken().trim();
- // throw an exception if L is used with other days of the month
- if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
- throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1);
- }
// throw an exception if L is used with other days of the week
if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) {
throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1);
@@ -483,7 +477,7 @@ public final class CronExpression implements Serializable, Cloneable {
if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) {
throw new ParseException("Support for specifying multiple \"nth\" days is not implemented.", -1);
}
-
+
StringTokenizer vTok = new StringTokenizer(expr, ",");
while (vTok.hasMoreTokens()) {
String v = vTok.nextToken();
@@ -495,7 +489,7 @@ public final class CronExpression implements Serializable, Cloneable {
if (exprOn <= DAY_OF_WEEK) {
throw new ParseException("Unexpected end of expression.",
- expression.length());
+ expression.length());
}
if (exprOn <= YEAR) {
@@ -519,12 +513,12 @@ public final class CronExpression implements Serializable, Cloneable {
throw pe;
} catch (Exception e) {
throw new ParseException("Illegal cron expression format ("
- + e.toString() + ")", 0);
+ + e + ")", 0);
}
}
protected int storeExpressionVals(int pos, String s, int type)
- throws ParseException {
+ throws ParseException {
int incr = 0;
int i = skipWhiteSpace(pos, s);
@@ -556,7 +550,7 @@ public final class CronExpression implements Serializable, Cloneable {
sval = getDayOfWeekNumber(sub);
if (sval < 0) {
throw new ParseException("Invalid Day-of-Week value: '"
- + sub + "'", i);
+ + sub + "'", i);
}
if (s.length() > i + 3) {
c = s.charAt(i + 3);
@@ -567,13 +561,13 @@ public final class CronExpression implements Serializable, Cloneable {
if (eval < 0) {
throw new ParseException(
"Invalid Day-of-Week value: '" + sub
- + "'", i);
+ + "'", i);
}
} else if (c == '#') {
try {
i += 4;
- nthdayOfWeek = Integer.parseInt(s.substring(i));
- if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
+ nthDayOfWeek = Integer.parseInt(s.substring(i));
+ if (nthDayOfWeek < 1 || nthDayOfWeek > 5) {
throw new Exception();
}
} catch (Exception e) {
@@ -582,7 +576,7 @@ public final class CronExpression implements Serializable, Cloneable {
i);
}
} else if (c == 'L') {
- lastdayOfWeek = true;
+ lastDayOfWeek = true;
i++;
}
}
@@ -601,22 +595,21 @@ public final class CronExpression implements Serializable, Cloneable {
if (c == '?') {
i++;
- if ((i + 1) < s.length()
+ if ((i + 1) < s.length()
&& (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
throw new ParseException("Illegal character after '?': "
- + s.charAt(i), i);
+ + s.charAt(i), i);
}
if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
throw new ParseException(
- "'?' can only be specified for Day-of-Month or Day-of-Week.",
- i);
+ "'?' can only be specified for Day-of-Month or Day-of-Week.",
+ i);
}
- if (type == DAY_OF_WEEK && !lastdayOfMonth) {
- int val = daysOfMonth.last();
- if (val == NO_SPEC_INT) {
+ if (type == DAY_OF_WEEK) {
+ if (!daysOfMonth.isEmpty() && daysOfMonth.last() == NO_SPEC_INT) {
throw new ParseException(
- "'?' can only be specified for Day-of-Month -OR- Day-of-Week.",
- i);
+ "'?' can only be specified for Day-of-Month -OR- Day-of-Week.",
+ i);
}
}
@@ -630,7 +623,7 @@ public final class CronExpression implements Serializable, Cloneable {
return i + 1;
} else if (c == '/'
&& ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s
- .charAt(i + 1) == '\t')) {
+ .charAt(i + 1) == '\t')) {
throw new ParseException("'/' must be followed by an integer.", i);
} else if (c == '*') {
i++;
@@ -657,29 +650,35 @@ public final class CronExpression implements Serializable, Cloneable {
return i;
} else if (c == 'L') {
i++;
- if (type == DAY_OF_MONTH) {
- lastdayOfMonth = true;
- }
if (type == DAY_OF_WEEK) {
addToSet(7, 7, 0, type);
}
- if(type == DAY_OF_MONTH && s.length() > i) {
- c = s.charAt(i);
- if(c == '-') {
- ValueSet vs = getValue(0, s, i+1);
- lastdayOffset = vs.value;
- if(lastdayOffset > 30) {
- throw new ParseException("Offset from last day must be <= 30", i + 1);
- }
- i = vs.pos;
- }
- if(s.length() > i) {
+ if (type == DAY_OF_MONTH) {
+ int dom = LAST_DAY_OFFSET_END;
+ boolean nearestWeekday = false;
+ if (s.length() > i) {
c = s.charAt(i);
- if(c == 'W') {
- nearestWeekday = true;
- i++;
+ if (c == '-') {
+ ValueSet vs = getValue(0, s, i + 1);
+ int offset = vs.value;
+ if (offset > MAX_LAST_DAY_OFFSET)
+ throw new ParseException("Offset from last day must be <= " + MAX_LAST_DAY_OFFSET, i + 1);
+ dom -= offset;
+ i = vs.pos;
+ }
+ if (s.length() > i) {
+ c = s.charAt(i);
+ if (c == 'W') {
+ nearestWeekday = true;
+ i++;
+ }
}
}
+ if (nearestWeekday) {
+ nearestWeekdays.add(dom);
+ } else {
+ daysOfMonth.add(dom);
+ }
}
return i;
} else if (c >= '0' && c <= '9') {
@@ -719,8 +718,8 @@ public final class CronExpression implements Serializable, Cloneable {
}
protected int checkNext(int pos, String s, int val, int type)
- throws ParseException {
-
+ throws ParseException {
+
int end = -1;
int i = pos;
@@ -733,10 +732,9 @@ public final class CronExpression implements Serializable, Cloneable {
if (c == 'L') {
if (type == DAY_OF_WEEK) {
- if(val < 1 || val > 7) {
+ if(val < 1 || val > 7)
throw new ParseException("Day-of-Week values must be between 1 and 7", -1);
- }
- lastdayOfWeek = true;
+ lastDayOfWeek = true;
} else {
throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
}
@@ -745,18 +743,14 @@ public final class CronExpression implements Serializable, Cloneable {
i++;
return i;
}
-
+
if (c == 'W') {
- if (type == DAY_OF_MONTH) {
- nearestWeekday = true;
- } else {
+ if (type != DAY_OF_MONTH) {
throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
}
- if(val > 31) {
+ if(val > 31)
throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i);
- }
- TreeSet set = getSet(type);
- set.add(val);
+ nearestWeekdays.add(val);
i++;
return i;
}
@@ -767,8 +761,8 @@ public final class CronExpression implements Serializable, Cloneable {
}
i++;
try {
- nthdayOfWeek = Integer.parseInt(s.substring(i));
- if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
+ nthDayOfWeek = Integer.parseInt(s.substring(i));
+ if (nthDayOfWeek < 1 || nthDayOfWeek > 5) {
throw new Exception();
}
} catch (Exception e) {
@@ -860,7 +854,7 @@ public final class CronExpression implements Serializable, Cloneable {
public String getCronExpression() {
return cronExpression;
}
-
+
public String getExpressionSummary() {
StringBuilder buf = new StringBuilder();
@@ -876,23 +870,20 @@ public final class CronExpression implements Serializable, Cloneable {
buf.append("daysOfMonth: ");
buf.append(getExpressionSetSummary(daysOfMonth));
buf.append("\n");
+ buf.append("nearestWeekdays: ");
+ buf.append(getExpressionSetSummary(nearestWeekdays));
+ buf.append("\n");
buf.append("months: ");
buf.append(getExpressionSetSummary(months));
buf.append("\n");
buf.append("daysOfWeek: ");
buf.append(getExpressionSetSummary(daysOfWeek));
buf.append("\n");
- buf.append("lastdayOfWeek: ");
- buf.append(lastdayOfWeek);
- buf.append("\n");
- buf.append("nearestWeekday: ");
- buf.append(nearestWeekday);
+ buf.append("lastDayOfWeek: ");
+ buf.append(lastDayOfWeek);
buf.append("\n");
buf.append("NthDayOfWeek: ");
- buf.append(nthdayOfWeek);
- buf.append("\n");
- buf.append("lastdayOfMonth: ");
- buf.append(lastdayOfMonth);
+ buf.append(nthDayOfWeek);
buf.append("\n");
buf.append("years: ");
buf.append(getExpressionSetSummary(years));
@@ -968,8 +959,8 @@ public final class CronExpression implements Serializable, Cloneable {
}
protected void addToSet(int val, int end, int incr, int type)
- throws ParseException {
-
+ throws ParseException {
+
TreeSet set = getSet(type);
if (type == SECOND || type == MINUTE) {
@@ -984,7 +975,7 @@ public final class CronExpression implements Serializable, Cloneable {
"Hour values must be between 0 and 23", -1);
}
} else if (type == DAY_OF_MONTH) {
- if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
+ if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT)
&& (val != NO_SPEC_INT)) {
throw new ParseException(
"Day of month values must be between 1 and 31", -1);
@@ -1008,7 +999,7 @@ public final class CronExpression implements Serializable, Cloneable {
} else {
set.add(NO_SPEC);
}
-
+
return;
}
@@ -1064,20 +1055,20 @@ public final class CronExpression implements Serializable, Cloneable {
}
}
- // if the end of the range is before the start, then we need to overflow into
- // the next day, month etc. This is done by adding the maximum amount for that
+ // if the end of the range is before the start, then we need to overflow into
+ // the next day, month etc. This is done by adding the maximum amount for that
// type, and using modulus max to determine the value being added.
int max = -1;
if (stopAt < startAt) {
switch (type) {
- case SECOND : max = 60; break;
- case MINUTE : max = 60; break;
- case HOUR : max = 24; break;
- case MONTH : max = 12; break;
- case DAY_OF_WEEK : max = 7; break;
- case DAY_OF_MONTH : max = 31; break;
- case YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
- default : throw new IllegalArgumentException("Unexpected type encountered");
+ case SECOND : max = 60; break;
+ case MINUTE : max = 60; break;
+ case HOUR : max = 24; break;
+ case MONTH : max = 12; break;
+ case DAY_OF_WEEK : max = 7; break;
+ case DAY_OF_MONTH : max = 31; break;
+ case YEAR : throw new IllegalArgumentException("Start year must be less than stop year");
+ default : throw new IllegalArgumentException("Unexpected type encountered");
}
stopAt += max;
}
@@ -1133,7 +1124,7 @@ public final class CronExpression implements Serializable, Cloneable {
c = s.charAt(i);
}
ValueSet val = new ValueSet();
-
+
val.pos = (i < s.length()) ? i : i + 1;
val.value = Integer.parseInt(s1.toString());
return val;
@@ -1174,7 +1165,7 @@ public final class CronExpression implements Serializable, Cloneable {
public Date getTimeAfter(Date afterTime) {
// Computation is based on Gregorian year only.
- Calendar cl = new java.util.GregorianCalendar(getTimeZone());
+ Calendar cl = new java.util.GregorianCalendar(getTimeZone());
// move ahead one second, since we're computing the time *after* the
// given time
@@ -1200,7 +1191,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get second.................................................
st = seconds.tailSet(sec);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
sec = st.first();
} else {
sec = seconds.first();
@@ -1215,7 +1206,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get minute.................................................
st = minutes.tailSet(min);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = min;
min = st.first();
} else {
@@ -1236,7 +1227,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get hour...................................................
st = hours.tailSet(hr);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = hr;
hr = st.first();
} else {
@@ -1258,66 +1249,17 @@ public final class CronExpression implements Serializable, Cloneable {
// 1-based
t = -1;
int tmon = mon;
-
+
// get day...................................................
boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
- st = daysOfMonth.tailSet(day);
- if (lastdayOfMonth) {
- if(!nearestWeekday) {
- t = day;
- day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- day -= lastdayOffset;
- if(t > day) {
- mon++;
- if(mon > 12) {
- mon = 1;
- tmon = 3333; // ensure test of mon != tmon further below fails
- cl.add(Calendar.YEAR, 1);
- }
- day = 1;
- }
- } else {
- t = day;
- day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- day -= lastdayOffset;
-
- java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
- tcal.set(Calendar.SECOND, 0);
- tcal.set(Calendar.MINUTE, 0);
- tcal.set(Calendar.HOUR_OF_DAY, 0);
- tcal.set(Calendar.DAY_OF_MONTH, day);
- tcal.set(Calendar.MONTH, mon - 1);
- tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
-
- int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- int dow = tcal.get(Calendar.DAY_OF_WEEK);
-
- if(dow == Calendar.SATURDAY && day == 1) {
- day += 2;
- } else if(dow == Calendar.SATURDAY) {
- day -= 1;
- } else if(dow == Calendar.SUNDAY && day == ldom) {
- day -= 2;
- } else if(dow == Calendar.SUNDAY) {
- day += 1;
- }
-
- tcal.set(Calendar.SECOND, sec);
- tcal.set(Calendar.MINUTE, min);
- tcal.set(Calendar.HOUR_OF_DAY, hr);
- tcal.set(Calendar.DAY_OF_MONTH, day);
- tcal.set(Calendar.MONTH, mon - 1);
- Date nTime = tcal.getTime();
- if(nTime.before(afterTime)) {
- day = 1;
- mon++;
- }
- }
- } else if(nearestWeekday) {
- t = day;
- day = daysOfMonth.first();
+ Optional smallestDay = findSmallestDay(day, mon, cl.get(Calendar.YEAR), daysOfMonth);
+ Optional smallestDayForWeekday = findSmallestDay(day, mon, cl.get(Calendar.YEAR), nearestWeekdays);
+ t = day;
+ day = -1;
+ if (smallestDayForWeekday.isPresent()) {
+ day = smallestDayForWeekday.get();
java.util.Calendar tcal = java.util.Calendar.getInstance(getTimeZone());
tcal.set(Calendar.SECOND, 0);
@@ -1326,7 +1268,7 @@ public final class CronExpression implements Serializable, Cloneable {
tcal.set(Calendar.DAY_OF_MONTH, day);
tcal.set(Calendar.MONTH, mon - 1);
tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));
-
+
int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
int dow = tcal.get(Calendar.DAY_OF_WEEK);
@@ -1334,13 +1276,13 @@ public final class CronExpression implements Serializable, Cloneable {
day += 2;
} else if(dow == Calendar.SATURDAY) {
day -= 1;
- } else if(dow == Calendar.SUNDAY && day == ldom) {
+ } else if(dow == Calendar.SUNDAY && day == ldom) {
day -= 2;
- } else if(dow == Calendar.SUNDAY) {
+ } else if(dow == Calendar.SUNDAY) {
day += 1;
}
-
-
+
+
tcal.set(Calendar.SECOND, sec);
tcal.set(Calendar.MINUTE, min);
tcal.set(Calendar.HOUR_OF_DAY, hr);
@@ -1348,23 +1290,17 @@ public final class CronExpression implements Serializable, Cloneable {
tcal.set(Calendar.MONTH, mon - 1);
Date nTime = tcal.getTime();
if(nTime.before(afterTime)) {
- day = daysOfMonth.first();
- mon++;
+ day = -1;
}
- } else if (st != null && st.size() != 0) {
- t = day;
- day = st.first();
- // make sure we don't over-run a short month, such as february
- int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
- if (day > lastDay) {
- day = daysOfMonth.first();
- mon++;
+ }
+ if (smallestDay.isPresent()) {
+ if (day == -1 || smallestDay.get() < day) {
+ day = smallestDay.get();
}
- } else {
- day = daysOfMonth.first();
+ } else if (day == -1) {
+ day = 1;
mon++;
}
-
if (day != t || mon != tmon) {
cl.set(Calendar.SECOND, 0);
cl.set(Calendar.MINUTE, 0);
@@ -1376,7 +1312,7 @@ public final class CronExpression implements Serializable, Cloneable {
continue;
}
} else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
- if (lastdayOfWeek) { // are we looking for the last XXX day of
+ if (lastDayOfWeek) { // are we looking for the last XXX day of
// the month?
int dow = daysOfWeek.first(); // desired
// d-o-w
@@ -1419,7 +1355,7 @@ public final class CronExpression implements Serializable, Cloneable {
continue;
}
- } else if (nthdayOfWeek != 0) {
+ } else if (nthDayOfWeek != 0) {
// are we looking for the Nth XXX day in the month?
int dow = daysOfWeek.first(); // desired
// d-o-w
@@ -1431,10 +1367,7 @@ public final class CronExpression implements Serializable, Cloneable {
daysToAdd = dow + (7 - cDow);
}
- boolean dayShifted = false;
- if (daysToAdd > 0) {
- dayShifted = true;
- }
+ boolean dayShifted = daysToAdd > 0;
day += daysToAdd;
int weekOfMonth = day / 7;
@@ -1442,11 +1375,11 @@ public final class CronExpression implements Serializable, Cloneable {
weekOfMonth++;
}
- daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
+ daysToAdd = (nthDayOfWeek - weekOfMonth) * 7;
day += daysToAdd;
if (daysToAdd < 0
|| day > getLastDayOfMonth(mon, cl
- .get(Calendar.YEAR))) {
+ .get(Calendar.YEAR))) {
cl.set(Calendar.SECOND, 0);
cl.set(Calendar.MINUTE, 0);
cl.set(Calendar.HOUR_OF_DAY, 0);
@@ -1468,7 +1401,7 @@ public final class CronExpression implements Serializable, Cloneable {
int dow = daysOfWeek.first(); // desired
// d-o-w
st = daysOfWeek.tailSet(cDow);
- if (st != null && st.size() > 0) {
+ if (st != null && !st.isEmpty()) {
dow = st.first();
}
@@ -1491,7 +1424,7 @@ public final class CronExpression implements Serializable, Cloneable {
cl.set(Calendar.MONTH, mon);
// no '- 1' here because we are promoting the month
continue;
- } else if (daysToAdd > 0) { // are we swithing days?
+ } else if (daysToAdd > 0) { // are we switching days?
cl.set(Calendar.SECOND, 0);
cl.set(Calendar.MINUTE, 0);
cl.set(Calendar.HOUR_OF_DAY, 0);
@@ -1522,7 +1455,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get month...................................................
st = months.tailSet(mon);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = mon;
mon = st.first();
} else {
@@ -1549,7 +1482,7 @@ public final class CronExpression implements Serializable, Cloneable {
// get year...................................................
st = years.tailSet(year);
- if (st != null && st.size() != 0) {
+ if (st != null && !st.isEmpty()) {
t = year;
year = st.first();
} else {
@@ -1578,7 +1511,7 @@ public final class CronExpression implements Serializable, Cloneable {
/**
* Advance the calendar to the particular hour paying particular attention
* to daylight saving problems.
- *
+ *
* @param cal the calendar to operate on
* @param hour the hour to set
*/
@@ -1592,21 +1525,21 @@ public final class CronExpression implements Serializable, Cloneable {
/**
* NOT YET IMPLEMENTED: Returns the time before the given time
* that the CronExpression matches.
- */
- public Date getTimeBefore(Date endTime) {
+ */
+ public Date getTimeBefore(Date endTime) {
// FUTURE_TODO: implement QUARTZ-423
return null;
}
/**
- * NOT YET IMPLEMENTED: Returns the final time that the
+ * NOT YET IMPLEMENTED: Returns the final time that the
* CronExpression will match.
*/
public Date getFinalFireTime() {
// FUTURE_TODO: implement QUARTZ-423
return null;
}
-
+
protected boolean isLeapYear(int year) {
return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}
@@ -1643,18 +1576,39 @@ public final class CronExpression implements Serializable, Cloneable {
+ monthNum);
}
}
-
+
+
+ private Optional findSmallestDay(int day, int mon, int year, TreeSet set) {
+ if (set.isEmpty()) {
+ return Optional.empty();
+ }
+
+ final int lastDay = getLastDayOfMonth(mon, year);
+ // For "L", "L-1", etc.
+ int smallestDay = Optional.ofNullable(set.ceiling(LAST_DAY_OFFSET_END - (lastDay - day)))
+ .map(d -> d - LAST_DAY_OFFSET_START + 1)
+ .orElse(Integer.MAX_VALUE);
+
+ // For "1", "2", etc.
+ 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 smallestDay == Integer.MAX_VALUE ? Optional.empty() : Optional.of(smallestDay);
+ }
private void readObject(java.io.ObjectInputStream stream)
- throws java.io.IOException, ClassNotFoundException {
-
+ throws java.io.IOException, ClassNotFoundException {
+
stream.defaultReadObject();
try {
buildExpression(cronExpression);
} catch (Exception ignore) {
} // never happens
- }
-
+ }
+
@Override
@Deprecated
public Object clone() {
@@ -1666,4 +1620,4 @@ class ValueSet {
public int value;
public int pos;
-}
+}
\ No newline at end of file
diff --git a/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js b/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js
index 22393722..3922d7cd 100755
--- a/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js
+++ b/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen.js
@@ -258,24 +258,27 @@
var weekly3 = $("",{"class":"line"});
$("",{type : "radio", value : "3", name : "week"}).appendTo(weekly3);
- $(weekly3).append("周期 从星期");
+ $(weekly3).append("周期 每周第");
$("",{type : "text", id : "weekStart_0", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3);
- $(weekly3).append("-");
+ $(weekly3).append("天-第");
$("",{type : "text", id : "weekEnd_0", value : "2", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly3);
+ $(weekly3).append("天");
$(weekly3).appendTo(weeklyTab);
var weekly4 = $("",{"class":"line"});
$("",{type : "radio", value : "4", name : "week"}).appendTo(weekly4);
- $(weekly4).append("第");
+ $(weekly4).append("从第");
$("",{type : "text", id : "weekStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4);
- $(weekly4).append("周的星期");
+ $(weekly4).append("天开始,间隔");
$("",{type : "text", id : "weekEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4);
+ $(weekly4).append("天执行一次");
$(weekly4).appendTo(weeklyTab);
var weekly5 = $("",{"class":"line"});
$("",{type : "radio", value : "5", name : "week"}).appendTo(weekly5);
- $(weekly5).append("本月最后一个星期");
+ $(weekly5).append("本月最后一周的第");
$("",{type : "text", id : "weekStart_2", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly5);
+ $(weekly5).append("天");
$(weekly5).appendTo(weeklyTab);
var weekly6 = $("",{"class":"line"});
@@ -283,7 +286,7 @@
$(weekly6).append("指定");
$(weekly6).appendTo(weeklyTab);
- $(weeklyTab).append('
1234567
');
+ $(weeklyTab).append('
周日周一周二周三周四周五周六
');
$("",{type : "hidden", id : "weekHidden"}).appendTo(weeklyTab);
$(weeklyTab).appendTo(tabContent);
diff --git a/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js b/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js
index cbf84ee3..0e2335d5 100755
--- a/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js
+++ b/xxl-job-admin/src/main/resources/static/plugins/cronGen/cronGen_en.js
@@ -268,7 +268,7 @@
$("",{type : "radio", value : "4", name : "week"}).appendTo(weekly4);
$(weekly4).append("The");
$("",{type : "text", id : "weekStart_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4);
- $(weekly4).append("th week, and day ");
+ $(weekly4).append("th week, once every ");
$("",{type : "text", id : "weekEnd_1", value : "1", style:"width:35px; height:20px; text-align: center; margin: 0 3px;"}).appendTo(weekly4);
$(weekly4).appendTo(weeklyTab);
@@ -283,7 +283,7 @@
$(weekly6).append("specify");
$(weekly6).appendTo(weeklyTab);
- $(weeklyTab).append('
1234567
');
+ $(weeklyTab).append('
SUNMONTUEWEDTHUFRISAT
');
$("",{type : "hidden", id : "weekHidden"}).appendTo(weeklyTab);
$(weeklyTab).appendTo(tabContent);
diff --git a/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/CronExpressionTest.java b/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/CronExpressionTest.java
new file mode 100644
index 00000000..b23ebe0d
--- /dev/null
+++ b/xxl-job-admin/src/test/java/com/xxl/job/admin/core/util/CronExpressionTest.java
@@ -0,0 +1,23 @@
+package com.xxl.job.admin.core.util;
+
+import com.xxl.job.admin.core.cron.CronExpression;
+import com.xxl.job.core.util.DateUtil;
+import org.junit.jupiter.api.Test;
+
+import java.text.ParseException;
+import java.util.Date;
+
+public class CronExpressionTest {
+
+ @Test
+ public void shouldWriteValueAsString() throws ParseException {
+ CronExpression cronExpression = new CronExpression("0 0 0 ? * 1");
+ Date lastTriggerTime = new Date();
+ for (int i = 0; i < 5; i++) {
+ Date nextTriggerTime = cronExpression.getNextValidTimeAfter(lastTriggerTime);
+ System.out.println(DateUtil.formatDateTime(nextTriggerTime));
+
+ lastTriggerTime = nextTriggerTime;
+ }
+ }
+}