diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 319165d2bec..c4206c63534 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -100,9 +100,9 @@ public @interface Scheduled { /** * A time zone for which the cron expression will be resolved. By default, this - * attribute is the empty String (i.e. the server's local time zone will be used). + * attribute is the empty String (i.e. the scheduler's time zone will be used). * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)}, - * or an empty String to indicate the server's default time zone + * or an empty String to indicate the scheduler's default time zone * @since 4.0 * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone) * @see java.util.TimeZone diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index db1372b70cb..28d1aea5f08 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -27,7 +27,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -84,7 +83,7 @@ import org.springframework.util.StringValueResolver; * "fixedRate", "fixedDelay", or "cron" expression provided via the annotation. * *

This post-processor is automatically registered by Spring's - * {@code } XML element, and also by the + * {@code } XML element and also by the * {@link EnableScheduling @EnableScheduling} annotation. * *

Autodetects any {@link SchedulingConfigurer} instances in the container, @@ -434,14 +433,14 @@ public class ScheduledAnnotationBeanPostProcessor Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); processedSchedule = true; if (!Scheduled.CRON_DISABLED.equals(cron)) { - TimeZone timeZone; + CronTrigger trigger; if (StringUtils.hasText(zone)) { - timeZone = StringUtils.parseTimeZoneString(zone); + trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone)); } else { - timeZone = TimeZone.getDefault(); + trigger = new CronTrigger(cron); } - tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger))); } } } 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 e2e6687055e..2549faa3efe 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -57,18 +57,12 @@ public final class CronExpression { private final String expression; - private CronExpression( - CronField seconds, - CronField minutes, - CronField hours, - CronField daysOfMonth, - CronField months, - CronField daysOfWeek, - 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()}; + // 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; } @@ -121,11 +115,8 @@ public final class CronExpression { * {@code LW}), it means "the last weekday of the month". * *

  • - * 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". + * In the "day of week" field, {@code dL} or {@code DDDL} stands for + * "the last day of week {@code d} (or {@code DDD}) in the month". *
  • * * @@ -177,7 +168,7 @@ public final class CronExpression { * the cron format */ public static CronExpression parse(String expression) { - Assert.hasLength(expression, "Expression string must not be empty"); + Assert.hasLength(expression, "Expression must not be empty"); expression = resolveMacros(expression); @@ -271,27 +262,18 @@ public final class CronExpression { @Override - public int hashCode() { - return Arrays.hashCode(this.fields); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof CronExpression && + Arrays.equals(this.fields, ((CronExpression) other).fields))); } @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o instanceof CronExpression) { - CronExpression other = (CronExpression) o; - return Arrays.equals(this.fields, other.fields); - } - else { - return false; - } + public int hashCode() { + return Arrays.hashCode(this.fields); } /** * Return the expression string used to create this {@code CronExpression}. - * @return the expression string */ @Override public String toString() { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index 6581cdd61fe..215ef40bbb1 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -39,6 +39,7 @@ public class CronTrigger implements Trigger { private final CronExpression expression; + @Nullable private final ZoneId zoneId; @@ -48,7 +49,8 @@ public class CronTrigger implements Trigger { * expression conventions */ public CronTrigger(String expression) { - this(expression, ZoneId.systemDefault()); + this.expression = CronExpression.parse(expression); + this.zoneId = null; } /** @@ -58,7 +60,9 @@ public class CronTrigger implements Trigger { * @param timeZone a time zone in which the trigger times will be generated */ public CronTrigger(String expression, TimeZone timeZone) { - this(expression, timeZone.toZoneId()); + this.expression = CronExpression.parse(expression); + Assert.notNull(timeZone, "TimeZone must not be null"); + this.zoneId = timeZone.toZoneId(); } /** @@ -70,10 +74,8 @@ public class CronTrigger implements Trigger { * @see CronExpression#parse(String) */ 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); + Assert.notNull(zoneId, "ZoneId must not be null"); this.zoneId = zoneId; } @@ -94,22 +96,23 @@ public class CronTrigger implements Trigger { */ @Override public Date nextExecutionTime(TriggerContext triggerContext) { - Date date = triggerContext.lastCompletionTime(); - if (date != null) { + Date timestamp = triggerContext.lastCompletionTime(); + if (timestamp != null) { Date scheduled = triggerContext.lastScheduledExecutionTime(); - if (scheduled != null && date.before(scheduled)) { + if (scheduled != null && timestamp.before(scheduled)) { // Previous task apparently executed too early... // Let's simply use the last calculated execution time then, // in order to prevent accidental re-fires in the same second. - date = scheduled; + timestamp = scheduled; } } else { - date = new Date(triggerContext.getClock().millis()); + timestamp = new Date(triggerContext.getClock().millis()); } - ZonedDateTime dateTime = ZonedDateTime.ofInstant(date.toInstant(), this.zoneId); - ZonedDateTime next = this.expression.next(dateTime); - return (next != null ? Date.from(next.toInstant()) : null); + ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone()); + ZonedDateTime zonedTimestamp = ZonedDateTime.ofInstant(timestamp.toInstant(), zone); + ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp); + return (nextTimestamp != null ? Date.from(nextTimestamp.toInstant()) : null); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java index 1fe501b1301..14185a30096 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -848,6 +848,7 @@ class CronTriggerTests { assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime()); } + private static void roundup(Calendar calendar) { calendar.add(Calendar.SECOND, 1); calendar.set(Calendar.MILLISECOND, 0); @@ -861,9 +862,7 @@ class CronTriggerTests { } private static TriggerContext getTriggerContext(Date lastCompletionTime) { - SimpleTriggerContext context = new SimpleTriggerContext(); - context.update(null, null, lastCompletionTime); - return context; + return new SimpleTriggerContext(null, null, lastCompletionTime); }