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 415c72381792..aa067ad0da46 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 @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; import org.springframework.scheduling.config.ScheduledTaskRegistrar; @@ -101,52 +102,64 @@ String zone() default ""; /** - * Execute the annotated method with a fixed period in milliseconds between the + * Execute the annotated method with a fixed period between the * end of the last invocation and the start of the next. - * @return the delay in milliseconds + * Using milliseconds by default with timeUnit(). + * @return the delay */ long fixedDelay() default -1; /** - * Execute the annotated method with a fixed period in milliseconds between the + * Execute the annotated method with a fixed period between the * end of the last invocation and the start of the next. - * @return the delay in milliseconds as a String value, e.g. a placeholder + * Using milliseconds by default with fixedDelayTimeUnit(). + * @return the delay as a String value, e.g. a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 */ String fixedDelayString() default ""; /** - * Execute the annotated method with a fixed period in milliseconds between + * Execute the annotated method with a fixed period between * invocations. - * @return the period in milliseconds + * Using milliseconds by default with timeUnit(). + * @return the period */ long fixedRate() default -1; /** - * Execute the annotated method with a fixed period in milliseconds between + * Execute the annotated method with a fixed period between * invocations. - * @return the period in milliseconds as a String value, e.g. a placeholder + * Using milliseconds by default with fixedRateTimeUnit(). + * @return the period as a String value, e.g. a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 */ String fixedRateString() default ""; /** - * Number of milliseconds to delay before the first execution of a + * Number to delay before the first execution of a * {@link #fixedRate} or {@link #fixedDelay} task. - * @return the initial delay in milliseconds + * Using milliseconds by default with timeUnit(). + * @return the initial * @since 3.2 */ long initialDelay() default -1; /** - * Number of milliseconds to delay before the first execution of a + * Number to delay before the first execution of a * {@link #fixedRate} or {@link #fixedDelay} task. + * Using milliseconds by default with initialDelayTimeUnit(). * @return the initial delay in milliseconds as a String value, e.g. a placeholder * or a {@link java.time.Duration#parse java.time.Duration} compliant value * @since 3.2.2 */ String initialDelayString() default ""; + /** + * Specify the {@link TimeUnit} to use for initialDelay, fixedRate and fixedDelay values. + * @return the {@link TimeUnit}, by default milliseconds will be used. + */ + TimeUnit timeUnit() default TimeUnit.MILLISECONDS; + } 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 14b73d6d7f7c..6bb2f21ad3d0 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 @@ -30,6 +30,7 @@ import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -398,7 +399,7 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) Set tasks = new LinkedHashSet<>(4); // Determine initial delay - long initialDelay = scheduled.initialDelay(); + long initialDelay = TimeUnit.MILLISECONDS.convert(scheduled.initialDelay(), scheduled.timeUnit()); String initialDelayString = scheduled.initialDelayString(); if (StringUtils.hasText(initialDelayString)) { Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); @@ -407,7 +408,7 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) } if (StringUtils.hasLength(initialDelayString)) { try { - initialDelay = parseDelayAsLong(initialDelayString); + initialDelay = TimeUnit.MILLISECONDS.convert(parseDelayAsLong(initialDelayString), scheduled.timeUnit()); } catch (RuntimeException ex) { throw new IllegalArgumentException( @@ -446,12 +447,13 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) } // Check fixed delay - long fixedDelay = scheduled.fixedDelay(); + long fixedDelay = TimeUnit.MILLISECONDS.convert(scheduled.fixedDelay(), scheduled.timeUnit()); if (fixedDelay >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay))); } + String fixedDelayString = scheduled.fixedDelayString(); if (StringUtils.hasText(fixedDelayString)) { if (this.embeddedValueResolver != null) { @@ -461,7 +463,7 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; try { - fixedDelay = parseDelayAsLong(fixedDelayString); + fixedDelay = TimeUnit.MILLISECONDS.convert(parseDelayAsLong(fixedDelayString), scheduled.timeUnit()); } catch (RuntimeException ex) { throw new IllegalArgumentException( @@ -472,7 +474,7 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) } // Check fixed rate - long fixedRate = scheduled.fixedRate(); + long fixedRate = TimeUnit.MILLISECONDS.convert(scheduled.fixedRate(), scheduled.timeUnit()); if (fixedRate >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; @@ -487,7 +489,7 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; try { - fixedRate = parseDelayAsLong(fixedRateString); + fixedRate = TimeUnit.MILLISECONDS.convert(parseDelayAsLong(fixedRateString), scheduled.timeUnit()); } catch (RuntimeException ex) { throw new IllegalArgumentException( 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..db982c4ca53a 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 @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Properties; import java.util.TimeZone; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -107,6 +108,62 @@ public void fixedDelayTask() { assertThat(task.getInterval()).isEqualTo(5000L); } + @Test + public void fixedDelayWithSecondsTimeUnitTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedDelayWithSecondsTimeUnitTestBean.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 fixedDelayWithMinutesTimeUnitTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedDelayWithMinutesTimeUnitTestBean.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(180000L); + } + @Test public void fixedRateTask() { BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); @@ -135,6 +192,62 @@ public void fixedRateTask() { assertThat(task.getInterval()).isEqualTo(3000L); } + @Test + public void fixedRateWithSecondsTimeUnitTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateWithSecondsTimeUnitTestBean.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(5000L); + } + + @Test + public void fixedRateWithMinutesTimeUnitTask() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateWithMinutesTimeUnitTestBean.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(180000L); + } + @Test public void fixedRateTaskWithInitialDelay() { BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); @@ -163,6 +276,62 @@ public void fixedRateTaskWithInitialDelay() { assertThat(task.getInterval()).isEqualTo(3000L); } + @Test + public void fixedRateTaskWithSecondsTimeUnitWithInitialDelay() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateWithSecondsTimeUnitInitialDelayTestBean.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(5000L); + assertThat(task.getInterval()).isEqualTo(3000L); + } + + @Test + public void fixedRateTaskWithMinutesTimeUnitWithInitialDelay() { + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(FixedRateWithMinutesTimeUnitInitialDelayTestBean.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(60000L); + assertThat(task.getInterval()).isEqualTo(180000L); + } + @Test public void severalFixedRatesWithRepeatedScheduledAnnotation() { BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); @@ -702,6 +871,22 @@ public void fixedDelay() { } } + static class FixedDelayWithSecondsTimeUnitTestBean { + + @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) + public void fixedDelay() { + } + + } + + static class FixedDelayWithMinutesTimeUnitTestBean { + + @Scheduled(fixedDelay = 3, timeUnit = TimeUnit.MINUTES) + public void fixedDelay() { + } + + } + static class FixedRateTestBean { @@ -709,6 +894,20 @@ static class FixedRateTestBean { public void fixedRate() { } } + static class FixedRateWithSecondsTimeUnitTestBean { + + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS) + public void fixedRate() { + } + } + + + static class FixedRateWithMinutesTimeUnitTestBean { + + @Scheduled(fixedRate = 3, timeUnit = TimeUnit.MINUTES) + public void fixedRate() { + } + } static class FixedRateWithInitialDelayTestBean { @@ -718,6 +917,20 @@ public void fixedRate() { } } + static class FixedRateWithSecondsTimeUnitInitialDelayTestBean { + + @Scheduled(fixedRate = 3, initialDelay = 5, timeUnit = TimeUnit.SECONDS) + public void fixedRate() { + } + } + + static class FixedRateWithMinutesTimeUnitInitialDelayTestBean { + + @Scheduled(fixedRate = 3, initialDelay = 1, timeUnit = TimeUnit.MINUTES) + public void fixedRate() { + } + } + static class SeveralFixedRatesWithSchedulesContainerAnnotationTestBean {