mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-05-09 23:51:34 +08:00
fix code
This commit is contained in:
parent
c2e1bbafc8
commit
5966dcc626
@ -1,10 +1,13 @@
|
|||||||
package cn.hutool.cron.pattern;
|
package cn.hutool.cron.pattern;
|
||||||
|
|
||||||
import cn.hutool.cron.pattern.matcher.MatcherTable;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.cron.pattern.matcher.PatternMatcher;
|
||||||
import cn.hutool.cron.pattern.parser.PatternParser;
|
import cn.hutool.cron.pattern.parser.PatternParser;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.List;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -66,7 +69,7 @@ import java.util.TimeZone;
|
|||||||
public class CronPattern {
|
public class CronPattern {
|
||||||
|
|
||||||
private final String pattern;
|
private final String pattern;
|
||||||
private final MatcherTable matcherTable;
|
private final List<PatternMatcher> matchers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析表达式为 CronPattern
|
* 解析表达式为 CronPattern
|
||||||
@ -86,11 +89,9 @@ public class CronPattern {
|
|||||||
*/
|
*/
|
||||||
public CronPattern(String pattern) {
|
public CronPattern(String pattern) {
|
||||||
this.pattern = pattern;
|
this.pattern = pattern;
|
||||||
this.matcherTable = PatternParser.parse(pattern);
|
this.matchers = PatternParser.parse(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------------------- match start
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 给定时间是否匹配定时任务表达式
|
* 给定时间是否匹配定时任务表达式
|
||||||
*
|
*
|
||||||
@ -116,24 +117,6 @@ public class CronPattern {
|
|||||||
return match(calendar, isMatchSecond);
|
return match(calendar, isMatchSecond);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 返回匹配到的下一个时间
|
|
||||||
*
|
|
||||||
* @param calendar 时间
|
|
||||||
* @return 匹配到的下一个时间
|
|
||||||
*/
|
|
||||||
public Calendar nextMatchAfter(Calendar calendar) {
|
|
||||||
final int second = calendar.get(Calendar.SECOND);
|
|
||||||
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);
|
|
||||||
final int month = calendar.get(Calendar.MONTH) + 1;// 月份从1开始
|
|
||||||
final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始,0和7都表示周日
|
|
||||||
final int year = calendar.get(Calendar.YEAR);
|
|
||||||
|
|
||||||
return this.matcherTable.nextMatchAfter(second, minute, hour, dayOfMonth, month, dayOfWeek, year, calendar.getTimeZone());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 给定时间是否匹配定时任务表达式
|
* 给定时间是否匹配定时任务表达式
|
||||||
*
|
*
|
||||||
@ -150,12 +133,73 @@ public class CronPattern {
|
|||||||
final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始,0和7都表示周日
|
final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始,0和7都表示周日
|
||||||
final int year = calendar.get(Calendar.YEAR);
|
final int year = calendar.get(Calendar.YEAR);
|
||||||
|
|
||||||
return this.matcherTable.match(second, minute, hour, dayOfMonth, month, dayOfWeek, year);
|
return match(second, minute, hour, dayOfMonth, month, dayOfWeek, year);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回匹配到的下一个时间
|
||||||
|
*
|
||||||
|
* @param calendar 时间
|
||||||
|
* @return 匹配到的下一个时间
|
||||||
|
*/
|
||||||
|
public Calendar nextMatchAfter(Calendar calendar) {
|
||||||
|
final int second = calendar.get(Calendar.SECOND);
|
||||||
|
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);
|
||||||
|
final int month = calendar.get(Calendar.MONTH) + 1;// 月份从1开始
|
||||||
|
final int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1; // 星期从0开始,0和7都表示周日
|
||||||
|
final int year = calendar.get(Calendar.YEAR);
|
||||||
|
|
||||||
|
return nextMatchAfter(second, minute, hour, dayOfMonth, month, dayOfWeek, year, calendar.getTimeZone());
|
||||||
}
|
}
|
||||||
// --------------------------------------------------------------------------------------- match end
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return this.pattern;
|
return this.pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给定时间是否匹配定时任务表达式
|
||||||
|
*
|
||||||
|
* @param second 秒数,-1表示不匹配此项
|
||||||
|
* @param minute 分钟
|
||||||
|
* @param hour 小时
|
||||||
|
* @param dayOfMonth 天
|
||||||
|
* @param month 月,从1开始
|
||||||
|
* @param dayOfWeek 周,从0开始,0和7都表示周日
|
||||||
|
* @param year 年
|
||||||
|
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
|
||||||
|
*/
|
||||||
|
private boolean match(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) {
|
||||||
|
for (PatternMatcher matcher : matchers) {
|
||||||
|
if (matcher.match(second, minute, hour, dayOfMonth, month, dayOfWeek, year)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一个最近的匹配日期时间
|
||||||
|
*
|
||||||
|
* @param second 秒
|
||||||
|
* @param minute 分
|
||||||
|
* @param hour 时
|
||||||
|
* @param dayOfMonth 天
|
||||||
|
* @param month 月(从1开始)
|
||||||
|
* @param dayOfWeek 周(从0开始, 0表示周日)
|
||||||
|
* @param year 年
|
||||||
|
* @param zone 时区
|
||||||
|
* @return {@link Calendar}
|
||||||
|
*/
|
||||||
|
private Calendar nextMatchAfter(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year, TimeZone zone) {
|
||||||
|
List<Calendar> nextMatchs = new ArrayList<>(second);
|
||||||
|
for (PatternMatcher matcher : matchers) {
|
||||||
|
nextMatchs.add(matcher.nextMatchAfter(
|
||||||
|
second, minute, hour, dayOfMonth, month, dayOfWeek, year, zone));
|
||||||
|
}
|
||||||
|
// 返回匹配到的最早日期
|
||||||
|
return CollUtil.min(nextMatchs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@ import cn.hutool.core.util.StrUtil;
|
|||||||
*
|
*
|
||||||
* @author Looly
|
* @author Looly
|
||||||
*/
|
*/
|
||||||
public class AlwaysTrueValueMatcher implements ValueMatcher {
|
public class AlwaysTrueMatcher implements PartMatcher {
|
||||||
|
|
||||||
public static AlwaysTrueValueMatcher INSTANCE = new AlwaysTrueValueMatcher();
|
public static AlwaysTrueMatcher INSTANCE = new AlwaysTrueMatcher();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean match(Integer t) {
|
public boolean match(Integer t) {
|
@ -12,7 +12,7 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* @author Looly
|
* @author Looly
|
||||||
*/
|
*/
|
||||||
public class BoolArrayValueMatcher implements ValueMatcher {
|
public class BoolArrayMatcher implements PartMatcher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户定义此字段的最小值
|
* 用户定义此字段的最小值
|
||||||
@ -25,7 +25,7 @@ public class BoolArrayValueMatcher implements ValueMatcher {
|
|||||||
*
|
*
|
||||||
* @param intValueList 匹配值列表
|
* @param intValueList 匹配值列表
|
||||||
*/
|
*/
|
||||||
public BoolArrayValueMatcher(List<Integer> intValueList) {
|
public BoolArrayMatcher(List<Integer> intValueList) {
|
||||||
Assert.isTrue(CollUtil.isNotEmpty(intValueList), "Values must be not empty!");
|
Assert.isTrue(CollUtil.isNotEmpty(intValueList), "Values must be not empty!");
|
||||||
bValues = new boolean[Collections.max(intValueList) + 1];
|
bValues = new boolean[Collections.max(intValueList) + 1];
|
||||||
int min = Integer.MAX_VALUE;
|
int min = Integer.MAX_VALUE;
|
@ -1,95 +0,0 @@
|
|||||||
package cn.hutool.cron.pattern.matcher;
|
|
||||||
|
|
||||||
import java.time.Year;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 日期和时间的单一匹配器
|
|
||||||
*
|
|
||||||
* @author looly
|
|
||||||
* @since 5.8.0
|
|
||||||
*/
|
|
||||||
public class DateTimeMatcher {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 秒字段匹配列表
|
|
||||||
*/
|
|
||||||
final ValueMatcher secondMatcher;
|
|
||||||
/**
|
|
||||||
* 分字段匹配列表
|
|
||||||
*/
|
|
||||||
final ValueMatcher minuteMatcher;
|
|
||||||
/**
|
|
||||||
* 时字段匹配列表
|
|
||||||
*/
|
|
||||||
final ValueMatcher hourMatcher;
|
|
||||||
/**
|
|
||||||
* 每月几号字段匹配列表
|
|
||||||
*/
|
|
||||||
final ValueMatcher dayOfMonthMatcher;
|
|
||||||
/**
|
|
||||||
* 月字段匹配列表
|
|
||||||
*/
|
|
||||||
final ValueMatcher monthMatcher;
|
|
||||||
/**
|
|
||||||
* 星期字段匹配列表
|
|
||||||
*/
|
|
||||||
final ValueMatcher dayOfWeekMatcher;
|
|
||||||
/**
|
|
||||||
* 年字段匹配列表
|
|
||||||
*/
|
|
||||||
final ValueMatcher yearMatcher;
|
|
||||||
|
|
||||||
public DateTimeMatcher(ValueMatcher secondMatcher,
|
|
||||||
ValueMatcher minuteMatcher,
|
|
||||||
ValueMatcher hourMatchers,
|
|
||||||
ValueMatcher dayOfMonthMatchers,
|
|
||||||
ValueMatcher monthMatchers,
|
|
||||||
ValueMatcher dayOfWeekMatchers,
|
|
||||||
ValueMatcher yearMatchers) {
|
|
||||||
|
|
||||||
this.secondMatcher = secondMatcher;
|
|
||||||
this.minuteMatcher = minuteMatcher;
|
|
||||||
this.hourMatcher = hourMatchers;
|
|
||||||
this.dayOfMonthMatcher = dayOfMonthMatchers;
|
|
||||||
this.monthMatcher = monthMatchers;
|
|
||||||
this.dayOfWeekMatcher = dayOfWeekMatchers;
|
|
||||||
this.yearMatcher = yearMatchers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 给定时间是否匹配定时任务表达式
|
|
||||||
*
|
|
||||||
* @param second 秒数,-1表示不匹配此项
|
|
||||||
* @param minute 分钟
|
|
||||||
* @param hour 小时
|
|
||||||
* @param dayOfMonth 天
|
|
||||||
* @param month 月,从1开始
|
|
||||||
* @param dayOfWeek 周,从0开始,0和7都表示周日
|
|
||||||
* @param year 年
|
|
||||||
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
|
|
||||||
*/
|
|
||||||
public boolean match(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) {
|
|
||||||
return ((second < 0) || secondMatcher.match(second)) // 匹配秒(非秒匹配模式下始终返回true)
|
|
||||||
&& minuteMatcher.match(minute)// 匹配分
|
|
||||||
&& hourMatcher.match(hour)// 匹配时
|
|
||||||
&& isMatchDayOfMonth(dayOfMonthMatcher, dayOfMonth, month, Year.isLeap(year))// 匹配日
|
|
||||||
&& monthMatcher.match(month) // 匹配月
|
|
||||||
&& dayOfWeekMatcher.match(dayOfWeek)// 匹配周
|
|
||||||
&& yearMatcher.match(year);// 匹配年
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否匹配日(指定月份的第几天)
|
|
||||||
*
|
|
||||||
* @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));
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,14 +10,14 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* @author Looly
|
* @author Looly
|
||||||
*/
|
*/
|
||||||
public class DayOfMonthValueMatcher extends BoolArrayValueMatcher {
|
public class DayOfMonthMatcher extends BoolArrayMatcher {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造
|
* 构造
|
||||||
*
|
*
|
||||||
* @param intValueList 匹配的日值
|
* @param intValueList 匹配的日值
|
||||||
*/
|
*/
|
||||||
public DayOfMonthValueMatcher(List<Integer> intValueList) {
|
public DayOfMonthMatcher(List<Integer> intValueList) {
|
||||||
super(intValueList);
|
super(intValueList);
|
||||||
}
|
}
|
||||||
|
|
@ -1,157 +0,0 @@
|
|||||||
package cn.hutool.cron.pattern.matcher;
|
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
|
||||||
import cn.hutool.core.lang.Console;
|
|
||||||
import cn.hutool.core.lang.mutable.MutableBool;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 时间匹配表,用于存放定时任务表达式解析后的结构信息
|
|
||||||
*
|
|
||||||
* @author looly
|
|
||||||
* @since 5.8.0
|
|
||||||
*/
|
|
||||||
public class MatcherTable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 秒字段匹配列表
|
|
||||||
*/
|
|
||||||
public final List<DateTimeMatcher> matchers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造
|
|
||||||
*
|
|
||||||
* @param size 表达式个数,用于表示复合表达式中单个表达式个数
|
|
||||||
*/
|
|
||||||
public MatcherTable(int size) {
|
|
||||||
matchers = new ArrayList<>(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取下一个最近的匹配日期时间
|
|
||||||
*
|
|
||||||
* @param second 秒
|
|
||||||
* @param minute 分
|
|
||||||
* @param hour 时
|
|
||||||
* @param dayOfMonth 天
|
|
||||||
* @param month 月(从0开始)
|
|
||||||
* @param dayOfWeek 周
|
|
||||||
* @param year 年
|
|
||||||
* @param zone 时区
|
|
||||||
* @return {@link Calendar}
|
|
||||||
*/
|
|
||||||
public Calendar nextMatchAfter(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year, TimeZone zone) {
|
|
||||||
List<Calendar> nextMatchs = new ArrayList<>(second);
|
|
||||||
for (DateTimeMatcher matcher : matchers) {
|
|
||||||
nextMatchs.add(singleNextMatchAfter(matcher, second, minute, hour,
|
|
||||||
dayOfMonth, month, dayOfWeek, year, zone));
|
|
||||||
}
|
|
||||||
// 返回匹配到的最早日期
|
|
||||||
return CollUtil.min(nextMatchs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取下一个匹配日期时间
|
|
||||||
*
|
|
||||||
* @param matcher 匹配器
|
|
||||||
* @param second 秒
|
|
||||||
* @param minute 分
|
|
||||||
* @param hour 时
|
|
||||||
* @param dayOfMonth 天
|
|
||||||
* @param month 月(从0开始)
|
|
||||||
* @param dayOfWeek 周
|
|
||||||
* @param year 年
|
|
||||||
* @param zone 时区
|
|
||||||
* @return {@link Calendar}
|
|
||||||
*/
|
|
||||||
private static Calendar singleNextMatchAfter(DateTimeMatcher matcher, int second, int minute, int hour,
|
|
||||||
int dayOfMonth, int month, int dayOfWeek, int year, TimeZone zone) {
|
|
||||||
|
|
||||||
Calendar calendar = Calendar.getInstance(zone);
|
|
||||||
|
|
||||||
// 上一个字段不一致,说明产生了新值,下一个字段使用最小值
|
|
||||||
MutableBool isNextEquals = new MutableBool(true);
|
|
||||||
// 年
|
|
||||||
final int nextYear = nextAfter(matcher.yearMatcher, year, isNextEquals);
|
|
||||||
calendar.set(Calendar.YEAR, nextYear);
|
|
||||||
|
|
||||||
// 周
|
|
||||||
final int nextDayOfWeek = nextAfter(matcher.dayOfWeekMatcher, dayOfWeek, isNextEquals);
|
|
||||||
calendar.set(Calendar.DAY_OF_WEEK, nextDayOfWeek);
|
|
||||||
|
|
||||||
// 月
|
|
||||||
final int nextMonth = nextAfter(matcher.monthMatcher, month + 1, isNextEquals);
|
|
||||||
calendar.set(Calendar.MONTH, nextMonth - 1);
|
|
||||||
|
|
||||||
// 日
|
|
||||||
final int nextDayOfMonth = nextAfter(matcher.dayOfMonthMatcher, dayOfMonth, isNextEquals);
|
|
||||||
calendar.set(Calendar.DAY_OF_MONTH, nextDayOfMonth);
|
|
||||||
|
|
||||||
// 时
|
|
||||||
final int nextHour = nextAfter(matcher.hourMatcher, hour, isNextEquals);
|
|
||||||
calendar.set(Calendar.HOUR, nextHour);
|
|
||||||
|
|
||||||
// 分
|
|
||||||
int nextMinute = nextAfter(matcher.minuteMatcher, minute, isNextEquals);
|
|
||||||
calendar.set(Calendar.MINUTE, nextMinute);
|
|
||||||
|
|
||||||
// 秒
|
|
||||||
final int nextSecond = nextAfter(matcher.secondMatcher, second, isNextEquals);
|
|
||||||
calendar.set(Calendar.SECOND, nextSecond);
|
|
||||||
|
|
||||||
Console.log(nextYear, nextDayOfWeek, nextMonth, nextDayOfMonth, nextHour, nextMinute, nextSecond);
|
|
||||||
return calendar;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取对应字段匹配器的下一个值
|
|
||||||
*
|
|
||||||
* @param matcher 匹配器
|
|
||||||
* @param value 值
|
|
||||||
* @param isNextEquals 是否下一个值和值相同。不同获取初始值,相同获取下一值,然后修改。
|
|
||||||
* @return 下一个值,-1标识匹配所有值的情况,应获取整个字段的最小值
|
|
||||||
*/
|
|
||||||
private static int nextAfter(ValueMatcher matcher, int value, MutableBool isNextEquals) {
|
|
||||||
int nextValue;
|
|
||||||
if (isNextEquals.get()) {
|
|
||||||
// 上一层级得到相同值,下级获取下个值
|
|
||||||
nextValue = matcher.nextAfter(value);
|
|
||||||
isNextEquals.set(nextValue == value);
|
|
||||||
} else {
|
|
||||||
// 上一层级的值得到了不同值,下级的所有值使用最小值
|
|
||||||
if (matcher instanceof AlwaysTrueValueMatcher) {
|
|
||||||
nextValue = value;
|
|
||||||
} else if (matcher instanceof BoolArrayValueMatcher) {
|
|
||||||
nextValue = ((BoolArrayValueMatcher) matcher).getMinValue();
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("Invalid matcher: " + matcher.getClass().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nextValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 给定时间是否匹配定时任务表达式
|
|
||||||
*
|
|
||||||
* @param second 秒数,-1表示不匹配此项
|
|
||||||
* @param minute 分钟
|
|
||||||
* @param hour 小时
|
|
||||||
* @param dayOfMonth 天
|
|
||||||
* @param month 月,从1开始
|
|
||||||
* @param dayOfWeek 周,从0开始,0和7都表示周日
|
|
||||||
* @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 (DateTimeMatcher matcher : matchers) {
|
|
||||||
if (matcher.match(second, minute, hour, dayOfMonth, month, dayOfWeek, year)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,12 +3,12 @@ package cn.hutool.cron.pattern.matcher;
|
|||||||
import cn.hutool.core.lang.Matcher;
|
import cn.hutool.core.lang.Matcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 值匹配器<br>
|
* 表达式中的某个位置部分匹配器<br>
|
||||||
* 用于匹配日期位中对应数字是否匹配
|
* 用于匹配日期位中对应数字是否匹配
|
||||||
*
|
*
|
||||||
* @author Looly
|
* @author Looly
|
||||||
*/
|
*/
|
||||||
public interface ValueMatcher extends Matcher<Integer> {
|
public interface PartMatcher extends Matcher<Integer> {
|
||||||
/**
|
/**
|
||||||
* 获取指定值之后的匹配值,也可以是指定值本身
|
* 获取指定值之后的匹配值,也可以是指定值本身
|
||||||
* @param value 指定的值
|
* @param value 指定的值
|
@ -0,0 +1,166 @@
|
|||||||
|
package cn.hutool.cron.pattern.matcher;
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.mutable.MutableBool;
|
||||||
|
import cn.hutool.cron.pattern.Part;
|
||||||
|
|
||||||
|
import java.time.Year;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单一表达式的匹配器,匹配器由7个{@link PartMatcher}组成
|
||||||
|
*
|
||||||
|
* @author looly
|
||||||
|
* @since 5.8.0
|
||||||
|
*/
|
||||||
|
public class PatternMatcher {
|
||||||
|
|
||||||
|
private final PartMatcher[] matchers;
|
||||||
|
|
||||||
|
public PatternMatcher(PartMatcher secondMatcher,
|
||||||
|
PartMatcher minuteMatcher,
|
||||||
|
PartMatcher hourMatcher,
|
||||||
|
PartMatcher dayOfMonthMatcher,
|
||||||
|
PartMatcher monthMatcher,
|
||||||
|
PartMatcher dayOfWeekMatcher,
|
||||||
|
PartMatcher yearMatcher) {
|
||||||
|
|
||||||
|
matchers = new PartMatcher[]{
|
||||||
|
secondMatcher,
|
||||||
|
minuteMatcher,
|
||||||
|
hourMatcher,
|
||||||
|
dayOfMonthMatcher,
|
||||||
|
monthMatcher,
|
||||||
|
dayOfWeekMatcher,
|
||||||
|
yearMatcher
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据表达式位置,获取对应的{@link PartMatcher}
|
||||||
|
* @param part 表达式位置
|
||||||
|
* @return {@link PartMatcher}
|
||||||
|
*/
|
||||||
|
public PartMatcher get(Part part){
|
||||||
|
return matchers[part.ordinal()];
|
||||||
|
}
|
||||||
|
|
||||||
|
//region match
|
||||||
|
/**
|
||||||
|
* 给定时间是否匹配定时任务表达式
|
||||||
|
*
|
||||||
|
* @param second 秒数,-1表示不匹配此项
|
||||||
|
* @param minute 分钟
|
||||||
|
* @param hour 小时
|
||||||
|
* @param dayOfMonth 天
|
||||||
|
* @param month 月,从1开始
|
||||||
|
* @param dayOfWeek 周,从0开始,0和7都表示周日
|
||||||
|
* @param year 年
|
||||||
|
* @return 如果匹配返回 {@code true}, 否则返回 {@code false}
|
||||||
|
*/
|
||||||
|
public boolean match(int second, int minute, int hour, int dayOfMonth, int month, int dayOfWeek, int year) {
|
||||||
|
return ((second < 0) || matchers[0].match(second)) // 匹配秒(非秒匹配模式下始终返回true)
|
||||||
|
&& matchers[1].match(minute)// 匹配分
|
||||||
|
&& matchers[2].match(hour)// 匹配时
|
||||||
|
&& isMatchDayOfMonth(matchers[3], dayOfMonth, month, Year.isLeap(year))// 匹配日
|
||||||
|
&& matchers[4].match(month) // 匹配月
|
||||||
|
&& matchers[5].match(dayOfWeek)// 匹配周
|
||||||
|
&& matchers[6].match(year);// 匹配年
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否匹配日(指定月份的第几天)
|
||||||
|
*
|
||||||
|
* @param matcher {@link PartMatcher}
|
||||||
|
* @param dayOfMonth 日
|
||||||
|
* @param month 月
|
||||||
|
* @param isLeapYear 是否闰年
|
||||||
|
* @return 是否匹配
|
||||||
|
*/
|
||||||
|
private static boolean isMatchDayOfMonth(PartMatcher matcher, int dayOfMonth, int month, boolean isLeapYear) {
|
||||||
|
return ((matcher instanceof DayOfMonthMatcher) //
|
||||||
|
? ((DayOfMonthMatcher) matcher).match(dayOfMonth, month, isLeapYear) //
|
||||||
|
: matcher.match(dayOfMonth));
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region nextMatchAfter
|
||||||
|
/**
|
||||||
|
* 获取下一个匹配日期时间
|
||||||
|
*
|
||||||
|
* @param second 秒
|
||||||
|
* @param minute 分
|
||||||
|
* @param hour 时
|
||||||
|
* @param dayOfMonth 天
|
||||||
|
* @param month 月(从1开始)
|
||||||
|
* @param dayOfWeek 周(从0开始, 0表示周日)
|
||||||
|
* @param year 年
|
||||||
|
* @param zone 时区
|
||||||
|
* @return {@link Calendar}
|
||||||
|
*/
|
||||||
|
public Calendar nextMatchAfter(int second, int minute, int hour,
|
||||||
|
int dayOfMonth, int month, int dayOfWeek, int year, TimeZone zone) {
|
||||||
|
|
||||||
|
Calendar calendar = Calendar.getInstance(zone);
|
||||||
|
|
||||||
|
// 上一个字段不一致,说明产生了新值,下一个字段使用最小值
|
||||||
|
MutableBool isNextEquals = new MutableBool(true);
|
||||||
|
// 年
|
||||||
|
final int nextYear = nextAfter(get(Part.YEAR), year, isNextEquals);
|
||||||
|
calendar.set(Calendar.YEAR, nextYear);
|
||||||
|
|
||||||
|
// 周
|
||||||
|
final int nextDayOfWeek = nextAfter(get(Part.DAY_OF_WEEK), dayOfWeek, isNextEquals);
|
||||||
|
calendar.set(Calendar.DAY_OF_WEEK, nextDayOfWeek + 1);
|
||||||
|
|
||||||
|
// 月
|
||||||
|
final int nextMonth = nextAfter(get(Part.MONTH), month, isNextEquals);
|
||||||
|
calendar.set(Calendar.MONTH, nextMonth - 1);
|
||||||
|
|
||||||
|
// 日
|
||||||
|
final int nextDayOfMonth = nextAfter(get(Part.DAY_OF_MONTH), dayOfMonth, isNextEquals);
|
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, nextDayOfMonth);
|
||||||
|
|
||||||
|
// 时
|
||||||
|
final int nextHour = nextAfter(get(Part.HOUR), hour, isNextEquals);
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, nextHour);
|
||||||
|
|
||||||
|
// 分
|
||||||
|
int nextMinute = nextAfter(get(Part.MINUTE), minute, isNextEquals);
|
||||||
|
calendar.set(Calendar.MINUTE, nextMinute);
|
||||||
|
|
||||||
|
// 秒
|
||||||
|
final int nextSecond = nextAfter(get(Part.SECOND), second, isNextEquals);
|
||||||
|
calendar.set(Calendar.SECOND, nextSecond);
|
||||||
|
|
||||||
|
return calendar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对应字段匹配器的下一个值
|
||||||
|
*
|
||||||
|
* @param matcher 匹配器
|
||||||
|
* @param value 值
|
||||||
|
* @param isNextEquals 是否下一个值和值相同。不同获取初始值,相同获取下一值,然后修改。
|
||||||
|
* @return 下一个值,-1标识匹配所有值的情况,应获取整个字段的最小值
|
||||||
|
*/
|
||||||
|
private static int nextAfter(PartMatcher matcher, int value, MutableBool isNextEquals) {
|
||||||
|
int nextValue;
|
||||||
|
if (isNextEquals.get()) {
|
||||||
|
// 上一层级得到相同值,下级获取下个值
|
||||||
|
nextValue = matcher.nextAfter(value);
|
||||||
|
isNextEquals.set(nextValue == value);
|
||||||
|
} else {
|
||||||
|
// 上一层级的值得到了不同值,下级的所有值使用最小值
|
||||||
|
if (matcher instanceof AlwaysTrueMatcher) {
|
||||||
|
nextValue = value;
|
||||||
|
} else if (matcher instanceof BoolArrayMatcher) {
|
||||||
|
nextValue = ((BoolArrayMatcher) matcher).getMinValue();
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid matcher: " + matcher.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextValue;
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
}
|
@ -9,7 +9,7 @@ import java.util.LinkedHashSet;
|
|||||||
*
|
*
|
||||||
* @author Looly
|
* @author Looly
|
||||||
*/
|
*/
|
||||||
public class YearValueMatcher implements ValueMatcher {
|
public class YearValueMatcher implements PartMatcher {
|
||||||
|
|
||||||
private final LinkedHashSet<Integer> valueList;
|
private final LinkedHashSet<Integer> valueList;
|
||||||
|
|
||||||
|
@ -7,17 +7,17 @@ import cn.hutool.core.util.NumberUtil;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.cron.CronException;
|
import cn.hutool.cron.CronException;
|
||||||
import cn.hutool.cron.pattern.Part;
|
import cn.hutool.cron.pattern.Part;
|
||||||
import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher;
|
import cn.hutool.cron.pattern.matcher.AlwaysTrueMatcher;
|
||||||
import cn.hutool.cron.pattern.matcher.BoolArrayValueMatcher;
|
import cn.hutool.cron.pattern.matcher.BoolArrayMatcher;
|
||||||
import cn.hutool.cron.pattern.matcher.DayOfMonthValueMatcher;
|
import cn.hutool.cron.pattern.matcher.DayOfMonthMatcher;
|
||||||
import cn.hutool.cron.pattern.matcher.ValueMatcher;
|
import cn.hutool.cron.pattern.matcher.PartMatcher;
|
||||||
import cn.hutool.cron.pattern.matcher.YearValueMatcher;
|
import cn.hutool.cron.pattern.matcher.YearValueMatcher;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时任务表达式各个部分的解析器,根据{@link Part}指定不同部分,解析为{@link ValueMatcher}<br>
|
* 定时任务表达式各个部分的解析器,根据{@link Part}指定不同部分,解析为{@link PartMatcher}<br>
|
||||||
* 每个部分支持:
|
* 每个部分支持:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li><strong>*</strong> :表示匹配这个位置所有的时间</li>
|
* <li><strong>*</strong> :表示匹配这个位置所有的时间</li>
|
||||||
@ -56,21 +56,21 @@ public class PartParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将表达式解析为{@link ValueMatcher}<br>
|
* 将表达式解析为{@link PartMatcher}<br>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>* 或者 ? 返回{@link AlwaysTrueValueMatcher}</li>
|
* <li>* 或者 ? 返回{@link AlwaysTrueMatcher}</li>
|
||||||
* <li>{@link Part#DAY_OF_MONTH} 返回{@link DayOfMonthValueMatcher}</li>
|
* <li>{@link Part#DAY_OF_MONTH} 返回{@link DayOfMonthMatcher}</li>
|
||||||
* <li>{@link Part#YEAR} 返回{@link YearValueMatcher}</li>
|
* <li>{@link Part#YEAR} 返回{@link YearValueMatcher}</li>
|
||||||
* <li>其他 返回{@link BoolArrayValueMatcher}</li>
|
* <li>其他 返回{@link BoolArrayMatcher}</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param value 表达式
|
* @param value 表达式
|
||||||
* @return {@link ValueMatcher}
|
* @return {@link PartMatcher}
|
||||||
*/
|
*/
|
||||||
public ValueMatcher parseAsValueMatcher(String value) {
|
public PartMatcher parse(String value) {
|
||||||
if (isMatchAllStr(value)) {
|
if (isMatchAllStr(value)) {
|
||||||
//兼容Quartz的"?"表达式,不会出现互斥情况,与"*"作用相同
|
//兼容Quartz的"?"表达式,不会出现互斥情况,与"*"作用相同
|
||||||
return new AlwaysTrueValueMatcher();
|
return new AlwaysTrueMatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Integer> values = parseArray(value);
|
final List<Integer> values = parseArray(value);
|
||||||
@ -80,11 +80,11 @@ public class PartParser {
|
|||||||
|
|
||||||
switch (this.part) {
|
switch (this.part) {
|
||||||
case DAY_OF_MONTH:
|
case DAY_OF_MONTH:
|
||||||
return new DayOfMonthValueMatcher(values);
|
return new DayOfMonthMatcher(values);
|
||||||
case YEAR:
|
case YEAR:
|
||||||
return new YearValueMatcher(values);
|
return new YearValueMatcher(values);
|
||||||
default:
|
default:
|
||||||
return new BoolArrayValueMatcher(values);
|
return new BoolArrayMatcher(values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,6 +243,11 @@ public class PartParser {
|
|||||||
i = parseAlias(value);
|
i = parseAlias(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支持负数
|
||||||
|
if(i < 0){
|
||||||
|
i += part.getMax();
|
||||||
|
}
|
||||||
|
|
||||||
// 周日可以用0或7表示,统一转换为0
|
// 周日可以用0或7表示,统一转换为0
|
||||||
if(Part.DAY_OF_WEEK.equals(this.part) && Week.SUNDAY.getIso8601Value() == i){
|
if(Part.DAY_OF_WEEK.equals(this.part) && Week.SUNDAY.getIso8601Value() == i){
|
||||||
i = Week.SUNDAY.ordinal();
|
i = Week.SUNDAY.ordinal();
|
||||||
|
@ -4,15 +4,15 @@ import cn.hutool.core.lang.Assert;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.cron.CronException;
|
import cn.hutool.cron.CronException;
|
||||||
import cn.hutool.cron.pattern.Part;
|
import cn.hutool.cron.pattern.Part;
|
||||||
import cn.hutool.cron.pattern.matcher.AlwaysTrueValueMatcher;
|
import cn.hutool.cron.pattern.matcher.AlwaysTrueMatcher;
|
||||||
import cn.hutool.cron.pattern.matcher.DateTimeMatcher;
|
import cn.hutool.cron.pattern.matcher.PartMatcher;
|
||||||
import cn.hutool.cron.pattern.matcher.MatcherTable;
|
import cn.hutool.cron.pattern.matcher.PatternMatcher;
|
||||||
import cn.hutool.cron.pattern.matcher.ValueMatcher;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时任务表达式解析器,用于将表达式字符串解析为{@link MatcherTable}
|
* 定时任务表达式解析器,用于将表达式字符串解析为{@link PatternMatcher}的列表
|
||||||
*
|
*
|
||||||
* @author looly
|
* @author looly
|
||||||
* @since 5.8.0
|
* @since 5.8.0
|
||||||
@ -28,12 +28,12 @@ public class PatternParser {
|
|||||||
private static final PartParser YEAR_VALUE_PARSER = PartParser.of(Part.YEAR);
|
private static final PartParser YEAR_VALUE_PARSER = PartParser.of(Part.YEAR);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析表达式到匹配表中
|
* 解析表达式到匹配列表中
|
||||||
*
|
*
|
||||||
* @param cronPattern 复合表达式
|
* @param cronPattern 复合表达式
|
||||||
* @return {@link MatcherTable}
|
* @return {@link List}
|
||||||
*/
|
*/
|
||||||
public static MatcherTable parse(String cronPattern) {
|
public static List<PatternMatcher> parse(String cronPattern) {
|
||||||
return parseGroupPattern(cronPattern);
|
return parseGroupPattern(cronPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,24 +44,24 @@ public class PatternParser {
|
|||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @param groupPattern 复合表达式
|
* @param groupPattern 复合表达式
|
||||||
* @return {@link MatcherTable}
|
* @return {@link List}
|
||||||
*/
|
*/
|
||||||
private static MatcherTable parseGroupPattern(String groupPattern) {
|
private static List<PatternMatcher> parseGroupPattern(String groupPattern) {
|
||||||
final List<String> patternList = StrUtil.splitTrim(groupPattern, '|');
|
final List<String> patternList = StrUtil.splitTrim(groupPattern, '|');
|
||||||
final MatcherTable matcherTable = new MatcherTable(patternList.size());
|
final List<PatternMatcher> patternMatchers = new ArrayList<>(patternList.size());
|
||||||
for (String pattern : patternList) {
|
for (String pattern : patternList) {
|
||||||
matcherTable.matchers.add(parseSinglePattern(pattern));
|
patternMatchers.add(parseSinglePattern(pattern));
|
||||||
}
|
}
|
||||||
return matcherTable;
|
return patternMatchers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析单一定时任务表达式
|
* 解析单一定时任务表达式
|
||||||
*
|
*
|
||||||
* @param pattern 表达式
|
* @param pattern 表达式
|
||||||
* @return {@link DateTimeMatcher}
|
* @return {@link PatternMatcher}
|
||||||
*/
|
*/
|
||||||
private static DateTimeMatcher parseSinglePattern(String pattern) {
|
private static PatternMatcher parseSinglePattern(String pattern) {
|
||||||
final String[] parts = pattern.split("\\s+");
|
final String[] parts = pattern.split("\\s+");
|
||||||
Assert.checkBetween(parts.length, 5, 7,
|
Assert.checkBetween(parts.length, 5, 7,
|
||||||
() -> new CronException("Pattern [{}] is invalid, it must be 5-7 parts!", pattern));
|
() -> new CronException("Pattern [{}] is invalid, it must be 5-7 parts!", pattern));
|
||||||
@ -76,26 +76,26 @@ public class PatternParser {
|
|||||||
final String secondPart = (1 == offset) ? parts[0] : "0";
|
final String secondPart = (1 == offset) ? parts[0] : "0";
|
||||||
|
|
||||||
// 年
|
// 年
|
||||||
ValueMatcher yearMatcher;
|
PartMatcher yearMatcher;
|
||||||
if (parts.length == 7) {// 支持年的表达式
|
if (parts.length == 7) {// 支持年的表达式
|
||||||
yearMatcher = YEAR_VALUE_PARSER.parseAsValueMatcher(parts[6]);
|
yearMatcher = YEAR_VALUE_PARSER.parse(parts[6]);
|
||||||
} else {// 不支持年的表达式,全部匹配
|
} else {// 不支持年的表达式,全部匹配
|
||||||
yearMatcher = AlwaysTrueValueMatcher.INSTANCE;
|
yearMatcher = AlwaysTrueMatcher.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DateTimeMatcher(
|
return new PatternMatcher(
|
||||||
// 秒
|
// 秒
|
||||||
SECOND_VALUE_PARSER.parseAsValueMatcher(secondPart),
|
SECOND_VALUE_PARSER.parse(secondPart),
|
||||||
// 分
|
// 分
|
||||||
MINUTE_VALUE_PARSER.parseAsValueMatcher(parts[offset]),
|
MINUTE_VALUE_PARSER.parse(parts[offset]),
|
||||||
// 时
|
// 时
|
||||||
HOUR_VALUE_PARSER.parseAsValueMatcher(parts[1 + offset]),
|
HOUR_VALUE_PARSER.parse(parts[1 + offset]),
|
||||||
// 天
|
// 天
|
||||||
DAY_OF_MONTH_VALUE_PARSER.parseAsValueMatcher(parts[2 + offset]),
|
DAY_OF_MONTH_VALUE_PARSER.parse(parts[2 + offset]),
|
||||||
// 月
|
// 月
|
||||||
MONTH_VALUE_PARSER.parseAsValueMatcher(parts[3 + offset]),
|
MONTH_VALUE_PARSER.parse(parts[3 + offset]),
|
||||||
// 周
|
// 周
|
||||||
DAY_OF_WEEK_VALUE_PARSER.parseAsValueMatcher(parts[4 + offset]),
|
DAY_OF_WEEK_VALUE_PARSER.parse(parts[4 + offset]),
|
||||||
// 年
|
// 年
|
||||||
yearMatcher
|
yearMatcher
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ package cn.hutool.cron.pattern;
|
|||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.lang.Console;
|
import cn.hutool.core.lang.Console;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
@ -13,7 +14,9 @@ public class CronPatternNextMatchTest {
|
|||||||
CronPattern pattern = new CronPattern("23 12 * 12 * * *");
|
CronPattern pattern = new CronPattern("23 12 * 12 * * *");
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
final Calendar calendar = pattern.nextMatchAfter(
|
final Calendar calendar = pattern.nextMatchAfter(
|
||||||
DateUtil.parse("2022-04-12 09:12:23").toCalendar());
|
DateUtil.parse("2022-04-12 09:12:12").toCalendar());
|
||||||
|
|
||||||
Console.log(DateUtil.date(calendar));
|
Console.log(DateUtil.date(calendar));
|
||||||
|
Assert.assertTrue(pattern.match(calendar, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,6 +122,14 @@ public class CronPatternTest {
|
|||||||
assertMatch(pattern, "2017-02-19 04:00:33");
|
assertMatch(pattern, "2017-02-19 04:00:33");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void patternNegativeTest() {
|
||||||
|
// -4表示倒数的数字,此处在小时上,-4表示 23 - 4,为19
|
||||||
|
CronPattern pattern = CronPattern.of("* 0 -4 * * ?");
|
||||||
|
assertMatch(pattern, "2017-02-09 19:00:00");
|
||||||
|
assertMatch(pattern, "2017-02-19 19:00:33");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void rangePatternTest() {
|
public void rangePatternTest() {
|
||||||
CronPattern pattern = CronPattern.of("* 20/2 * * * ?");
|
CronPattern pattern = CronPattern.of("* 20/2 * * * ?");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user