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 c2aa1df37b..0df42573c7 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 @@ -41,6 +41,7 @@ import java.lang.annotation.Target; * composed annotations with attribute overrides. * * @author Mark Fisher + * @author Juergen Hoeller * @author Dave Syer * @author Chris Beams * @since 3.0 @@ -55,10 +56,21 @@ import java.lang.annotation.Target; public @interface Scheduled { /** - * A cron-like expression, extending the usual UN*X definition to include - * triggers on the second as well as minute, hour, day of month, month - * and day of week. e.g. {@code "0 * * * * MON-FRI"} means once per minute on - * weekdays (at the top of the minute - the 0th second). + * A special cron expression value that indicates a disabled trigger: {@value}. + *

This is primarily meant for use with ${...} placeholders, allowing for + * external disabling of corresponding scheduled methods. + * @since 5.1 + */ + String CRON_DISABLED = "-"; + + + /** + * A cron-like expression, extending the usual UN*X definition to include triggers + * on the second as well as minute, hour, day of month, month and day of week. + *

E.g. {@code "0 * * * * MON-FRI"} means once per minute on weekdays + * (at the top of the minute - the 0th second). + *

The special value {@link #CRON_DISABLED "-"} indicates a disabled cron trigger, + * primarily meant for externally specified values resolved by a ${...} placeholder. * @return an expression that can be parsed to a cron schedule * @see org.springframework.scheduling.support.CronSequenceGenerator */ 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 a0c57691a7..d012fd43b7 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 @@ -105,7 +105,7 @@ public class ScheduledAnnotationBeanPostProcessor SmartInitializingSingleton, ApplicationListener, DisposableBean { /** - * The default name of the {@link TaskScheduler} bean to pick up: "taskScheduler". + * The default name of the {@link TaskScheduler} bean to pick up: {@value}. *

Note that the initial lookup happens by type; this is just the fallback * in case of multiple scheduler beans found in the context. * @since 4.2 @@ -405,14 +405,16 @@ public class ScheduledAnnotationBeanPostProcessor if (StringUtils.hasLength(cron)) { Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); processedSchedule = true; - TimeZone timeZone; - if (StringUtils.hasText(zone)) { - timeZone = StringUtils.parseTimeZoneString(zone); - } - else { - timeZone = TimeZone.getDefault(); + if (!Scheduled.CRON_DISABLED.equals(cron)) { + TimeZone timeZone; + if (StringUtils.hasText(zone)) { + timeZone = StringUtils.parseTimeZoneString(zone); + } + else { + timeZone = TimeZone.getDefault(); + } + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); } - tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); } } @@ -478,12 +480,8 @@ public class ScheduledAnnotationBeanPostProcessor // Finally register the scheduled tasks synchronized (this.scheduledTasks) { - Set registeredTasks = this.scheduledTasks.get(bean); - if (registeredTasks == null) { - registeredTasks = new LinkedHashSet<>(4); - this.scheduledTasks.put(bean, registeredTasks); - } - registeredTasks.addAll(tasks); + Set regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4)); + regTasks.addAll(tasks); } } catch (IllegalArgumentException ex) { 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 75693b23ed..59340eeff6 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 @@ -45,6 +45,7 @@ import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.scheduling.config.CronTask; import org.springframework.scheduling.config.IntervalTask; +import org.springframework.scheduling.config.ScheduledTaskHolder; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.support.CronTrigger; import org.springframework.scheduling.support.ScheduledMethodRunnable; @@ -82,7 +83,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -108,7 +111,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -134,7 +139,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -195,7 +202,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(2, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -231,7 +240,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -259,7 +270,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -307,7 +320,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } @Test(expected = BeanCreationException.class) - public void cronTaskWithMethodValidation() throws InterruptedException { + public void cronTaskWithMethodValidation() { BeanDefinition validationDefinition = new RootBeanDefinition(MethodValidationPostProcessor.class); BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); BeanDefinition targetDefinition = new RootBeanDefinition(CronTestBean.class); @@ -325,7 +338,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -350,7 +365,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor( postProcessor).getPropertyValue("registrar"); @@ -376,7 +393,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -407,7 +426,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -424,6 +445,24 @@ public class ScheduledAnnotationBeanPostProcessorTests { assertEquals(businessHoursCronExpression, task.getExpression()); } + @Test + public void propertyPlaceholderWithInactiveCron() { + String businessHoursCronExpression = "-"; + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition placeholderDefinition = new RootBeanDefinition(PropertyPlaceholderConfigurer.class); + Properties properties = new Properties(); + properties.setProperty("schedules.businessHours", businessHoursCronExpression); + placeholderDefinition.getPropertyValues().addPropertyValue("properties", properties); + BeanDefinition targetDefinition = new RootBeanDefinition(PropertyPlaceholderWithCronTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("placeholder", placeholderDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertTrue(postProcessor.getScheduledTasks().isEmpty()); + } + @Test public void propertyPlaceholderWithFixedDelayInMillis() { propertyPlaceholderWithFixedDelay(false); @@ -447,7 +486,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -488,7 +529,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -518,7 +561,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.getBeanFactory().registerSingleton("schedules", schedules); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -549,7 +594,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -574,7 +621,9 @@ public class ScheduledAnnotationBeanPostProcessorTests { context.registerBeanDefinition("target", targetDefinition); context.refresh(); - Object postProcessor = context.getBean("postProcessor"); + ScheduledTaskHolder postProcessor = context.getBean("postProcessor", ScheduledTaskHolder.class); + assertEquals(1, postProcessor.getScheduledTasks().size()); + Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); @@ -803,6 +852,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } } + static class PropertyPlaceholderWithCronTestBean { @Scheduled(cron = "${schedules.businessHours}")