diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java
index c3f8990df3..ff0a8732d0 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java
@@ -41,6 +41,16 @@ 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;
@@ -111,6 +121,15 @@ public final class CronExpression {
*
{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight
*
*
+ * The following macros are also supported:
+ *
+ * - {@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},
+ * - {@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},
+ * - {@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},
+ * - {@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},
+ * - {@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.
+ *
+ *
* @param expression the expression string to parse
* @return the parsed {@code CronExpression} object
* @throws IllegalArgumentException in the expression does not conform to
@@ -119,6 +138,8 @@ public final class CronExpression {
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(
@@ -141,6 +162,17 @@ public final class CronExpression {
}
+ 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
diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java
index 0e6fd69076..e77fd644d0 100644
--- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java
+++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java
@@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.DayOfWeek.MONDAY;
+import static java.time.DayOfWeek.SUNDAY;
import static java.time.DayOfWeek.TUESDAY;
import static java.time.DayOfWeek.WEDNESDAY;
import static java.time.temporal.TemporalAdjusters.next;
@@ -462,4 +463,124 @@ class CronExpressionTests {
assertThat(actual.getDayOfMonth()).isEqualTo(13);
}
+ @Test
+ void yearly() {
+ CronExpression expression = CronExpression.parse("@yearly");
+ assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *"));
+
+ LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10);
+ LocalDateTime expected = LocalDateTime.of(last.getYear() + 1, 1, 1, 0, 0);
+
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusYears(1);
+ actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusYears(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void annually() {
+ CronExpression expression = CronExpression.parse("@annually");
+ assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *"));
+ assertThat(expression).isEqualTo(CronExpression.parse("@yearly"));
+ }
+
+ @Test
+ void monthly() {
+ CronExpression expression = CronExpression.parse("@monthly");
+ assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 * *"));
+
+ LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10);
+ LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0);
+
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusMonths(1);
+ actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusMonths(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void weekly() {
+ CronExpression expression = CronExpression.parse("@weekly");
+ assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * 0"));
+
+ LocalDateTime last = LocalDateTime.now();
+ LocalDateTime expected = last.with(next(SUNDAY)).withHour(0).withMinute(0).withSecond(0).withNano(0);
+
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusWeeks(1);
+ actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusWeeks(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void daily() {
+ CronExpression expression = CronExpression.parse("@daily");
+ assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *"));
+
+ LocalDateTime last = LocalDateTime.now();
+ LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
+
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusDays(1);
+ actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusDays(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+ @Test
+ void midnight() {
+ CronExpression expression = CronExpression.parse("@midnight");
+ assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *"));
+ assertThat(expression).isEqualTo(CronExpression.parse("@daily"));
+ }
+
+ @Test
+ void hourly() {
+ CronExpression expression = CronExpression.parse("@hourly");
+ assertThat(expression).isEqualTo(CronExpression.parse("0 0 * * * *"));
+
+ LocalDateTime last = LocalDateTime.now();
+ LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(0).withNano(0);
+
+ LocalDateTime actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusHours(1);
+ actual = expression.next(last);
+ assertThat(actual).isEqualTo(expected);
+
+ last = actual;
+ expected = expected.plusHours(1);
+ assertThat(expression.next(last)).isEqualTo(expected);
+ }
+
+
}