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