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 {