In Spring source code, the source code for cron parsing is in CronExpression. When the scheduled task is created, the CornExpression. Parse method is called for parsing
public CronTrigger(String expression, ZoneId zoneId) {
Assert.hasLength(expression, "Expression must not be empty");
Assert.notNull(zoneId, "ZoneId must not be null");
this.expression = CronExpression.parse(expression);
this.zoneId = zoneId;
}
Copy the code
Now let’s demystify the parsing of cron expressions
public static CronExpression parse(String expression) {
Assert.hasLength(expression, "Expression string must not be empty");
// If expression is the annotation form, replace the annotation with the following form (see end)
expression = resolveMacros(expression);
/ / StringUtils. TokenizeToStringArray and the split method
String[] fields = StringUtils.tokenizeToStringArray(expression, "");
if(fields.length ! =6) {
// The cron expression must consist of six items
throw new IllegalArgumentException(String.format(
"Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
}
try {
CronField seconds = CronField.parseSeconds(fields[0]); // The first term is seconds
CronField minutes = CronField.parseMinutes(fields[1]); // The second term is points
CronField hours = CronField.parseHours(fields[2]); // The third term is time
CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]); // The fourth item is the day
CronField months = CronField.parseMonth(fields[4]); // The fifth item is month
CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]); // The sixth item is the year
return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
}
catch (IllegalArgumentException ex) {
String msg = ex.getMessage() + " in cron expression \"" + expression + "\" ";
throw newIllegalArgumentException(msg, ex); }}/ / resolveMacros function
private static String resolveMacros(String expression) {
expression = expression.trim();
for (int i = 0; i < MACROS.length; i = i + 2) {
if (MACROS[i].equalsIgnoreCase(expression)) {
return MACROS[i + 1]; }}return expression;
}
private static final String[] MACROS = new String[] {
"@yearly"."0 0 0 1 1 *"."@annually"."0 0 0 1 1 *"."@monthly"."0 0 0 1 * *"."@weekly"."0 0 0 * * 0"."@daily"."0 0 0 * * *"."@midnight"."0 0 0 * * *"."@hourly"."0 0 * * * *"
};
Copy the code
Now, remember that the cron expression order must be six entries in seconds, minutes, hours, days, months, and years, or be replaced with MACROS defined in the system, separated by Spaces. So how exactly is each term resolved and expressed? Take a look at the definition in CronField.
/ / SEC.
public static CronField parseSeconds(String value) {
return BitsCronField.parseSeconds(value);
}
// This call stack is like a nesting doll
public static BitsCronField parseSeconds(String value) {
return parseField(value, Type.SECOND);
}
private static BitsCronField parseField(String value, Type type) {
Assert.hasLength(value, "Value must not be empty");
Assert.notNull(type, "Type must not be null");
try {
BitsCronField result = new BitsCronField(type);
// Separate strings by commas, i.e., we can separate each item with commas, representing different times
String[] fields = StringUtils.delimitedListToStringArray(value, ",");
for (String field : fields) {
int slashPos = field.indexOf('/');
// Check whether there are slashes in the time
if (slashPos == -1) {
// If not, parse and set the time range
ValueRange range = parseRange(field, type);
result.setBits(range);
}
else {
String rangeStr = value.substring(0, slashPos);
String deltaStr = value.substring(slashPos + 1);
// Parse and create a time range based on the content before the slash
ValueRange range = parseRange(rangeStr, type);
if (rangeStr.indexOf(The '-') = = -1) {
// If the expression before the slash does not contain the slash, set the end time of the current range to the maximum value of the current type
range = ValueRange.of(range.getMinimum(), type.range().getMaximum());
}
int delta = Integer.parseInt(deltaStr);
if (delta <= 0) {
throw new IllegalArgumentException("Incrementer delta must be 1 or higher");
}
// Put delta in to set the time rangeresult.setBits(range, delta); }}return result;
}
catch (DateTimeException | IllegalArgumentException ex) {
String msg = ex.getMessage() + "'" + value + "'";
throw newIllegalArgumentException(msg, ex); }}// parseRange
private static ValueRange parseRange(String value, Type type) {
if (value.equals("*")) {
// Return range() of that type if it is *
return type.range();
}
else {
int hyphenPos = value.indexOf(The '-');
if (hyphenPos == -1) {
int result = type.checkValidValue(Integer.parseInt(value));
// If there is no bar, the start and end of the time period are the current event points
return ValueRange.of(result, result);
}
else {
// If there is a bar, the beginning of the period is the number before the bar, and the end is the number after the bar
int min = Integer.parseInt(value.substring(0, hyphenPos));
int max = Integer.parseInt(value.substring(hyphenPos + 1));
min = type.checkValidValue(min); / / check
max = type.checkValidValue(max); / / check
returnValueRange.of(min, max); }}}BitsCronField is implemented with a long integer of bits to store a time bit
private void setBits(ValueRange range) {
// If there is no delta
if (range.getMinimum() == range.getMaximum()) {
// If it is a point in time, since our bits default value is 0, the semantics here are to set the bits range.getMinimum() bit to 1
setBit((int) range.getMinimum());
}
else {
// If it is a time period, move Mask left to range.getMinimum() bit and set it to minMask
// Move Mask unsigned right - (range.getMaximum() + 1) bits
// private static final long MASK = 0xFFFFFFFFFFFFFFFFL;
GetMinimum () and range.getMaximum() bits, set to 1
long minMask = MASK << range.getMinimum();
long maxMask = MASK >>> - (range.getMaximum() + 1);
this.bits |= (minMask & maxMask); }}// Call this method with a slash
private void setBits(ValueRange range, int delta) {
if (delta == 1) {
// If there is a delta and it is 1, it is the same as none
setBits(range);
}
else {
// If delta is not 1, set position 1 according to delta for tolerance
for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) { setBit(i); }}}// Get the result of the current bits and (1L << index) bitwise or, where there is one bit
// We know that primitive types have default values. The default value of long is 0
For example, if it is a point in time, since our bits default value is 0, the semantics here are to simply set the range.getMinimum() position of bits to 1
private void setBit(int index) {
this.bits |= (1L << index);
}
Copy the code
The type.range method is called, and depending on the call stack, it will end up in the ChronoField enumeration, that is, if it is an asterisk, it will return the full range of events for the current parse type. From this we can see that the asterisk represents all times for all current parse types, a time period if there is a dash in the expression, and a point in time if it is a pure number.
public enum ChronoField implements TemporalField {
NANO_OF_SECOND("NanoOfSecond", NANOS, SECONDS, ValueRange.of(0.999 _999_999)),
NANO_OF_DAY("NanoOfDay", NANOS, DAYS, ValueRange.of(0.86400L * 1000_000_000L - 1)),
MICRO_OF_SECOND("MicroOfSecond", MICROS, SECONDS, ValueRange.of(0.999 _999)),
MICRO_OF_DAY("MicroOfDay", MICROS, DAYS, ValueRange.of(0.86400L * 1000_000L - 1)),
MILLI_OF_SECOND("MilliOfSecond", MILLIS, SECONDS, ValueRange.of(0.999)),
MILLI_OF_DAY("MilliOfDay", MILLIS, DAYS, ValueRange.of(0.86400L * 1000L - 1)),
SECOND_OF_MINUTE("SecondOfMinute", SECONDS, MINUTES, ValueRange.of(0.59), "second"),
SECOND_OF_DAY("SecondOfDay", SECONDS, DAYS, ValueRange.of(0.86400L - 1)),
MINUTE_OF_HOUR("MinuteOfHour", MINUTES, HOURS, ValueRange.of(0.59), "minute"),
MINUTE_OF_DAY("MinuteOfDay", MINUTES, DAYS, ValueRange.of(0, (24 * 60) - 1)),
HOUR_OF_AMPM("HourOfAmPm", HOURS, HALF_DAYS, ValueRange.of(0.11)),
CLOCK_HOUR_OF_AMPM("ClockHourOfAmPm", HOURS, HALF_DAYS, ValueRange.of(1.12)),
HOUR_OF_DAY("HourOfDay", HOURS, DAYS, ValueRange.of(0.23), "hour"),
CLOCK_HOUR_OF_DAY("ClockHourOfDay", HOURS, DAYS, ValueRange.of(1.24)),
AMPM_OF_DAY("AmPmOfDay", HALF_DAYS, DAYS, ValueRange.of(0.1), "dayperiod"),
DAY_OF_WEEK("DayOfWeek", DAYS, WEEKS, ValueRange.of(1.7), "weekday"),
ALIGNED_DAY_OF_WEEK_IN_MONTH("AlignedDayOfWeekInMonth", DAYS, WEEKS, ValueRange.of(1.7)),
ALIGNED_DAY_OF_WEEK_IN_YEAR("AlignedDayOfWeekInYear", DAYS, WEEKS, ValueRange.of(1.7)),
DAY_OF_MONTH("DayOfMonth", DAYS, MONTHS, ValueRange.of(1.28.31), "day"),
DAY_OF_YEAR("DayOfYear", DAYS, YEARS, ValueRange.of(1.365.366)),
EPOCH_DAY("EpochDay", DAYS, FOREVER, ValueRange.of((long) (Year.MIN_VALUE * 365.25), (long) (Year.MAX_VALUE * 365.25))),
ALIGNED_WEEK_OF_MONTH("AlignedWeekOfMonth", WEEKS, MONTHS, ValueRange.of(1.4.5)),
ALIGNED_WEEK_OF_YEAR("AlignedWeekOfYear", WEEKS, YEARS, ValueRange.of(1.53)),
MONTH_OF_YEAR("MonthOfYear", MONTHS, YEARS, ValueRange.of(1.12), "month"),
PROLEPTIC_MONTH("ProlepticMonth", MONTHS, FOREVER, ValueRange.of(Year.MIN_VALUE * 12L, Year.MAX_VALUE * 12L + 11)),
YEAR_OF_ERA("YearOfEra", YEARS, FOREVER, ValueRange.of(1, Year.MAX_VALUE, Year.MAX_VALUE + 1)),
YEAR("Year", YEARS, FOREVER, ValueRange.of(Year.MIN_VALUE, Year.MAX_VALUE), "year"),
ERA("Era", ERAS, FOREVER, ValueRange.of(0.1), "era"),
INSTANT_SECONDS("InstantSeconds", SECONDS, FOREVER, ValueRange.of(Long.MIN_VALUE, Long.MAX_VALUE)),
OFFSET_SECONDS("OffsetSeconds", SECONDS, FOREVER, ValueRange.of(-18 * 3600.18 * 3600));
private final String name;
private final TemporalUnit baseUnit;
private final TemporalUnit rangeUnit;
private final ValueRange range;
private final String displayNameKey;
private ChronoField(String name, TemporalUnit baseUnit, TemporalUnit rangeUnit, ValueRange range) {
this.name = name;
this.baseUnit = baseUnit;
this.rangeUnit = rangeUnit;
this.range = range;
this.displayNameKey = null;
}
private ChronoField(String name, TemporalUnit baseUnit, TemporalUnit rangeUnit, ValueRange range, String displayNameKey) {
this.name = name;
this.baseUnit = baseUnit;
this.rangeUnit = rangeUnit;
this.range = range;
this.displayNameKey = displayNameKey;
}
/ /... .
@Override
public ValueRange range(a) {
return range;
}
/ /... .
}
Copy the code
It is concluded that the rules
From the above source code analysis, we can summarize such a set of cron expression parsing rules
1. Cron expressions can be registered in six parts: second, minute, day, month, year, separated by Spaces. A limited set of strings starting with @ is defined to replace standard Cron expressions
private static final String[] MACROS = new String[] { "@yearly"."0 0 0 1 1 *"."@annually"."0 0 0 1 1 *"."@monthly"."0 0 0 1 * *"."@weekly"."0 0 0 * * 0"."@daily"."0 0 0 * * *"."@midnight"."0 0 0 * * *"."@hourly"."0 0 * * * *" }; Copy the code
Such as:
@Scheduled(cron = "@yearly") public void test(a){ logger.info("123"); } Copy the code
2. Each item can be separated by a comma to indicate a different point in time
Such as:
@scheduled (cron = "1,2,3 0 * * *") public void test(a){ logger.info("123"); } Copy the code
3. Each item can be separated by a horizontal bar to indicate the time period
Such as:
@scheduled (Scheduled cron = "1,2-4,5, 0 * * *") public void test(a){ logger.info("123"); } Copy the code
4. For each term, a combination of slashes and dashes can be used to indicate the point in time at which the tolerance is based on the value after the slash
Such as:
@Scheduled(cron = "1,2-20/3,5 0 0 * * *") public void test(a){ logger.info("123"); } Copy the code
5. For each entry, use an asterisk to indicate the entire range of the current time type
Such as:
@scheduled (cron = "1,2-20/3,5 * * * * *") public void test(a){ logger.info("123"); } Copy the code
Fried chicken spicy chicken original article, reproduced please indicate the source