Skip to content

Commit c791d26

Browse files
committed
Add flexible Duration value parsing in scheduled tasks
Closes spring-projectsgh-22013
1 parent c2367b3 commit c791d26

File tree

2 files changed

+132
-25
lines changed

2 files changed

+132
-25
lines changed

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

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
import java.lang.reflect.Method;
2020
import java.time.Duration;
21+
import java.time.temporal.ChronoUnit;
2122
import java.util.ArrayList;
2223
import java.util.Arrays;
2324
import java.util.Collection;
2425
import java.util.Collections;
26+
import java.util.HashMap;
2527
import java.util.IdentityHashMap;
2628
import java.util.LinkedHashSet;
2729
import java.util.List;
@@ -30,6 +32,8 @@
3032
import java.util.TimeZone;
3133
import java.util.concurrent.ConcurrentHashMap;
3234
import java.util.concurrent.ScheduledExecutorService;
35+
import java.util.regex.Matcher;
36+
import java.util.regex.Pattern;
3337

3438
import org.apache.commons.logging.Log;
3539
import org.apache.commons.logging.LogFactory;
@@ -115,6 +119,20 @@ public class ScheduledAnnotationBeanPostProcessor
115119
*/
116120
public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
117121

122+
private static Pattern DURATION_IN_TEXT_FORMAT = Pattern.compile("^([\\+\\-]?\\d+)([a-zA-Z]{1,2})$");
123+
124+
private static final Map<String, ChronoUnit> UNITS;
125+
126+
static {
127+
Map<String, ChronoUnit> units = new HashMap<>();
128+
units.put("ns", ChronoUnit.NANOS);
129+
units.put("ms", ChronoUnit.MILLIS);
130+
units.put("s", ChronoUnit.SECONDS);
131+
units.put("m", ChronoUnit.MINUTES);
132+
units.put("h", ChronoUnit.HOURS);
133+
units.put("d", ChronoUnit.DAYS);
134+
UNITS = Collections.unmodifiableMap(units);
135+
}
118136

119137
protected final Log logger = LogFactory.getLog(getClass());
120138

@@ -394,13 +412,9 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
394412
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
395413
}
396414
if (StringUtils.hasLength(initialDelayString)) {
397-
try {
398-
initialDelay = parseDelayAsLong(initialDelayString);
399-
}
400-
catch (RuntimeException ex) {
401-
throw new IllegalArgumentException(
402-
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
403-
}
415+
initialDelay = parseDelayAsLong(initialDelayString,
416+
"Invalid initialDelayString value \"" +
417+
initialDelayString + "\" - cannot parse into long");
404418
}
405419
}
406420

@@ -448,13 +462,9 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
448462
if (StringUtils.hasLength(fixedDelayString)) {
449463
Assert.isTrue(!processedSchedule, errorMessage);
450464
processedSchedule = true;
451-
try {
452-
fixedDelay = parseDelayAsLong(fixedDelayString);
453-
}
454-
catch (RuntimeException ex) {
455-
throw new IllegalArgumentException(
456-
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
457-
}
465+
fixedDelay = parseDelayAsLong(fixedDelayString,
466+
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
467+
458468
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
459469
}
460470
}
@@ -474,13 +484,9 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
474484
if (StringUtils.hasLength(fixedRateString)) {
475485
Assert.isTrue(!processedSchedule, errorMessage);
476486
processedSchedule = true;
477-
try {
478-
fixedRate = parseDelayAsLong(fixedRateString);
479-
}
480-
catch (RuntimeException ex) {
481-
throw new IllegalArgumentException(
482-
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
483-
}
487+
fixedRate = parseDelayAsLong(fixedRateString,
488+
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
489+
484490
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
485491
}
486492
}
@@ -515,11 +521,24 @@ protected Runnable createRunnable(Object target, Method method) {
515521
return new ScheduledMethodRunnable(target, invocableMethod);
516522
}
517523

518-
private static long parseDelayAsLong(String value) throws RuntimeException {
519-
if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) {
520-
return Duration.parse(value).toMillis();
524+
private static long parseDelayAsLong(String value, String exceptionMessage) throws IllegalArgumentException {
525+
try {
526+
if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) {
527+
return Duration.parse(value).toMillis();
528+
}
529+
return Long.parseLong(value);
530+
531+
}
532+
catch (RuntimeException ex) {
533+
Matcher matcher = DURATION_IN_TEXT_FORMAT.matcher(value);
534+
535+
Assert.isTrue(matcher.matches(), exceptionMessage);
536+
long amount = Long.parseLong(matcher.group(1));
537+
ChronoUnit unit = UNITS.get(matcher.group(2).toLowerCase());
538+
Assert.notNull(unit, exceptionMessage);
539+
540+
return Duration.of(amount, unit).toMillis();
521541
}
522-
return Long.parseLong(value);
523542
}
524543

525544
private static boolean isP(char ch) {

spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060

6161
import static org.assertj.core.api.Assertions.assertThat;
6262
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
63+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6364

6465
/**
6566
* @author Mark Fisher
@@ -107,6 +108,34 @@ public void fixedDelayTask() {
107108
assertThat(task.getInterval()).isEqualTo(5000L);
108109
}
109110

111+
@Test
112+
public void fixedDelayTaskInSimpleReadableForm() {
113+
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
114+
BeanDefinition targetDefinition = new RootBeanDefinition(FixedDelayInSimpleReadableFormTestBean.class);
115+
context.registerBeanDefinition("postProcessor", processorDefinition);
116+
context.registerBeanDefinition("target", targetDefinition);
117+
context.refresh();
118+
119+
ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class);
120+
assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1);
121+
122+
Object target = context.getBean("target");
123+
ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar)
124+
new DirectFieldAccessor(postProcessor).getPropertyValue("registrar");
125+
@SuppressWarnings("unchecked")
126+
List<IntervalTask> fixedDelayTasks = (List<IntervalTask>)
127+
new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks");
128+
assertThat(fixedDelayTasks.size()).isEqualTo(1);
129+
IntervalTask task = fixedDelayTasks.get(0);
130+
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
131+
Object targetObject = runnable.getTarget();
132+
Method targetMethod = runnable.getMethod();
133+
assertThat(targetObject).isEqualTo(target);
134+
assertThat(targetMethod.getName()).isEqualTo("fixedDelay");
135+
assertThat(task.getInitialDelay()).isEqualTo(0L);
136+
assertThat(task.getInterval()).isEqualTo(5000L);
137+
}
138+
110139
@Test
111140
public void fixedRateTask() {
112141
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
@@ -135,6 +164,44 @@ public void fixedRateTask() {
135164
assertThat(task.getInterval()).isEqualTo(3000L);
136165
}
137166

167+
@Test
168+
public void fixedRateTaskInSimpleReadableForm() {
169+
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
170+
BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateInSimpleReadableFormTestBean.class);
171+
context.registerBeanDefinition("postProcessor", processorDefinition);
172+
context.registerBeanDefinition("target", targetDefinition);
173+
context.refresh();
174+
175+
ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class);
176+
assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1);
177+
178+
Object target = context.getBean("target");
179+
ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar)
180+
new DirectFieldAccessor(postProcessor).getPropertyValue("registrar");
181+
@SuppressWarnings("unchecked")
182+
List<IntervalTask> fixedRateTasks = (List<IntervalTask>)
183+
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
184+
assertThat(fixedRateTasks.size()).isEqualTo(1);
185+
IntervalTask task = fixedRateTasks.get(0);
186+
ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable();
187+
Object targetObject = runnable.getTarget();
188+
Method targetMethod = runnable.getMethod();
189+
assertThat(targetObject).isEqualTo(target);
190+
assertThat(targetMethod.getName()).isEqualTo("fixedRate");
191+
assertThat(task.getInitialDelay()).isEqualTo(0L);
192+
assertThat(task.getInterval()).isEqualTo(3000L);
193+
}
194+
195+
@Test
196+
public void fixedRateTaskIncorrectSimpleReadableForm() {
197+
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
198+
BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateIncorrectSimpleReadableFormTestBean.class);
199+
context.registerBeanDefinition("postProcessor", processorDefinition);
200+
context.registerBeanDefinition("target", targetDefinition);
201+
assertThatThrownBy(context::refresh).isInstanceOf(BeanCreationException.class)
202+
.hasCauseInstanceOf(IllegalStateException.class);
203+
}
204+
138205
@Test
139206
public void fixedRateTaskWithInitialDelay() {
140207
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
@@ -702,6 +769,13 @@ public void fixedDelay() {
702769
}
703770
}
704771

772+
static class FixedDelayInSimpleReadableFormTestBean {
773+
774+
@Scheduled(fixedDelayString = "5s")
775+
public void fixedDelay() {
776+
}
777+
}
778+
705779

706780
static class FixedRateTestBean {
707781

@@ -710,6 +784,20 @@ public void fixedRate() {
710784
}
711785
}
712786

787+
static class FixedRateInSimpleReadableFormTestBean {
788+
789+
@Scheduled(fixedRateString = "3000ms")
790+
public void fixedRate() {
791+
}
792+
}
793+
794+
static class FixedRateIncorrectSimpleReadableFormTestBean {
795+
796+
@Scheduled(fixedRateString = "5az")
797+
public void fixedRate() {
798+
}
799+
}
800+
713801

714802
static class FixedRateWithInitialDelayTestBean {
715803

0 commit comments

Comments
 (0)