Skip to content

Commit 8f6c56f

Browse files
committed
Support for one-time tasks with just @scheduled(initialDelay=...)
Closes gh-31211
1 parent e63e3a6 commit 8f6c56f

File tree

10 files changed

+312
-70
lines changed

10 files changed

+312
-70
lines changed

Diff for: framework-docs/modules/ROOT/pages/integration/scheduling.adoc

+15-4
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ For example, the previous example can also be written as follows.
346346

347347
If you need a fixed-rate execution, you can use the `fixedRate` attribute within the
348348
annotation. The following method is invoked every five seconds (measured between the
349-
successive start times of each invocation).
349+
successive start times of each invocation):
350350

351351
[source,java,indent=0,subs="verbatim,quotes"]
352352
----
@@ -356,9 +356,9 @@ successive start times of each invocation).
356356
}
357357
----
358358

359-
For fixed-delay and fixed-rate tasks, you can specify an initial delay by indicating the
360-
amount of time to wait before the first execution of the method, as the following
361-
`fixedRate` example shows.
359+
For fixed-delay and fixed-rate tasks, you can specify an initial delay by indicating
360+
the amount of time to wait before the first execution of the method, as the following
361+
`fixedRate` example shows:
362362

363363
[source,java,indent=0,subs="verbatim,quotes"]
364364
----
@@ -368,6 +368,17 @@ amount of time to wait before the first execution of the method, as the followin
368368
}
369369
----
370370

371+
For one-time tasks, you can just specify an initial delay by indicating the amount
372+
of time to wait before the intended execution of the method:
373+
374+
[source,java,indent=0,subs="verbatim,quotes"]
375+
----
376+
@Scheduled(initialDelay = 1000)
377+
public void doSomething() {
378+
// something that should run only once
379+
}
380+
----
381+
371382
If simple periodic scheduling is not expressive enough, you can provide a
372383
xref:integration/scheduling.adoc#scheduling-cron-expression[cron expression].
373384
The following example runs only on weekdays:

Diff for: spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
2929

3030
/**
31-
* Annotation that marks a method to be scheduled. Exactly one of the
32-
* {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes
33-
* must be specified.
31+
* Annotation that marks a method to be scheduled. For periodic tasks, exactly one
32+
* of the {@link #cron}, {@link #fixedDelay}, or {@link #fixedRate} attributes
33+
* must be specified, and additionally an optional {@link #initialDelay}.
34+
* For a one-time task, it is sufficient to just specify an {@link #initialDelay}.
3435
*
3536
* <p>The annotated method must not accept arguments. It will typically have
3637
* a {@code void} return type; if not, the returned value will be ignored

Diff for: spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java

+20-13
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import org.springframework.scheduling.config.CronTask;
6666
import org.springframework.scheduling.config.FixedDelayTask;
6767
import org.springframework.scheduling.config.FixedRateTask;
68+
import org.springframework.scheduling.config.OneTimeTask;
6869
import org.springframework.scheduling.config.ScheduledTask;
6970
import org.springframework.scheduling.config.ScheduledTaskHolder;
7071
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@@ -393,8 +394,7 @@ private void processScheduledAsync(Scheduled scheduled, Method method, Object be
393394
private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method method, Object bean) {
394395
try {
395396
boolean processedSchedule = false;
396-
String errorMessage =
397-
"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
397+
String errorMessage = "Exactly one of the 'cron', 'fixedDelay' or 'fixedRate' attributes is required";
398398

399399
Set<ScheduledTask> tasks = new LinkedHashSet<>(4);
400400

@@ -442,18 +442,15 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method
442442
}
443443

444444
// At this point we don't need to differentiate between initial delay set or not anymore
445-
if (initialDelay.isNegative()) {
446-
initialDelay = Duration.ZERO;
447-
}
445+
Duration delayToUse = (initialDelay.isNegative() ? Duration.ZERO : initialDelay);
448446

449447
// Check fixed delay
450448
Duration fixedDelay = toDuration(scheduled.fixedDelay(), scheduled.timeUnit());
451449
if (!fixedDelay.isNegative()) {
452450
Assert.isTrue(!processedSchedule, errorMessage);
453451
processedSchedule = true;
454-
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
452+
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
455453
}
456-
457454
String fixedDelayString = scheduled.fixedDelayString();
458455
if (StringUtils.hasText(fixedDelayString)) {
459456
if (this.embeddedValueResolver != null) {
@@ -469,7 +466,7 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method
469466
throw new IllegalArgumentException(
470467
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
471468
}
472-
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
469+
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
473470
}
474471
}
475472

@@ -478,7 +475,7 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method
478475
if (!fixedRate.isNegative()) {
479476
Assert.isTrue(!processedSchedule, errorMessage);
480477
processedSchedule = true;
481-
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
478+
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
482479
}
483480
String fixedRateString = scheduled.fixedRateString();
484481
if (StringUtils.hasText(fixedRateString)) {
@@ -495,12 +492,16 @@ private void processScheduledTask(Scheduled scheduled, Runnable runnable, Method
495492
throw new IllegalArgumentException(
496493
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
497494
}
498-
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
495+
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, delayToUse)));
499496
}
500497
}
501498

502-
// Check whether we had any attribute set
503-
Assert.isTrue(processedSchedule, errorMessage);
499+
if (!processedSchedule) {
500+
if (initialDelay.isNegative()) {
501+
throw new IllegalArgumentException("One-time task only supported with specified initial delay");
502+
}
503+
tasks.add(this.registrar.scheduleOneTimeTask(new OneTimeTask(runnable, delayToUse)));
504+
}
504505

505506
// Finally register the scheduled tasks
506507
synchronized (this.scheduledTasks) {
@@ -548,7 +549,13 @@ protected Runnable createRunnable(Object target, Method method) {
548549
}
549550

550551
private static Duration toDuration(long value, TimeUnit timeUnit) {
551-
return Duration.of(value, timeUnit.toChronoUnit());
552+
try {
553+
return Duration.of(value, timeUnit.toChronoUnit());
554+
}
555+
catch (Exception ex) {
556+
throw new IllegalArgumentException(
557+
"Unsupported unit " + timeUnit + " for value \"" + value + "\": " + ex.getMessage(), ex);
558+
}
552559
}
553560

554561
private static Duration toDuration(String value, TimeUnit timeUnit) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.config;
18+
19+
import java.time.Duration;
20+
21+
import org.springframework.util.Assert;
22+
23+
/**
24+
* {@link Task} implementation defining a {@code Runnable} with an initial delay.
25+
*
26+
* @author Juergen Hoeller
27+
* @since 6.1
28+
*/
29+
public class DelayedTask extends Task {
30+
31+
private final Duration initialDelay;
32+
33+
34+
/**
35+
* Create a new {@code DelayedTask}.
36+
* @param runnable the underlying task to execute
37+
* @param initialDelay the initial delay before execution of the task
38+
*/
39+
public DelayedTask(Runnable runnable, Duration initialDelay) {
40+
super(runnable);
41+
Assert.notNull(initialDelay, "InitialDelay must not be null");
42+
this.initialDelay = initialDelay;
43+
}
44+
45+
/**
46+
* Copy constructor.
47+
*/
48+
DelayedTask(DelayedTask task) {
49+
super(task.getRunnable());
50+
Assert.notNull(task, "DelayedTask must not be null");
51+
this.initialDelay = task.getInitialDelayDuration();
52+
}
53+
54+
55+
/**
56+
* Return the initial delay before first execution of the task.
57+
*/
58+
public Duration getInitialDelayDuration() {
59+
return this.initialDelay;
60+
}
61+
62+
}

Diff for: spring-context/src/main/java/org/springframework/scheduling/config/FixedRateTask.java

-1
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,4 @@ public FixedRateTask(Runnable runnable, Duration interval, Duration initialDelay
5656
super(task);
5757
}
5858

59-
6059
}

Diff for: spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java

+7-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,12 +31,10 @@
3131
* @see ScheduledTaskRegistrar#addFixedRateTask(IntervalTask)
3232
* @see ScheduledTaskRegistrar#addFixedDelayTask(IntervalTask)
3333
*/
34-
public class IntervalTask extends Task {
34+
public class IntervalTask extends DelayedTask {
3535

3636
private final Duration interval;
3737

38-
private final Duration initialDelay;
39-
4038

4139
/**
4240
* Create a new {@code IntervalTask}.
@@ -79,28 +77,20 @@ public IntervalTask(Runnable runnable, Duration interval) {
7977
* @since 6.0
8078
*/
8179
public IntervalTask(Runnable runnable, Duration interval, Duration initialDelay) {
82-
super(runnable);
80+
super(runnable, initialDelay);
8381
Assert.notNull(interval, "Interval must not be null");
84-
Assert.notNull(initialDelay, "InitialDelay must not be null");
85-
8682
this.interval = interval;
87-
this.initialDelay = initialDelay;
8883
}
8984

9085
/**
9186
* Copy constructor.
9287
*/
9388
IntervalTask(IntervalTask task) {
94-
super(task.getRunnable());
95-
Assert.notNull(task, "IntervalTask must not be null");
96-
89+
super(task);
9790
this.interval = task.getIntervalDuration();
98-
this.initialDelay = task.getInitialDelayDuration();
9991
}
10092

10193

102-
103-
10494
/**
10595
* Return how often in milliseconds the task should be executed.
10696
* @deprecated as of 6.0, in favor of {@link #getIntervalDuration()}
@@ -124,15 +114,16 @@ public Duration getIntervalDuration() {
124114
*/
125115
@Deprecated(since = "6.0")
126116
public long getInitialDelay() {
127-
return this.initialDelay.toMillis();
117+
return getInitialDelayDuration().toMillis();
128118
}
129119

130120
/**
131121
* Return the initial delay before first execution of the task.
132122
* @since 6.0
133123
*/
124+
@Override
134125
public Duration getInitialDelayDuration() {
135-
return this.initialDelay;
126+
return super.getInitialDelayDuration();
136127
}
137128

138129
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.config;
18+
19+
import java.time.Duration;
20+
21+
/**
22+
* {@link Task} implementation defining a {@code Runnable} with an initial delay.
23+
*
24+
* @author Juergen Hoeller
25+
* @since 6.1
26+
* @see ScheduledTaskRegistrar#addOneTimeTask(DelayedTask)
27+
*/
28+
public class OneTimeTask extends DelayedTask {
29+
30+
/**
31+
* Create a new {@code DelayedTask}.
32+
* @param runnable the underlying task to execute
33+
* @param initialDelay the initial delay before execution of the task
34+
*/
35+
public OneTimeTask(Runnable runnable, Duration initialDelay) {
36+
super(runnable, initialDelay);
37+
}
38+
39+
OneTimeTask(DelayedTask task) {
40+
super(task);
41+
}
42+
43+
}

0 commit comments

Comments
 (0)