From f9325a83765ba959df72555e2a7418b1b2b0d89a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 7 Aug 2013 16:40:47 +0200 Subject: [PATCH] Added Java 8 compliant @Schedules container annotation for @Scheduled Issue: SPR-10532 --- .../scheduling/annotation/Scheduled.java | 2 + .../ScheduledAnnotationBeanPostProcessor.java | 258 ++++++++++-------- .../scheduling/annotation/Schedules.java | 44 +++ ...duledAnnotationBeanPostProcessorTests.java | 43 +++ 4 files changed, 229 insertions(+), 118 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java 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 383e4adc5de..cf061223be4 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 @@ -18,6 +18,7 @@ package org.springframework.scheduling.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -45,6 +46,7 @@ import java.lang.annotation.Target; @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(Schedules.class) public @interface Scheduled { /** 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 f9abb41c56e..657ee68f81c 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 @@ -110,120 +110,20 @@ public class ScheduledAnnotationBeanPostProcessor @Override public Object postProcessAfterInitialization(final Object bean, String beanName) { - final Class targetClass = AopUtils.getTargetClass(bean); + Class targetClass = AopUtils.getTargetClass(bean); ReflectionUtils.doWithMethods(targetClass, new MethodCallback() { @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class); - if (annotation != null) { - try { - Assert.isTrue(void.class.equals(method.getReturnType()), - "Only void-returning methods may be annotated with @Scheduled"); - Assert.isTrue(method.getParameterTypes().length == 0, - "Only no-arg methods may be annotated with @Scheduled"); - if (AopUtils.isJdkDynamicProxy(bean)) { - try { - // found a @Scheduled method on the target class for this JDK proxy -> is it - // also present on the proxy itself? - method = bean.getClass().getMethod(method.getName(), method.getParameterTypes()); - } - catch (SecurityException ex) { - ReflectionUtils.handleReflectionException(ex); - } - catch (NoSuchMethodException ex) { - throw new IllegalStateException(String.format( - "@Scheduled method '%s' found on bean target class '%s', " + - "but not found in any interface(s) for bean JDK proxy. Either " + - "pull the method up to an interface or switch to subclass (CGLIB) " + - "proxies by setting proxy-target-class/proxyTargetClass " + - "attribute to 'true'", method.getName(), targetClass.getSimpleName())); - } - } - Runnable runnable = new ScheduledMethodRunnable(bean, method); - boolean processedSchedule = false; - String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; - // Determine initial delay - long initialDelay = annotation.initialDelay(); - String initialDelayString = annotation.initialDelayString(); - if (!"".equals(initialDelayString)) { - Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); - if (embeddedValueResolver != null) { - initialDelayString = embeddedValueResolver.resolveStringValue(initialDelayString); - } - try { - initialDelay = Integer.parseInt(initialDelayString); - } - catch (NumberFormatException ex) { - throw new IllegalArgumentException( - "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer"); - } - } - // Check cron expression - String cron = annotation.cron(); - if (!"".equals(cron)) { - Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); - processedSchedule = true; - if (embeddedValueResolver != null) { - cron = embeddedValueResolver.resolveStringValue(cron); - } - registrar.addCronTask(new CronTask(runnable, cron)); - } - // At this point we don't need to differentiate between initial delay set or not anymore - if (initialDelay < 0) { - initialDelay = 0; - } - // Check fixed delay - long fixedDelay = annotation.fixedDelay(); - if (fixedDelay >= 0) { - Assert.isTrue(!processedSchedule, errorMessage); - processedSchedule = true; - registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); - } - String fixedDelayString = annotation.fixedDelayString(); - if (!"".equals(fixedDelayString)) { - Assert.isTrue(!processedSchedule, errorMessage); - processedSchedule = true; - if (embeddedValueResolver != null) { - fixedDelayString = embeddedValueResolver.resolveStringValue(fixedDelayString); - } - try { - fixedDelay = Integer.parseInt(fixedDelayString); - } - catch (NumberFormatException ex) { - throw new IllegalArgumentException( - "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer"); - } - registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); - } - // Check fixed rate - long fixedRate = annotation.fixedRate(); - if (fixedRate >= 0) { - Assert.isTrue(!processedSchedule, errorMessage); - processedSchedule = true; - registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); - } - String fixedRateString = annotation.fixedRateString(); - if (!"".equals(fixedRateString)) { - Assert.isTrue(!processedSchedule, errorMessage); - processedSchedule = true; - if (embeddedValueResolver != null) { - fixedRateString = embeddedValueResolver.resolveStringValue(fixedRateString); - } - try { - fixedRate = Integer.parseInt(fixedRateString); - } - catch (NumberFormatException ex) { - throw new IllegalArgumentException( - "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer"); - } - registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); - } - // Check whether we had any attribute set - Assert.isTrue(processedSchedule, errorMessage); + Schedules schedules = AnnotationUtils.getAnnotation(method, Schedules.class); + if (schedules != null) { + for (Scheduled scheduled : schedules.value()) { + processScheduled(scheduled, method, bean); } - catch (IllegalArgumentException ex) { - throw new IllegalStateException( - "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); + } + else { + Scheduled scheduled = AnnotationUtils.getAnnotation(method, Scheduled.class); + if (scheduled != null) { + processScheduled(scheduled, method, bean); } } } @@ -231,23 +131,146 @@ public class ScheduledAnnotationBeanPostProcessor return bean; } + protected void processScheduled(Scheduled scheduled, Method method, Object bean) { + try { + Assert.isTrue(void.class.equals(method.getReturnType()), + "Only void-returning methods may be annotated with @Scheduled"); + Assert.isTrue(method.getParameterTypes().length == 0, + "Only no-arg methods may be annotated with @Scheduled"); + + if (AopUtils.isJdkDynamicProxy(bean)) { + try { + // found a @Scheduled method on the target class for this JDK proxy -> is it + // also present on the proxy itself? + method = bean.getClass().getMethod(method.getName(), method.getParameterTypes()); + } + catch (SecurityException ex) { + ReflectionUtils.handleReflectionException(ex); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException(String.format( + "@Scheduled method '%s' found on bean target class '%s', " + + "but not found in any interface(s) for bean JDK proxy. Either " + + "pull the method up to an interface or switch to subclass (CGLIB) " + + "proxies by setting proxy-target-class/proxyTargetClass " + + "attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName())); + } + } + + Runnable runnable = new ScheduledMethodRunnable(bean, method); + boolean processedSchedule = false; + String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; + + // Determine initial delay + long initialDelay = scheduled.initialDelay(); + String initialDelayString = scheduled.initialDelayString(); + if (!"".equals(initialDelayString)) { + Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); + if (this.embeddedValueResolver != null) { + initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString); + } + try { + initialDelay = Integer.parseInt(initialDelayString); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException( + "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer"); + } + } + + // Check cron expression + String cron = scheduled.cron(); + if (!"".equals(cron)) { + Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); + processedSchedule = true; + if (this.embeddedValueResolver != null) { + cron = this.embeddedValueResolver.resolveStringValue(cron); + } + this.registrar.addCronTask(new CronTask(runnable, cron)); + } + + // At this point we don't need to differentiate between initial delay set or not anymore + if (initialDelay < 0) { + initialDelay = 0; + } + + // Check fixed delay + long fixedDelay = scheduled.fixedDelay(); + if (fixedDelay >= 0) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); + } + String fixedDelayString = scheduled.fixedDelayString(); + if (!"".equals(fixedDelayString)) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + if (this.embeddedValueResolver != null) { + fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString); + } + try { + fixedDelay = Integer.parseInt(fixedDelayString); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException( + "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer"); + } + this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); + } + + // Check fixed rate + long fixedRate = scheduled.fixedRate(); + if (fixedRate >= 0) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); + } + String fixedRateString = scheduled.fixedRateString(); + if (!"".equals(fixedRateString)) { + Assert.isTrue(!processedSchedule, errorMessage); + processedSchedule = true; + if (this.embeddedValueResolver != null) { + fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString); + } + try { + fixedRate = Integer.parseInt(fixedRateString); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException( + "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer"); + } + this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); + } + + // Check whether we had any attribute set + Assert.isTrue(processedSchedule, errorMessage); + } + catch (IllegalArgumentException ex) { + throw new IllegalStateException( + "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); + } + } + @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (event.getApplicationContext() != this.applicationContext) { return; } - Map configurers = - this.applicationContext.getBeansOfType(SchedulingConfigurer.class); + if (this.scheduler != null) { this.registrar.setScheduler(this.scheduler); } + + Map configurers = + this.applicationContext.getBeansOfType(SchedulingConfigurer.class); for (SchedulingConfigurer configurer : configurers.values()) { configurer.configureTasks(this.registrar); } + if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) { Map schedulers = new HashMap(); - schedulers.putAll(applicationContext.getBeansOfType(TaskScheduler.class)); - schedulers.putAll(applicationContext.getBeansOfType(ScheduledExecutorService.class)); + schedulers.putAll(this.applicationContext.getBeansOfType(TaskScheduler.class)); + schedulers.putAll(this.applicationContext.getBeansOfType(ScheduledExecutorService.class)); if (schedulers.size() == 0) { // do nothing -> fall back to default scheduler } @@ -263,14 +286,13 @@ public class ScheduledAnnotationBeanPostProcessor "configureTasks() callback. Found the following beans: " + schedulers.keySet()); } } + this.registrar.afterPropertiesSet(); } @Override - public void destroy() throws Exception { - if (this.registrar != null) { - this.registrar.destroy(); - } + public void destroy() { + this.registrar.destroy(); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java new file mode 100644 index 00000000000..3cba41869ab --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.scheduling.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link Scheduled} annotations. + * + *

Can be used natively, declaring several nested {@link Scheduled} annotations. + * Can also be used in conjunction with Java 8's support for repeatable annotations, + * where {@link Scheduled} can simply be declared several times on the same method, + * implicitly generating this container annotation. + * + * @author Juergen Hoeller + * @since 4.0 + * @see Scheduled + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Schedules { + + Scheduled[] value(); + +} 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 91bf0f697e6..0de473bcd00 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 @@ -128,6 +128,40 @@ public class ScheduledAnnotationBeanPostProcessorTests { assertEquals(3000L, task.getInterval()); } + @Test + public void severalFixedRates() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition(SeveralFixedRatesTestBean.class); + context.registerBeanDefinition("postProcessor", processorDefinition); + context.registerBeanDefinition("target", targetDefinition); + context.refresh(); + Object postProcessor = context.getBean("postProcessor"); + Object target = context.getBean("target"); + ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) + new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) + new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); + assertEquals(2, fixedRateTasks.size()); + IntervalTask task1 = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable1 = (ScheduledMethodRunnable) task1.getRunnable(); + Object targetObject = runnable1.getTarget(); + Method targetMethod = runnable1.getMethod(); + assertEquals(target, targetObject); + assertEquals("fixedRate", targetMethod.getName()); + assertEquals(0, task1.getInitialDelay()); + assertEquals(4000L, task1.getInterval()); + IntervalTask task2 = fixedRateTasks.get(1); + ScheduledMethodRunnable runnable2 = (ScheduledMethodRunnable) task2.getRunnable(); + targetObject = runnable2.getTarget(); + targetMethod = runnable2.getMethod(); + assertEquals(target, targetObject); + assertEquals("fixedRate", targetMethod.getName()); + assertEquals(2000L, task2.getInitialDelay()); + assertEquals(4000L, task2.getInterval()); + } + @Test public void cronTask() throws InterruptedException { Assume.group(TestGroup.LONG_RUNNING); @@ -404,6 +438,15 @@ public class ScheduledAnnotationBeanPostProcessorTests { } + static class SeveralFixedRatesTestBean { + + @Scheduled(fixedRate=4000) + @Scheduled(fixedRate=4000, initialDelay=2000) + public void fixedRate() { + } + } + + static class CronTestBean { @Scheduled(cron="*/7 * * * * ?")