Browse Source

Default time zone resolution from scheduler-wide Clock

Closes gh-31948
pull/32173/head
Juergen Hoeller 2 years ago
parent
commit
c1db06af88
  1. 6
      spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java
  2. 13
      spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java
  3. 46
      spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java
  4. 31
      spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java
  5. 7
      spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java

6
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * 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)}, * @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 * @since 4.0
* @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone) * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone)
* @see java.util.TimeZone * @see java.util.TimeZone

13
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -84,7 +83,7 @@ import org.springframework.util.StringValueResolver;
* "fixedRate", "fixedDelay", or "cron" expression provided via the annotation. * "fixedRate", "fixedDelay", or "cron" expression provided via the annotation.
* *
* <p>This post-processor is automatically registered by Spring's * <p>This post-processor is automatically registered by Spring's
* {@code <task:annotation-driven>} XML element, and also by the * {@code <task:annotation-driven>} XML element and also by the
* {@link EnableScheduling @EnableScheduling} annotation. * {@link EnableScheduling @EnableScheduling} annotation.
* *
* <p>Autodetects any {@link SchedulingConfigurer} instances in the container, * <p>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"); Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true; processedSchedule = true;
if (!Scheduled.CRON_DISABLED.equals(cron)) { if (!Scheduled.CRON_DISABLED.equals(cron)) {
TimeZone timeZone; CronTrigger trigger;
if (StringUtils.hasText(zone)) { if (StringUtils.hasText(zone)) {
timeZone = StringUtils.parseTimeZoneString(zone); trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone));
} }
else { 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)));
} }
} }
} }

46
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 final String expression;
private CronExpression( private CronExpression(CronField seconds, CronField minutes, CronField hours,
CronField seconds, CronField daysOfMonth, CronField months, CronField daysOfWeek, String expression) {
CronField minutes,
CronField hours,
CronField daysOfMonth,
CronField months,
CronField daysOfWeek,
String expression) {
// reverse order, to make big changes first // Reverse order, to make big changes first.
// to make sure we end up at 0 nanos, we add an extra field // 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.fields = new CronField[] {daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()};
this.expression = expression; this.expression = expression;
} }
@ -121,11 +115,8 @@ public final class CronExpression {
* {@code LW}), it means "the last weekday of the month". * {@code LW}), it means "the last weekday of the month".
* </li> * </li>
* <li> * <li>
* In the "day of week" field, {@code L} stands for "the last day of the * In the "day of week" field, {@code dL} or {@code DDDL} stands for
* week". * "the last day of week {@code d} (or {@code DDD}) in the month".
* 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> * </li>
* </ul> * </ul>
* </li> * </li>
@ -177,7 +168,7 @@ public final class CronExpression {
* the cron format * the cron format
*/ */
public static CronExpression parse(String expression) { 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); expression = resolveMacros(expression);
@ -271,27 +262,18 @@ public final class CronExpression {
@Override @Override
public int hashCode() { public boolean equals(@Nullable Object other) {
return Arrays.hashCode(this.fields); return (this == other || (other instanceof CronExpression &&
Arrays.equals(this.fields, ((CronExpression) other).fields)));
} }
@Override @Override
public boolean equals(Object o) { public int hashCode() {
if (this == o) { return Arrays.hashCode(this.fields);
return true;
}
if (o instanceof CronExpression) {
CronExpression other = (CronExpression) o;
return Arrays.equals(this.fields, other.fields);
}
else {
return false;
}
} }
/** /**
* Return the expression string used to create this {@code CronExpression}. * Return the expression string used to create this {@code CronExpression}.
* @return the expression string
*/ */
@Override @Override
public String toString() { public String toString() {

31
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; private final CronExpression expression;
@Nullable
private final ZoneId zoneId; private final ZoneId zoneId;
@ -48,7 +49,8 @@ public class CronTrigger implements Trigger {
* expression conventions * expression conventions
*/ */
public CronTrigger(String expression) { 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 * @param timeZone a time zone in which the trigger times will be generated
*/ */
public CronTrigger(String expression, TimeZone timeZone) { 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) * @see CronExpression#parse(String)
*/ */
public CronTrigger(String expression, ZoneId zoneId) { 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.expression = CronExpression.parse(expression);
Assert.notNull(zoneId, "ZoneId must not be null");
this.zoneId = zoneId; this.zoneId = zoneId;
} }
@ -94,22 +96,23 @@ public class CronTrigger implements Trigger {
*/ */
@Override @Override
public Date nextExecutionTime(TriggerContext triggerContext) { public Date nextExecutionTime(TriggerContext triggerContext) {
Date date = triggerContext.lastCompletionTime(); Date timestamp = triggerContext.lastCompletionTime();
if (date != null) { if (timestamp != null) {
Date scheduled = triggerContext.lastScheduledExecutionTime(); Date scheduled = triggerContext.lastScheduledExecutionTime();
if (scheduled != null && date.before(scheduled)) { if (scheduled != null && timestamp.before(scheduled)) {
// Previous task apparently executed too early... // Previous task apparently executed too early...
// Let's simply use the last calculated execution time then, // Let's simply use the last calculated execution time then,
// in order to prevent accidental re-fires in the same second. // in order to prevent accidental re-fires in the same second.
date = scheduled; timestamp = scheduled;
} }
} }
else { else {
date = new Date(triggerContext.getClock().millis()); timestamp = new Date(triggerContext.getClock().millis());
} }
ZonedDateTime dateTime = ZonedDateTime.ofInstant(date.toInstant(), this.zoneId); ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone());
ZonedDateTime next = this.expression.next(dateTime); ZonedDateTime zonedTimestamp = ZonedDateTime.ofInstant(timestamp.toInstant(), zone);
return (next != null ? Date.from(next.toInstant()) : null); ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp);
return (nextTimestamp != null ? Date.from(nextTimestamp.toInstant()) : null);
} }

7
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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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()); assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime());
} }
private static void roundup(Calendar calendar) { private static void roundup(Calendar calendar) {
calendar.add(Calendar.SECOND, 1); calendar.add(Calendar.SECOND, 1);
calendar.set(Calendar.MILLISECOND, 0); calendar.set(Calendar.MILLISECOND, 0);
@ -861,9 +862,7 @@ class CronTriggerTests {
} }
private static TriggerContext getTriggerContext(Date lastCompletionTime) { private static TriggerContext getTriggerContext(Date lastCompletionTime) {
SimpleTriggerContext context = new SimpleTriggerContext(); return new SimpleTriggerContext(null, null, lastCompletionTime);
context.update(null, null, lastCompletionTime);
return context;
} }

Loading…
Cancel
Save