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 f03d55f46eff..82ab2ddeec57 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 @@ -18,10 +18,12 @@ import java.lang.reflect.Method; import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -30,6 +32,8 @@ import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -115,6 +119,20 @@ public class ScheduledAnnotationBeanPostProcessor */ public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler"; + private static Pattern DURATION_IN_TEXT_FORMAT = Pattern.compile("^([\\+\\-]?\\d+)([a-zA-Z]{1,2})$"); + + private static final Map UNITS; + + static { + Map units = new HashMap<>(); + units.put("ns", ChronoUnit.NANOS); + units.put("ms", ChronoUnit.MILLIS); + units.put("s", ChronoUnit.SECONDS); + units.put("m", ChronoUnit.MINUTES); + units.put("h", ChronoUnit.HOURS); + units.put("d", ChronoUnit.DAYS); + UNITS = Collections.unmodifiableMap(units); + } protected final Log logger = LogFactory.getLog(getClass()); @@ -394,13 +412,9 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString); } if (StringUtils.hasLength(initialDelayString)) { - try { - initialDelay = parseDelayAsLong(initialDelayString); - } - catch (RuntimeException ex) { - throw new IllegalArgumentException( - "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long"); - } + initialDelay = parseDelayAsLong(initialDelayString, + "Invalid initialDelayString value \"" + + initialDelayString + "\" - cannot parse into long"); } } @@ -448,13 +462,9 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) if (StringUtils.hasLength(fixedDelayString)) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - try { - fixedDelay = parseDelayAsLong(fixedDelayString); - } - catch (RuntimeException ex) { - throw new IllegalArgumentException( - "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long"); - } + fixedDelay = parseDelayAsLong(fixedDelayString, + "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long"); + tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); } } @@ -474,13 +484,9 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) if (StringUtils.hasLength(fixedRateString)) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - try { - fixedRate = parseDelayAsLong(fixedRateString); - } - catch (RuntimeException ex) { - throw new IllegalArgumentException( - "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long"); - } + fixedRate = parseDelayAsLong(fixedRateString, + "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long"); + tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay))); } } @@ -515,11 +521,24 @@ protected Runnable createRunnable(Object target, Method method) { return new ScheduledMethodRunnable(target, invocableMethod); } - private static long parseDelayAsLong(String value) throws RuntimeException { - if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) { - return Duration.parse(value).toMillis(); + private static long parseDelayAsLong(String value, String exceptionMessage) throws IllegalArgumentException { + try { + if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) { + return Duration.parse(value).toMillis(); + } + return Long.parseLong(value); + + } + catch (RuntimeException ex) { + Matcher matcher = DURATION_IN_TEXT_FORMAT.matcher(value); + + Assert.isTrue(matcher.matches(), exceptionMessage); + long amount = Long.parseLong(matcher.group(1)); + ChronoUnit unit = UNITS.get(matcher.group(2).toLowerCase()); + Assert.notNull(unit, exceptionMessage); + + return Duration.of(amount, unit).toMillis(); } - return Long.parseLong(value); } private static boolean isP(char ch) { 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 842910070227..f7f3866574a1 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 @@ -60,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Mark Fisher @@ -107,6 +108,34 @@ public void fixedDelayTask() { assertThat(task.getInterval()).isEqualTo(5000L); } + @Test + public void fixedDelayTaskInSimpleReadableForm() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedDelayInSimpleReadableFormTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedDelayTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks"); + assertThat(fixedDelayTasks.size()).isEqualTo(1); + IntervalTask task = fixedDelayTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedDelay"); + assertThat(task.getInitialDelay()).isEqualTo(0L); + assertThat(task.getInterval()).isEqualTo(5000L); + } + @Test public void fixedRateTask() { BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); @@ -135,6 +164,44 @@ public void fixedRateTask() { assertThat(task.getInterval()).isEqualTo(3000L); } + @Test + public void fixedRateTaskInSimpleReadableForm() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateInSimpleReadableFormTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertThat(postProcessor.getScheduledTasks().size()).isEqualTo(1); + + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertThat(fixedRateTasks.size()).isEqualTo(1); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertThat(targetObject).isEqualTo(target); + assertThat(targetMethod.getName()).isEqualTo("fixedRate"); + assertThat(task.getInitialDelay()).isEqualTo(0L); + assertThat(task.getInterval()).isEqualTo(3000L); + } + + @Test + public void fixedRateTaskIncorrectSimpleReadableForm() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateIncorrectSimpleReadableFormTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + assertThatThrownBy(context::refresh).isInstanceOf(BeanCreationException.class) + .hasCauseInstanceOf(IllegalStateException.class); + } + @Test public void fixedRateTaskWithInitialDelay() { BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); @@ -702,6 +769,13 @@ public void fixedDelay() { } } + static class FixedDelayInSimpleReadableFormTestBean { + + @Scheduled(fixedDelayString = "5s") + public void fixedDelay() { + } + } + static class FixedRateTestBean { @@ -710,6 +784,20 @@ public void fixedRate() { } } + static class FixedRateInSimpleReadableFormTestBean { + + @Scheduled(fixedRateString = "3000ms") + public void fixedRate() { + } + } + + static class FixedRateIncorrectSimpleReadableFormTestBean { + + @Scheduled(fixedRateString = "5az") + public void fixedRate() { + } + } + static class FixedRateWithInitialDelayTestBean {