diff --git a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc
index c6e7af27a1..c03d59d397 100644
--- a/framework-docs/modules/ROOT/pages/integration/scheduling.adoc
+++ b/framework-docs/modules/ROOT/pages/integration/scheduling.adoc
@@ -346,7 +346,7 @@ For example, the previous example can also be written as follows.
If you need a fixed-rate execution, you can use the `fixedRate` attribute within the
annotation. The following method is invoked every five seconds (measured between the
-successive start times of each invocation).
+successive start times of each invocation):
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -356,9 +356,9 @@ successive start times of each invocation).
}
----
-For fixed-delay and fixed-rate tasks, you can specify an initial delay by indicating the
-amount of time to wait before the first execution of the method, as the following
-`fixedRate` example shows.
+For fixed-delay and fixed-rate tasks, you can specify an initial delay by indicating
+the amount of time to wait before the first execution of the method, as the following
+`fixedRate` example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -368,6 +368,17 @@ amount of time to wait before the first execution of the method, as the followin
}
----
+For one-time tasks, you can just specify an initial delay by indicating the amount
+of time to wait before the intended execution of the method:
+
+[source,java,indent=0,subs="verbatim,quotes"]
+----
+ @Scheduled(initialDelay = 1000)
+ public void doSomething() {
+ // something that should run only once
+ }
+----
+
If simple periodic scheduling is not expressive enough, you can provide a
xref:integration/scheduling.adoc#scheduling-cron-expression[cron expression].
The following example runs only on weekdays:
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 b9ba2081c0..43436644ca 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
@@ -28,9 +28,10 @@ import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
- * Annotation that marks a method to be scheduled. Exactly one of the
- * {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes
- * must be specified.
+ * Annotation that marks a method to be scheduled. For periodic tasks, exactly one
+ * of the {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes
+ * must be specified, and additionally an optional {@link #initialDelay}.
+ * For a one-time task, it is sufficient to just specify an {@link #initialDelay}.
*
*
The annotated method must not accept arguments. It will typically have
* a {@code void} return type; if not, the returned value will be ignored
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 ad70614a15..d584852d3c 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
@@ -65,6 +65,7 @@ import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.FixedDelayTask;
import org.springframework.scheduling.config.FixedRateTask;
+import org.springframework.scheduling.config.OneTimeTask;
import org.springframework.scheduling.config.ScheduledTask;
import org.springframework.scheduling.config.ScheduledTaskHolder;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@@ -393,8 +394,7 @@ public class ScheduledAnnotationBeanPostProcessor
private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) {
try {
boolean processedSchedule = false;
- String errorMessage =
- "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
+ String errorMessage = "Exactly one of the 'cron', 'fixedDelay' or 'fixedRate' attributes is required";
Set tasks = new LinkedHashSet<>(4);
@@ -442,18 +442,15 @@ public class ScheduledAnnotationBeanPostProcessor
}
// At this point we don't need to differentiate between initial delay set or not anymore
- if (initialDelay.isNegative()) {
- initialDelay = Duration.ZERO;
- }
+ Duration delayToUse = (initialDelay.isNegative() ? Duration.ZERO : initialDelay);
// Check fixed delay
Duration fixedDelay = toDuration(scheduled.fixedDelay(), scheduled.timeUnit());
if (!fixedDelay.isNegative()) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
- tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
+ tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
}
-
String fixedDelayString = scheduled.fixedDelayString();
if (StringUtils.hasText(fixedDelayString)) {
if (this.embeddedValueResolver != null) {
@@ -469,7 +466,7 @@ public class ScheduledAnnotationBeanPostProcessor
throw new IllegalArgumentException(
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
}
- tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
+ tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
}
}
@@ -478,7 +475,7 @@ public class ScheduledAnnotationBeanPostProcessor
if (!fixedRate.isNegative()) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
- tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
+ tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
}
String fixedRateString = scheduled.fixedRateString();
if (StringUtils.hasText(fixedRateString)) {
@@ -495,12 +492,16 @@ public class ScheduledAnnotationBeanPostProcessor
throw new IllegalArgumentException(
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
}
- tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
+ tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
}
}
- // Check whether we had any attribute set
- Assert.isTrue(processedSchedule, errorMessage);
+ if (!processedSchedule) {
+ if (initialDelay.isNegative()) {
+ throw new IllegalArgumentException("One-time task only supported with specified initial delay");
+ }
+ tasks.add(this.registrar.scheduleOneTimeTask(new OneTimeTask(runnable, delayToUse)));
+ }
// Finally register the scheduled tasks
synchronized (this.scheduledTasks) {
@@ -548,7 +549,13 @@ public class ScheduledAnnotationBeanPostProcessor
}
private static Duration toDuration(long value, TimeUnit timeUnit) {
- return Duration.of(value, timeUnit.toChronoUnit());
+ try {
+ return Duration.of(value, timeUnit.toChronoUnit());
+ }
+ catch (Exception ex) {
+ throw new IllegalArgumentException(
+ "Unsupported unit " + timeUnit + " for value \"" + value + "\": " + ex.getMessage(), ex);
+ }
}
private static Duration toDuration(String value, TimeUnit timeUnit) {
diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/DelayedTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/DelayedTask.java
new file mode 100644
index 0000000000..497db7b110
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/scheduling/config/DelayedTask.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2002-2023 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.config;
+
+import java.time.Duration;
+
+import org.springframework.util.Assert;
+
+/**
+ * {@link Task} implementation defining a {@code Runnable} with an initial delay.
+ *
+ * @author Juergen Hoeller
+ * @since 6.1
+ */
+public class DelayedTask extends Task {
+
+ private final Duration initialDelay;
+
+
+ /**
+ * Create a new {@code DelayedTask}.
+ * @param runnable the underlying task to execute
+ * @param initialDelay the initial delay before execution of the task
+ */
+ public DelayedTask(Runnable runnable, Duration initialDelay) {
+ super(runnable);
+ Assert.notNull(initialDelay, "InitialDelay must not be null");
+ this.initialDelay = initialDelay;
+ }
+
+ /**
+ * Copy constructor.
+ */
+ DelayedTask(DelayedTask task) {
+ super(task.getRunnable());
+ Assert.notNull(task, "DelayedTask must not be null");
+ this.initialDelay = task.getInitialDelayDuration();
+ }
+
+
+ /**
+ * Return the initial delay before first execution of the task.
+ */
+ public Duration getInitialDelayDuration() {
+ return this.initialDelay;
+ }
+
+}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java
index 99350a1e3b..9e4e28190e 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java
@@ -56,5 +56,4 @@ public class FixedRateTask extends IntervalTask {
super(task);
}
-
}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java
index a0a44146ba..782053fac8 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -31,12 +31,10 @@ import org.springframework.util.Assert;
* @see ScheduledTaskRegistrar#addFixedRateTask(IntervalTask)
* @see ScheduledTaskRegistrar#addFixedDelayTask(IntervalTask)
*/
-public class IntervalTask extends Task {
+public class IntervalTask extends DelayedTask {
private final Duration interval;
- private final Duration initialDelay;
-
/**
* Create a new {@code IntervalTask}.
@@ -79,28 +77,20 @@ public class IntervalTask extends Task {
* @since 6.0
*/
public IntervalTask(Runnable runnable, Duration interval, Duration initialDelay) {
- super(runnable);
+ super(runnable, initialDelay);
Assert.notNull(interval, "Interval must not be null");
- Assert.notNull(initialDelay, "InitialDelay must not be null");
-
this.interval = interval;
- this.initialDelay = initialDelay;
}
/**
* Copy constructor.
*/
IntervalTask(IntervalTask task) {
- super(task.getRunnable());
- Assert.notNull(task, "IntervalTask must not be null");
-
+ super(task);
this.interval = task.getIntervalDuration();
- this.initialDelay = task.getInitialDelayDuration();
}
-
-
/**
* Return how often in milliseconds the task should be executed.
* @deprecated as of 6.0, in favor of {@link #getIntervalDuration()}
@@ -124,15 +114,16 @@ public class IntervalTask extends Task {
*/
@Deprecated(since = "6.0")
public long getInitialDelay() {
- return this.initialDelay.toMillis();
+ return getInitialDelayDuration().toMillis();
}
/**
* Return the initial delay before first execution of the task.
* @since 6.0
*/
+ @Override
public Duration getInitialDelayDuration() {
- return this.initialDelay;
+ return super.getInitialDelayDuration();
}
}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/OneTimeTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/OneTimeTask.java
new file mode 100644
index 0000000000..e71d8d91ca
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/scheduling/config/OneTimeTask.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2002-2023 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.config;
+
+import java.time.Duration;
+
+/**
+ * {@link Task} implementation defining a {@code Runnable} with an initial delay.
+ *
+ * @author Juergen Hoeller
+ * @since 6.1
+ * @see ScheduledTaskRegistrar#addOneTimeTask(DelayedTask)
+ */
+public class OneTimeTask extends DelayedTask {
+
+ /**
+ * Create a new {@code DelayedTask}.
+ * @param runnable the underlying task to execute
+ * @param initialDelay the initial delay before execution of the task
+ */
+ public OneTimeTask(Runnable runnable, Duration initialDelay) {
+ super(runnable, initialDelay);
+ }
+
+ OneTimeTask(DelayedTask task) {
+ super(task);
+ }
+
+}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java
index 706e1cad33..02cadb58f5 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java
@@ -95,6 +95,9 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
@Nullable
private List fixedDelayTasks;
+ @Nullable
+ private List oneTimeTasks;
+
private final Map unresolvedTasks = new HashMap<>(16);
private final Set scheduledTasks = new LinkedHashSet<>(16);
@@ -136,6 +139,14 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
return this.taskScheduler;
}
+ /**
+ * Configure an {@link ObservationRegistry} to record observations for scheduled tasks.
+ * @since 6.1
+ */
+ public void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) {
+ this.observationRegistry = observationRegistry;
+ }
+
/**
* Return the {@link ObservationRegistry} for this registrar.
* @since 6.1
@@ -145,14 +156,6 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
return this.observationRegistry;
}
- /**
- * Configure an {@link ObservationRegistry} to record observations for scheduled tasks.
- * @since 6.1
- */
- public void setObservationRegistry(@Nullable ObservationRegistry observationRegistry) {
- this.observationRegistry = observationRegistry;
- }
-
/**
* Specify triggered tasks as a Map of Runnables (the tasks) and Trigger objects
* (typically custom implementations of the {@link Trigger} interface).
@@ -318,7 +321,7 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
*/
@Deprecated(since = "6.0")
public void addFixedRateTask(Runnable task, long interval) {
- addFixedRateTask(new IntervalTask(task, Duration.ofMillis(interval)));
+ addFixedRateTask(new IntervalTask(task, interval));
}
/**
@@ -334,6 +337,7 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
* Add a fixed-rate {@link IntervalTask}.
* @since 3.2
* @see TaskScheduler#scheduleAtFixedRate(Runnable, Duration)
+ * @see FixedRateTask
*/
public void addFixedRateTask(IntervalTask task) {
if (this.fixedRateTasks == null) {
@@ -347,8 +351,8 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
* @deprecated as of 6.0, in favor of {@link #addFixedDelayTask(Runnable, Duration)}
*/
@Deprecated(since = "6.0")
- public void addFixedDelayTask(Runnable task, long delay) {
- addFixedDelayTask(new IntervalTask(task, Duration.ofMillis(delay)));
+ public void addFixedDelayTask(Runnable task, long interval) {
+ addFixedDelayTask(new IntervalTask(task, interval));
}
/**
@@ -356,14 +360,15 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
* @since 6.0
* @see TaskScheduler#scheduleWithFixedDelay(Runnable, Duration)
*/
- public void addFixedDelayTask(Runnable task, Duration delay) {
- addFixedDelayTask(new IntervalTask(task, delay));
+ public void addFixedDelayTask(Runnable task, Duration interval) {
+ addFixedDelayTask(new IntervalTask(task, interval));
}
/**
* Add a fixed-delay {@link IntervalTask}.
* @since 3.2
* @see TaskScheduler#scheduleWithFixedDelay(Runnable, Duration)
+ * @see FixedDelayTask
*/
public void addFixedDelayTask(IntervalTask task) {
if (this.fixedDelayTasks == null) {
@@ -372,6 +377,28 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
this.fixedDelayTasks.add(task);
}
+ /**
+ * Add a Runnable task to be triggered once after the given initial delay.
+ * @since 6.1
+ * @see TaskScheduler#schedule(Runnable, Instant)
+ */
+ public void addOneTimeTask(Runnable task, Duration initialDelay) {
+ addOneTimeTask(new OneTimeTask(task, initialDelay));
+ }
+
+ /**
+ * Add a one-time {@link DelayedTask}.
+ * @since 6.1
+ * @see TaskScheduler#schedule(Runnable, Instant)
+ * @see OneTimeTask
+ */
+ public void addOneTimeTask(DelayedTask task) {
+ if (this.oneTimeTasks == null) {
+ this.oneTimeTasks = new ArrayList<>();
+ }
+ this.oneTimeTasks.add(task);
+ }
+
/**
* Return whether this {@code ScheduledTaskRegistrar} has any tasks registered.
@@ -381,7 +408,8 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
return (!CollectionUtils.isEmpty(this.triggerTasks) ||
!CollectionUtils.isEmpty(this.cronTasks) ||
!CollectionUtils.isEmpty(this.fixedRateTasks) ||
- !CollectionUtils.isEmpty(this.fixedDelayTasks));
+ !CollectionUtils.isEmpty(this.fixedDelayTasks) ||
+ !CollectionUtils.isEmpty(this.oneTimeTasks));
}
@@ -432,6 +460,16 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
}
}
}
+ if (this.oneTimeTasks != null) {
+ for (DelayedTask task : this.oneTimeTasks) {
+ if (task instanceof OneTimeTask oneTimeTask) {
+ addScheduledTask(scheduleOneTimeTask(oneTimeTask));
+ }
+ else {
+ addScheduledTask(scheduleOneTimeTask(new OneTimeTask(task)));
+ }
+ }
+ }
}
private void addScheduledTask(@Nullable ScheduledTask task) {
@@ -558,6 +596,32 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing
return (newTask ? scheduledTask : null);
}
+ /**
+ * Schedule the specified one-time task, either right away if possible
+ * or on initialization of the scheduler.
+ * @return a handle to the scheduled task, allowing to cancel it
+ * (or {@code null} if processing a previously registered task)
+ * @since 6.1
+ */
+ @Nullable
+ public ScheduledTask scheduleOneTimeTask(OneTimeTask task) {
+ ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
+ boolean newTask = false;
+ if (scheduledTask == null) {
+ scheduledTask = new ScheduledTask(task);
+ newTask = true;
+ }
+ if (this.taskScheduler != null) {
+ Instant startTime = this.taskScheduler.getClock().instant().plus(task.getInitialDelayDuration());
+ scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), startTime);
+ }
+ else {
+ addOneTimeTask(task);
+ this.unresolvedTasks.put(task, scheduledTask);
+ }
+ return (newTask ? scheduledTask : null);
+ }
+
/**
* Return all locally registered tasks that have been scheduled by this registrar.
diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java
index aa3a1d5f06..1d990a7a63 100644
--- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java
+++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java
@@ -218,15 +218,6 @@ public class EnableSchedulingTests {
assertThat(ctx.getBean(ThreadAwareWorker.class).executedByThread).startsWith("taskScheduler-");
}
- @Test
- @EnabledForTestGroups(LONG_RUNNING)
- public void withTriggerTask() throws InterruptedException {
- ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class);
-
- Thread.sleep(110);
- assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThan(1);
- }
-
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void withInitiallyDelayedFixedRateTask() throws InterruptedException {
@@ -235,9 +226,30 @@ public class EnableSchedulingTests {
Thread.sleep(1950);
AtomicInteger counter = ctx.getBean(AtomicInteger.class);
- // The @Scheduled method should have been called at least once but
- // not more times than the delay allows.
- assertThat(counter.get()).isBetween(1, 10);
+ // The @Scheduled method should have been called several times
+ // but not more times than the delay allows.
+ assertThat(counter.get()).isBetween(2, 10);
+ }
+
+ @Test
+ @EnabledForTestGroups(LONG_RUNNING)
+ public void withOneTimeTask() throws InterruptedException {
+ ctx = new AnnotationConfigApplicationContext(OneTimeTaskConfig.class);
+
+ Thread.sleep(110);
+ AtomicInteger counter = ctx.getBean(AtomicInteger.class);
+
+ // The @Scheduled method should have been called exactly once.
+ assertThat(counter.get()).isEqualTo(1);
+ }
+
+ @Test
+ @EnabledForTestGroups(LONG_RUNNING)
+ public void withTriggerTask() throws InterruptedException {
+ ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class);
+
+ Thread.sleep(110);
+ assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThan(1);
}
@@ -598,6 +610,38 @@ public class EnableSchedulingTests {
}
+ @Configuration
+ @EnableScheduling
+ static class FixedRateTaskConfig_withInitialDelay {
+
+ @Bean
+ public AtomicInteger counter() {
+ return new AtomicInteger();
+ }
+
+ @Scheduled(initialDelay = 1000, fixedRate = 100)
+ public void task() {
+ counter().incrementAndGet();
+ }
+ }
+
+
+ @Configuration
+ @EnableScheduling
+ static class OneTimeTaskConfig {
+
+ @Bean
+ public AtomicInteger counter() {
+ return new AtomicInteger();
+ }
+
+ @Scheduled(initialDelay = 10)
+ public void task() {
+ counter().incrementAndGet();
+ }
+ }
+
+
@Configuration
static class TriggerTaskConfig {
@@ -616,20 +660,4 @@ public class EnableSchedulingTests {
}
}
-
- @Configuration
- @EnableScheduling
- static class FixedRateTaskConfig_withInitialDelay {
-
- @Bean
- public AtomicInteger counter() {
- return new AtomicInteger();
- }
-
- @Scheduled(initialDelay = 1000, fixedRate = 100)
- public void task() {
- counter().incrementAndGet();
- }
- }
-
}
diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java
index 417de9d825..90dececdce 100644
--- a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java
+++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java
@@ -57,6 +57,7 @@ import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.IntervalTask;
+import org.springframework.scheduling.config.OneTimeTask;
import org.springframework.scheduling.config.ScheduledTaskHolder;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
@@ -266,6 +267,33 @@ class ScheduledAnnotationBeanPostProcessorTests {
assertThat(task2.getIntervalDuration()).isEqualTo(Duration.ofMillis(4_000L));
}
+ @Test
+ void oneTimeTask() {
+ BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
+ BeanDefinition targetDefinition = new RootBeanDefinition(OneTimeTaskBean.class);
+ context.registerBeanDefinition("postProcessor", processorDefinition);
+ context.registerBeanDefinition("target", targetDefinition);
+ context.refresh();
+
+ ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class);
+ assertThat(postProcessor.getScheduledTasks()).hasSize(1);
+
+ Object target = context.getBean("target");
+ ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar)
+ new DirectFieldAccessor(postProcessor).getPropertyValue("registrar");
+ @SuppressWarnings("unchecked")
+ List oneTimeTasks = (List)
+ new DirectFieldAccessor(registrar).getPropertyValue("oneTimeTasks");
+ assertThat(oneTimeTasks).hasSize(1);
+ OneTimeTask task = oneTimeTasks.get(0);
+ ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
+ Object targetObject = runnable.getTarget();
+ Method targetMethod = runnable.getMethod();
+ assertThat(targetObject).isEqualTo(target);
+ assertThat(targetMethod.getName()).isEqualTo("oneTimeTask");
+ assertThat(task.getInitialDelayDuration()).isEqualTo(Duration.ofMillis(2_000L));
+ }
+
@Test
void cronTask() {
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
@@ -848,6 +876,14 @@ class ScheduledAnnotationBeanPostProcessorTests {
}
+ static class OneTimeTaskBean {
+
+ @Scheduled(initialDelay = 2_000)
+ private void oneTimeTask() {
+ }
+ }
+
+
@Validated
static class CronTestBean {