diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47b286e1e..bf4f4ca78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,6 +33,7 @@
* 【poi 】 优化ExcelBase,将alias放入
* 【poi 】 优化ExcelBase,将alias放入
* 【core 】 改进StrUtil#startWith、endWith性能
+* 【cron 】 增加CronPatternParser、MatcherTable
### 🐞Bug修复
* 【core 】 修复ObjectUtil.hasNull传入null返回true的问题(pr#555@Gitee)
diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java
index 6df40341a..ab642f7d5 100644
--- a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java
+++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java
@@ -1756,7 +1756,7 @@ public class DateUtil extends CalendarUtil {
* @return 是否闰年
*/
public static boolean isLeapYear(int year) {
- return new GregorianCalendar().isLeapYear(year);
+ return Year.isLeap(year);
}
/**
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java
index 4b57f8c8a..ce05317de 100644
--- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java
@@ -1,25 +1,10 @@
package cn.hutool.cron.pattern;
-import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.cron.CronException;
-import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher;
-import cn.hutool.cron.pattern.matcher.DayOfMonthValueMatcher;
-import cn.hutool.cron.pattern.matcher.ValueMatcher;
-import cn.hutool.cron.pattern.matcher.ValueMatcherBuilder;
-import cn.hutool.cron.pattern.parser.DayOfMonthValueParser;
-import cn.hutool.cron.pattern.parser.DayOfWeekValueParser;
-import cn.hutool.cron.pattern.parser.HourValueParser;
-import cn.hutool.cron.pattern.parser.MinuteValueParser;
-import cn.hutool.cron.pattern.parser.MonthValueParser;
-import cn.hutool.cron.pattern.parser.SecondValueParser;
-import cn.hutool.cron.pattern.parser.ValueParser;
-import cn.hutool.cron.pattern.parser.YearValueParser;
+import cn.hutool.cron.pattern.matcher.MatcherTable;
+import cn.hutool.cron.pattern.parser.CronPatternParser;
-import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
-import java.util.List;
import java.util.TimeZone;
/**
@@ -32,14 +17,14 @@ import java.util.TimeZone;
*
月 :范围:1~12,同时支持不区分大小写的别名:"jan","feb", "mar", "apr", "may","jun", "jul", "aug", "sep","oct", "nov", "dec"
* 周 :范围:0 (Sunday)~6(Saturday),7也可以表示周日,同时支持不区分大小写的别名:"sun","mon", "tue", "wed", "thu","fri", "sat","L" 表示周六
*
- *
+ *
* 为了兼容Quartz表达式,同时支持6位和7位表达式,其中:
*
*
* 当为6位时,第一位表示秒 ,范围0~59,但是第一位不做匹配
* 当为7位时,最后一位表示年 ,范围1970~2099,但是第7位不做解析,也不做匹配
*
- *
+ *
* 当定时任务运行到的时间匹配这些表达式后,任务被启动。
* 注意:
*
@@ -47,7 +32,7 @@ import java.util.TimeZone;
* 当isMatchSecond为{@code true}时才会匹配秒部分
* 默认都是关闭的
*
- *
+ *
* 对于每一个子表达式,同样支持以下形式:
*
* - * :表示匹配这个位置所有的时间
@@ -62,10 +47,10 @@ import java.util.TimeZone;
*
* 间隔(/) > 区间(-) > 列表(,)
*
- *
+ *
* 例如 2,3,6/3中,由于“/”优先级高,因此相当于2,3,(6/3),结果与 2,3,6等价
*
- *
+ *
* 一些例子:
*
* - 5 * * * * :每个点钟的5分执行,00:05,01:05……
@@ -77,36 +62,11 @@ import java.util.TimeZone;
*
*
* @author Looly
- *
*/
public class CronPattern {
- private static final ValueParser SECOND_VALUE_PARSER = new SecondValueParser();
- private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser();
- private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser();
- private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser();
- private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser();
- private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser();
- private static final ValueParser YEAR_VALUE_PARSER = new YearValueParser();
-
private final String pattern;
-
- /** 秒字段匹配列表 */
- private final List secondMatchers = new ArrayList<>();
- /** 分字段匹配列表 */
- private final List minuteMatchers = new ArrayList<>();
- /** 时字段匹配列表 */
- private final List hourMatchers = new ArrayList<>();
- /** 每月几号字段匹配列表 */
- private final List dayOfMonthMatchers = new ArrayList<>();
- /** 月字段匹配列表 */
- private final List monthMatchers = new ArrayList<>();
- /** 星期字段匹配列表 */
- private final List dayOfWeekMatchers = new ArrayList<>();
- /** 年字段匹配列表 */
- private final List yearMatchers = new ArrayList<>();
- /** 匹配器个数,取决于复合任务表达式中的单一表达式个数 */
- private int matcherSize;
+ private final MatcherTable matcherTable;
/**
* 构造
@@ -115,14 +75,15 @@ public class CronPattern {
*/
public CronPattern(String pattern) {
this.pattern = pattern;
- parseGroupPattern(pattern);
+ this.matcherTable = CronPatternParser.create().parse(pattern);
}
// --------------------------------------------------------------------------------------- match start
+
/**
* 给定时间是否匹配定时任务表达式
*
- * @param millis 时间毫秒数
+ * @param millis 时间毫秒数
* @param isMatchSecond 是否匹配秒
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
*/
@@ -133,8 +94,8 @@ public class CronPattern {
/**
* 给定时间是否匹配定时任务表达式
*
- * @param timezone 时区 {@link TimeZone}
- * @param millis 时间毫秒数
+ * @param timezone 时区 {@link TimeZone}
+ * @param millis 时间毫秒数
* @param isMatchSecond 是否匹配秒
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
*/
@@ -147,11 +108,12 @@ public class CronPattern {
/**
* 给定时间是否匹配定时任务表达式
*
- * @param calendar 时间
+ * @param calendar 时间
* @param isMatchSecond 是否匹配秒
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
*/
public boolean match(GregorianCalendar calendar, boolean isMatchSecond) {
+ final int second = isMatchSecond ? calendar.get(Calendar.SECOND) : -1;
final int minute = calendar.get(Calendar.MINUTE);
final int hour = calendar.get(Calendar.HOUR_OF_DAY);
final int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
@@ -159,20 +121,7 @@ public class CronPattern {
final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始,0和7都表示周日
final int year = calendar.get(Calendar.YEAR);
- boolean eval;
- for (int i = 0; i < matcherSize; i++) {
- eval = ((false == isMatchSecond) || secondMatchers.get(i).match(calendar.get(Calendar.SECOND))) // 匹配秒(非秒匹配模式下始终返回true)
- && minuteMatchers.get(i).match(minute)// 匹配分
- && hourMatchers.get(i).match(hour)// 匹配时
- && isMatchDayOfMonth(dayOfMonthMatchers.get(i), dayOfMonth, month, calendar.isLeapYear(year))// 匹配日
- && monthMatchers.get(i).match(month) // 匹配月
- && dayOfWeekMatchers.get(i).match(dayOfWeek)// 匹配周
- && isMatch(yearMatchers, i, year);// 匹配年
- if (eval) {
- return true;
- }
- }
- return false;
+ return this.matcherTable.match(second, minute, hour, dayOfMonth, month, dayOfWeek, year);
}
// --------------------------------------------------------------------------------------- match end
@@ -180,114 +129,4 @@ public class CronPattern {
public String toString() {
return this.pattern;
}
-
- // -------------------------------------------------------------------------------------- Private method start
- /**
- * 是否匹配日(指定月份的第几天)
- *
- * @param matcher {@link ValueMatcher}
- * @param dayOfMonth 日
- * @param month 月
- * @param isLeapYear 是否闰年
- * @return 是否匹配
- */
- private static boolean isMatchDayOfMonth(ValueMatcher matcher, int dayOfMonth, int month, boolean isLeapYear) {
- return ((matcher instanceof DayOfMonthValueMatcher) //
- ? ((DayOfMonthValueMatcher) matcher).match(dayOfMonth, month, isLeapYear) //
- : matcher.match(dayOfMonth));
- }
-
- /**
- * 是否匹配指定的日期时间位置
- *
- * @param matchers 匹配器列表
- * @param index 位置
- * @param value 被匹配的值
- * @return 是否匹配
- * @since 4.0.2
- */
- private static boolean isMatch(List matchers, int index, int value) {
- return (matchers.size() <= index) || matchers.get(index).match(value);
- }
-
- /**
- * 解析复合任务表达式
- *
- * @param groupPattern 复合表达式
- */
- private void parseGroupPattern(String groupPattern) {
- List patternList = StrUtil.split(groupPattern, '|');
- for (String pattern : patternList) {
- parseSinglePattern(pattern);
- }
- }
-
- /**
- * 解析单一定时任务表达式
- *
- * @param pattern 表达式
- */
- private void parseSinglePattern(String pattern) {
- final String[] parts = pattern.split("\\s");
-
- int offset = 0;// 偏移量用于兼容Quartz表达式,当表达式有6或7项时,第一项为秒
- if (parts.length == 6 || parts.length == 7) {
- offset = 1;
- } else if (parts.length != 5) {
- throw new CronException("Pattern [{}] is invalid, it must be 5-7 parts!", pattern);
- }
-
- // 秒
- if (1 == offset) {// 支持秒的表达式
- try {
- this.secondMatchers.add(ValueMatcherBuilder.build(parts[0], SECOND_VALUE_PARSER));
- } catch (Exception e) {
- throw new CronException(e, "Invalid pattern [{}], parsing 'second' field error!", pattern);
- }
- } else {// 不支持秒的表达式,则第一位按照表达式生成时间的秒数赋值,表示整分匹配
- this.secondMatchers.add(ValueMatcherBuilder.build(String.valueOf(DateUtil.date().second()), SECOND_VALUE_PARSER));
- }
- // 分
- try {
- this.minuteMatchers.add(ValueMatcherBuilder.build(parts[offset], MINUTE_VALUE_PARSER));
- } catch (Exception e) {
- throw new CronException(e, "Invalid pattern [{}], parsing 'minute' field error!", pattern);
- }
- // 小时
- try {
- this.hourMatchers.add(ValueMatcherBuilder.build(parts[1 + offset], HOUR_VALUE_PARSER));
- } catch (Exception e) {
- throw new CronException(e, "Invalid pattern [{}], parsing 'hour' field error!", pattern);
- }
- // 每月第几天
- try {
- this.dayOfMonthMatchers.add(ValueMatcherBuilder.build(parts[2 + offset], DAY_OF_MONTH_VALUE_PARSER));
- } catch (Exception e) {
- throw new CronException(e, "Invalid pattern [{}], parsing 'day of month' field error!", pattern);
- }
- // 月
- try {
- this.monthMatchers.add(ValueMatcherBuilder.build(parts[3 + offset], MONTH_VALUE_PARSER));
- } catch (Exception e) {
- throw new CronException(e, "Invalid pattern [{}], parsing 'month' field error!", pattern);
- }
- // 星期几
- try {
- this.dayOfWeekMatchers.add(ValueMatcherBuilder.build(parts[4 + offset], DAY_OF_WEEK_VALUE_PARSER));
- } catch (Exception e) {
- throw new CronException(e, "Invalid pattern [{}], parsing 'day of week' field error!", pattern);
- }
- // 年
- if (parts.length == 7) {// 支持年的表达式
- try {
- this.yearMatchers.add(ValueMatcherBuilder.build(parts[6], YEAR_VALUE_PARSER));
- } catch (Exception e) {
- throw new CronException(e, "Invalid pattern [{}], parsing 'year' field error!", pattern);
- }
- } else {// 不支持年的表达式,全部匹配
- this.yearMatchers.add(new AlwaysTrueValueMatcher());
- }
- matcherSize++;
- }
- // -------------------------------------------------------------------------------------- Private method end
}
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/MatcherTable.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/MatcherTable.java
new file mode 100644
index 000000000..9dc20ff02
--- /dev/null
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/MatcherTable.java
@@ -0,0 +1,119 @@
+package cn.hutool.cron.pattern.matcher;
+
+import java.time.Year;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 时间匹配表,用于存放定时任务表达式解析后的结构信息
+ *
+ * @author looly
+ * @since 5.8.0
+ */
+public class MatcherTable {
+
+ /**
+ * 匹配器个数,取决于复合任务表达式中的单一表达式个数
+ */
+ public int matcherSize;
+ /**
+ * 秒字段匹配列表
+ */
+ public final List secondMatchers;
+ /**
+ * 分字段匹配列表
+ */
+ public final List minuteMatchers;
+ /**
+ * 时字段匹配列表
+ */
+ public final List hourMatchers;
+ /**
+ * 每月几号字段匹配列表
+ */
+ public final List dayOfMonthMatchers;
+ /**
+ * 月字段匹配列表
+ */
+ public final List monthMatchers;
+ /**
+ * 星期字段匹配列表
+ */
+ public final List dayOfWeekMatchers;
+ /**
+ * 年字段匹配列表
+ */
+ public final List yearMatchers;
+
+ /**
+ * 构造
+ *
+ * @param size 表达式个数,用于表示复合表达式中单个表达式个数
+ */
+ public MatcherTable(int size) {
+ matcherSize = size;
+ secondMatchers = new ArrayList<>(size);
+ minuteMatchers = new ArrayList<>(size);
+ hourMatchers = new ArrayList<>(size);
+ dayOfMonthMatchers = new ArrayList<>(size);
+ monthMatchers = new ArrayList<>(size);
+ dayOfWeekMatchers = new ArrayList<>(size);
+ yearMatchers = new ArrayList<>(size);
+ }
+
+ /**
+ * 给定时间是否匹配定时任务表达式
+ *
+ * @param second 秒数,-1表示不匹配此项
+ * @param minute 分钟
+ * @param hour 小时
+ * @param dayOfMonth 天
+ * @param month 月
+ * @param dayOfWeek 周几
+ * @param year 年
+ * @return 如果匹配返回 {@code true}, 否则返回 {@code false}
+ */
+ public boolean match(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) {
+ for (int i = 0; i < matcherSize; i++) {
+ boolean eval = ((second < 0) || secondMatchers.get(i).match(second)) // 匹配秒(非秒匹配模式下始终返回true)
+ && minuteMatchers.get(i).match(minute)// 匹配分
+ && hourMatchers.get(i).match(hour)// 匹配时
+ && isMatchDayOfMonth(dayOfMonthMatchers.get(i), dayOfMonth, month, Year.isLeap(year))// 匹配日
+ && monthMatchers.get(i).match(month) // 匹配月
+ && dayOfWeekMatchers.get(i).match(dayOfWeek)// 匹配周
+ && isMatch(yearMatchers, i, year);// 匹配年
+ if (eval) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 是否匹配日(指定月份的第几天)
+ *
+ * @param matcher {@link ValueMatcher}
+ * @param dayOfMonth 日
+ * @param month 月
+ * @param isLeapYear 是否闰年
+ * @return 是否匹配
+ */
+ private static boolean isMatchDayOfMonth(ValueMatcher matcher, int dayOfMonth, int month, boolean isLeapYear) {
+ return ((matcher instanceof DayOfMonthValueMatcher) //
+ ? ((DayOfMonthValueMatcher) matcher).match(dayOfMonth, month, isLeapYear) //
+ : matcher.match(dayOfMonth));
+ }
+
+ /**
+ * 是否匹配指定的日期时间位置
+ *
+ * @param matchers 匹配器列表
+ * @param index 位置
+ * @param value 被匹配的值
+ * @return 是否匹配
+ * @since 4.0.2
+ */
+ private static boolean isMatch(List matchers, int index, int value) {
+ return (matchers.size() <= index) || matchers.get(index).match(value);
+ }
+}
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/CronPatternParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/CronPatternParser.java
new file mode 100644
index 000000000..e85e20dee
--- /dev/null
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/CronPatternParser.java
@@ -0,0 +1,132 @@
+package cn.hutool.cron.pattern.parser;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.cron.CronException;
+import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher;
+import cn.hutool.cron.pattern.matcher.MatcherTable;
+import cn.hutool.cron.pattern.matcher.ValueMatcherBuilder;
+
+import java.util.List;
+
+/**
+ * 定时任务表达式解析器,用于将表达式字符串解析为{@link MatcherTable}
+ *
+ * @author looly
+ * @since 5.8.0
+ */
+public class CronPatternParser {
+
+ private static final ValueParser SECOND_VALUE_PARSER = new SecondValueParser();
+ private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser();
+ private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser();
+ private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser();
+ private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser();
+ private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser();
+ private static final ValueParser YEAR_VALUE_PARSER = new YearValueParser();
+
+ /**
+ * 创建表达式解析器
+ *
+ * @return CronPatternParser
+ */
+ public static CronPatternParser create() {
+ return new CronPatternParser();
+ }
+
+ private MatcherTable matcherTable;
+
+ /**
+ * 解析表达式到匹配表中
+ *
+ * @param cronPattern 复合表达式
+ * @return {@link MatcherTable}
+ */
+ public MatcherTable parse(String cronPattern) {
+ parseGroupPattern(cronPattern);
+ return this.matcherTable;
+ }
+
+ /**
+ * 解析复合任务表达式,格式为:
+ *
+ * cronA | cronB | ...
+ *
+ *
+ * @param groupPattern 复合表达式
+ */
+ private void parseGroupPattern(String groupPattern) {
+ final List patternList = StrUtil.split(groupPattern, '|');
+ matcherTable = new MatcherTable(patternList.size());
+ for (String pattern : patternList) {
+ parseSinglePattern(pattern);
+ }
+ }
+
+ /**
+ * 解析单一定时任务表达式
+ *
+ * @param pattern 表达式
+ */
+ private void parseSinglePattern(String pattern) {
+ final String[] parts = pattern.split("\\s");
+
+ int offset = 0;// 偏移量用于兼容Quartz表达式,当表达式有6或7项时,第一项为秒
+ if (parts.length == 6 || parts.length == 7) {
+ offset = 1;
+ } else if (parts.length != 5) {
+ throw new CronException("Pattern [{}] is invalid, it must be 5-7 parts!", pattern);
+ }
+
+ // 秒
+ if (1 == offset) {// 支持秒的表达式
+ try {
+ matcherTable.secondMatchers.add(ValueMatcherBuilder.build(parts[0], SECOND_VALUE_PARSER));
+ } catch (Exception e) {
+ throw new CronException(e, "Invalid pattern [{}], parsing 'second' field error!", pattern);
+ }
+ } else {// 不支持秒的表达式,则第一位按照表达式生成时间的秒数赋值,表示整分匹配
+ matcherTable.secondMatchers.add(ValueMatcherBuilder.build(String.valueOf(DateUtil.date().second()), SECOND_VALUE_PARSER));
+ }
+ // 分
+ try {
+ matcherTable.minuteMatchers.add(ValueMatcherBuilder.build(parts[offset], MINUTE_VALUE_PARSER));
+ } catch (Exception e) {
+ throw new CronException(e, "Invalid pattern [{}], parsing 'minute' field error!", pattern);
+ }
+ // 小时
+ try {
+ matcherTable.hourMatchers.add(ValueMatcherBuilder.build(parts[1 + offset], HOUR_VALUE_PARSER));
+ } catch (Exception e) {
+ throw new CronException(e, "Invalid pattern [{}], parsing 'hour' field error!", pattern);
+ }
+ // 每月第几天
+ try {
+ matcherTable.dayOfMonthMatchers.add(ValueMatcherBuilder.build(parts[2 + offset], DAY_OF_MONTH_VALUE_PARSER));
+ } catch (Exception e) {
+ throw new CronException(e, "Invalid pattern [{}], parsing 'day of month' field error!", pattern);
+ }
+ // 月
+ try {
+ matcherTable.monthMatchers.add(ValueMatcherBuilder.build(parts[3 + offset], MONTH_VALUE_PARSER));
+ } catch (Exception e) {
+ throw new CronException(e, "Invalid pattern [{}], parsing 'month' field error!", pattern);
+ }
+ // 星期几
+ try {
+ matcherTable.dayOfWeekMatchers.add(ValueMatcherBuilder.build(parts[4 + offset], DAY_OF_WEEK_VALUE_PARSER));
+ } catch (Exception e) {
+ throw new CronException(e, "Invalid pattern [{}], parsing 'day of week' field error!", pattern);
+ }
+ // 年
+ if (parts.length == 7) {// 支持年的表达式
+ try {
+ matcherTable.yearMatchers.add(ValueMatcherBuilder.build(parts[6], YEAR_VALUE_PARSER));
+ } catch (Exception e) {
+ throw new CronException(e, "Invalid pattern [{}], parsing 'year' field error!", pattern);
+ }
+ } else {// 不支持年的表达式,全部匹配
+ matcherTable.yearMatchers.add(new AlwaysTrueValueMatcher());
+ }
+ }
+}
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java
index ab870a8d1..3a85d72e0 100644
--- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/MonthValueParser.java
@@ -4,7 +4,7 @@ import cn.hutool.cron.CronException;
/**
* 月份值处理
- * 限定于1-12,1表示一月,支持别名,如一月是{@code jan}
+ * 限定于1-12,1表示一月,支持别名(忽略大小写),如一月是{@code jan}
*
* @author Looly
*/