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 415c723817..de78660739 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.Repeatable; 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; @@ -46,6 +47,7 @@ import org.springframework.scheduling.config.ScheduledTaskRegistrar; * @author Juergen Hoeller * @author Dave Syer * @author Chris Beams + * @author Victor Brown * @since 3.0 * @see EnableScheduling * @see ScheduledAnnotationBeanPostProcessor @@ -101,52 +103,64 @@ public @interface Scheduled { 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 14b73d6d7f..52fae5dbab 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.Set; 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; @@ -94,6 +95,7 @@ import org.springframework.util.StringValueResolver; * @author Juergen Hoeller * @author Chris Beams * @author Elizabeth Chatman + * @author Victor Brown * @since 3.0 * @see Scheduled * @see EnableScheduling @@ -398,7 +400,7 @@ public class ScheduledAnnotationBeanPostProcessor 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 +409,7 @@ public class ScheduledAnnotationBeanPostProcessor } if (StringUtils.hasLength(initialDelayString)) { try { - initialDelay = parseDelayAsLong(initialDelayString); + initialDelay = TimeUnit.MILLISECONDS.convert(parseDelayAsLong(initialDelayString), scheduled.timeUnit()); } catch (RuntimeException ex) { throw new IllegalArgumentException( @@ -446,12 +448,13 @@ public class ScheduledAnnotationBeanPostProcessor } // 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 +464,7 @@ public class ScheduledAnnotationBeanPostProcessor 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 +475,7 @@ public class ScheduledAnnotationBeanPostProcessor } // 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 +490,7 @@ public class ScheduledAnnotationBeanPostProcessor 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 8429100702..494bf075c6 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -29,6 +29,7 @@ import java.util.List; 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; @@ -67,6 +68,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Chris Beams * @author Sam Brannen * @author Stevo Slavić + * @author Victor Brown */ public class ScheduledAnnotationBeanPostProcessorTests { @@ -107,6 +109,62 @@ public class ScheduledAnnotationBeanPostProcessorTests { 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 +193,62 @@ public class ScheduledAnnotationBeanPostProcessorTests { 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 +277,62 @@ public class ScheduledAnnotationBeanPostProcessorTests { 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 +872,22 @@ public class ScheduledAnnotationBeanPostProcessorTests { } } + 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 +895,20 @@ public class ScheduledAnnotationBeanPostProcessorTests { 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 +918,20 @@ public class ScheduledAnnotationBeanPostProcessorTests { } } + 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 {