spring CronExpression 源码

  • 2022-08-08
  • 浏览 (444)

spring CronExpression 代码

文件路径:/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java

/*
 * Copyright 2002-2021 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.scheduling.support;

import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.Arrays;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Representation of a
 * <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
 * that can calculate the next time it matches.
 *
 * <p>{@code CronExpression} instances are created through
 * {@link #parse(String)}; the next match is determined with
 * {@link #next(Temporal)}.
 *
 * @author Arjen Poutsma
 * @since 5.3
 * @see CronTrigger
 */
public final class CronExpression {

	static final int MAX_ATTEMPTS = 366;

	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 * * * *"
	};


	private final CronField[] fields;

	private final String expression;


	private CronExpression(
			CronField seconds,
			CronField minutes,
			CronField hours,
			CronField daysOfMonth,
			CronField months,
			CronField daysOfWeek,
			String expression) {

		// reverse order, to make big changes first
		// to make sure we end up at 0 nanos, we add an extra field
		this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()};
		this.expression = expression;
	}


	/**
	 * Parse the given
	 * <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
	 * string into a {@code CronExpression}.
	 * The string has six single space-separated time and date fields:
	 * <pre>
	 * &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; second (0-59)
	 * &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; minute (0 - 59)
	 * &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; hour (0 - 23)
	 * &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; day of the month (1 - 31)
	 * &#9474; &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; month (1 - 12) (or JAN-DEC)
	 * &#9474; &#9474; &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; day of the week (0 - 7)
	 * &#9474; &#9474; &#9474; &#9474; &#9474; &#9474;          (0 or 7 is Sunday, or MON-SUN)
	 * &#9474; &#9474; &#9474; &#9474; &#9474; &#9474;
	 * &#42; &#42; &#42; &#42; &#42; &#42;
	 * </pre>
	 *
	 * <p>The following rules apply:
	 * <ul>
	 * <li>
	 * A field may be an asterisk ({@code *}), which always stands for
	 * "first-last". For the "day of the month" or "day of the week" fields, a
	 * question mark ({@code ?}) may be used instead of an asterisk.
	 * </li>
	 * <li>
	 * Ranges of numbers are expressed by two numbers separated with a hyphen
	 * ({@code -}). The specified range is inclusive.
	 * </li>
	 * <li>Following a range (or {@code *}) with {@code /n} specifies
	 * the interval of the number's value through the range.
	 * </li>
	 * <li>
	 * English names can also be used for the "month" and "day of week" fields.
	 * Use the first three letters of the particular day or month (case does not
	 * matter).
	 * </li>
	 * <li>
	 * The "day of month" and "day of week" fields can contain a
	 * {@code L}-character, which stands for "last", and has a different meaning
	 * in each field:
	 * <ul>
	 * <li>
	 * In the "day of month" field, {@code L} stands for "the last day of the
	 * month". If followed by an negative offset (i.e. {@code L-n}), it means
	 * "{@code n}th-to-last day of the month". If followed by {@code W} (i.e.
	 * {@code LW}), it means "the last weekday of the month".
	 * </li>
	 * <li>
	 * In the "day of week" field, {@code L} stands for "the last day of the
	 * week".
	 * If prefixed by a number or three-letter name (i.e. {@code dL} or
	 * {@code DDDL}), it means "the last day of week {@code d} (or {@code DDD})
	 * in the month".
	 * </li>
	 * </ul>
	 * </li>
	 * <li>
	 * The "day of month" field can be {@code nW}, which stands for "the nearest
	 * weekday to day of the month {@code n}".
	 * If {@code n} falls on Saturday, this yields the Friday before it.
	 * If {@code n} falls on Sunday, this yields the Monday after,
	 * which also happens if {@code n} is {@code 1} and falls on a Saturday
	 * (i.e. {@code 1W} stands for "the first weekday of the month").
	 * </li>
	 * <li>
	 * The "day of week" field can be {@code d#n} (or {@code DDD#n}), which
	 * stands for "the {@code n}-th day of week {@code d} (or {@code DDD}) in
	 * the month".
	 * </li>
	 * </ul>
	 *
	 * <p>Example expressions:
	 * <ul>
	 * <li>{@code "0 0 * * * *"} = the top of every hour of every day.</li>
	 * <li><code>"*&#47;10 * * * * *"</code> = every ten seconds.</li>
	 * <li>{@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.</li>
	 * <li>{@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.</li>
	 * <li>{@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.</li>
	 * <li>{@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays</li>
	 * <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li>
	 * <li>{@code "0 0 0 L * *"} = last day of the month at midnight</li>
	 * <li>{@code "0 0 0 L-3 * *"} = third-to-last day of the month at midnight</li>
	 * <li>{@code "0 0 0 1W * *"} = first weekday of the month at midnight</li>
	 * <li>{@code "0 0 0 LW * *"} = last weekday of the month at midnight</li>
	 * <li>{@code "0 0 0 * * 5L"} = last Friday of the month at midnight</li>
	 * <li>{@code "0 0 0 * * THUL"} = last Thursday of the month at midnight</li>
	 * <li>{@code "0 0 0 ? * 5#2"} = the second Friday in the month at midnight</li>
	 * <li>{@code "0 0 0 ? * MON#1"} = the first Monday in the month at midnight</li>
	 * </ul>
	 *
	 * <p>The following macros are also supported:
	 * <ul>
	 * <li>{@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},</li>
	 * <li>{@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},</li>
	 * <li>{@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},</li>
	 * <li>{@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},</li>
	 * <li>{@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.</li>
	 * </ul>
	 * @param expression the expression string to parse
	 * @return the parsed {@code CronExpression} object
	 * @throws IllegalArgumentException in the expression does not conform to
	 * the cron format
	 */
	public static CronExpression parse(String expression) {
		Assert.hasLength(expression, "Expression string must not be empty");

		expression = resolveMacros(expression);

		String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
		if (fields.length != 6) {
			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]);
			CronField minutes = CronField.parseMinutes(fields[1]);
			CronField hours = CronField.parseHours(fields[2]);
			CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]);
			CronField months = CronField.parseMonth(fields[4]);
			CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]);

			return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
		}
		catch (IllegalArgumentException ex) {
			String msg = ex.getMessage() + " in cron expression \"" + expression + "\"";
			throw new IllegalArgumentException(msg, ex);
		}
	}

	/**
	 * Determine whether the given string represents a valid cron expression.
	 * @param expression the expression to evaluate
	 * @return {@code true} if the given expression is a valid cron expression
	 * @since 5.3.8
	 */
	public static boolean isValidExpression(@Nullable String expression) {
		if (expression == null) {
			return false;
		}
		try {
			parse(expression);
			return true;
		}
		catch (IllegalArgumentException ex) {
			return false;
		}
	}


	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;
	}


	/**
	 * Calculate the next {@link Temporal} that matches this expression.
	 * @param temporal the seed value
	 * @param <T> the type of temporal
	 * @return the next temporal that matches this expression, or {@code null}
	 * if no such temporal can be found
	 */
	@Nullable
	public <T extends Temporal & Comparable<? super T>> T next(T temporal) {
		return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
	}


	@Nullable
	private <T extends Temporal & Comparable<? super T>> T nextOrSame(T temporal) {
		for (int i = 0; i < MAX_ATTEMPTS; i++) {
			T result = nextOrSameInternal(temporal);
			if (result == null || result.equals(temporal)) {
				return result;
			}
			temporal = result;
		}
		return null;
	}

	@Nullable
	private <T extends Temporal & Comparable<? super T>> T nextOrSameInternal(T temporal) {
		for (CronField field : this.fields) {
			temporal = field.nextOrSame(temporal);
			if (temporal == null) {
				return null;
			}
		}
		return temporal;
	}


	@Override
	public int hashCode() {
		return Arrays.hashCode(this.fields);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o instanceof CronExpression other) {
			return Arrays.equals(this.fields, other.fields);
		}
		else {
			return false;
		}
	}

	/**
	 * Return the expression string used to create this {@code CronExpression}.
	 * @return the expression string
	 */
	@Override
	public String toString() {
		return this.expression;
	}

}

相关信息

spring 源码目录

相关文章

spring BitsCronField 源码

spring CompositeCronField 源码

spring CronField 源码

spring CronSequenceGenerator 源码

spring CronTrigger 源码

spring DelegatingErrorHandlingRunnable 源码

spring MethodInvokingRunnable 源码

spring PeriodicTrigger 源码

spring QuartzCronField 源码

spring ScheduledMethodRunnable 源码

0  赞