Added Java 8 compliant @Schedules container annotation for @Scheduled

Issue: SPR-10532
This commit is contained in:
Juergen Hoeller 2013-08-07 16:40:47 +02:00 committed by unknown
parent ead0124b39
commit f9325a8376
4 changed files with 229 additions and 118 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.scheduling.annotation;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
@ -45,6 +46,7 @@ import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
@Repeatable(Schedules.class)
public @interface Scheduled { public @interface Scheduled {
/** /**

View File

@ -110,17 +110,34 @@ public class ScheduledAnnotationBeanPostProcessor
@Override @Override
public Object postProcessAfterInitialization(final Object bean, String beanName) { public Object postProcessAfterInitialization(final Object bean, String beanName) {
final Class<?> targetClass = AopUtils.getTargetClass(bean); Class<?> targetClass = AopUtils.getTargetClass(bean);
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() { ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
@Override @Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class); Schedules schedules = AnnotationUtils.getAnnotation(method, Schedules.class);
if (annotation != null) { if (schedules != null) {
for (Scheduled scheduled : schedules.value()) {
processScheduled(scheduled, method, bean);
}
}
else {
Scheduled scheduled = AnnotationUtils.getAnnotation(method, Scheduled.class);
if (scheduled != null) {
processScheduled(scheduled, method, bean);
}
}
}
});
return bean;
}
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try { try {
Assert.isTrue(void.class.equals(method.getReturnType()), Assert.isTrue(void.class.equals(method.getReturnType()),
"Only void-returning methods may be annotated with @Scheduled"); "Only void-returning methods may be annotated with @Scheduled");
Assert.isTrue(method.getParameterTypes().length == 0, Assert.isTrue(method.getParameterTypes().length == 0,
"Only no-arg methods may be annotated with @Scheduled"); "Only no-arg methods may be annotated with @Scheduled");
if (AopUtils.isJdkDynamicProxy(bean)) { if (AopUtils.isJdkDynamicProxy(bean)) {
try { try {
// found a @Scheduled method on the target class for this JDK proxy -> is it // found a @Scheduled method on the target class for this JDK proxy -> is it
@ -136,19 +153,21 @@ public class ScheduledAnnotationBeanPostProcessor
"but not found in any interface(s) for bean JDK proxy. Either " + "but not found in any interface(s) for bean JDK proxy. Either " +
"pull the method up to an interface or switch to subclass (CGLIB) " + "pull the method up to an interface or switch to subclass (CGLIB) " +
"proxies by setting proxy-target-class/proxyTargetClass " + "proxies by setting proxy-target-class/proxyTargetClass " +
"attribute to 'true'", method.getName(), targetClass.getSimpleName())); "attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()));
} }
} }
Runnable runnable = new ScheduledMethodRunnable(bean, method); Runnable runnable = new ScheduledMethodRunnable(bean, method);
boolean processedSchedule = false; boolean processedSchedule = false;
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
// Determine initial delay // Determine initial delay
long initialDelay = annotation.initialDelay(); long initialDelay = scheduled.initialDelay();
String initialDelayString = annotation.initialDelayString(); String initialDelayString = scheduled.initialDelayString();
if (!"".equals(initialDelayString)) { if (!"".equals(initialDelayString)) {
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
if (embeddedValueResolver != null) { if (this.embeddedValueResolver != null) {
initialDelayString = embeddedValueResolver.resolveStringValue(initialDelayString); initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
} }
try { try {
initialDelay = Integer.parseInt(initialDelayString); initialDelay = Integer.parseInt(initialDelayString);
@ -158,33 +177,36 @@ public class ScheduledAnnotationBeanPostProcessor
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer"); "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
} }
} }
// Check cron expression // Check cron expression
String cron = annotation.cron(); String cron = scheduled.cron();
if (!"".equals(cron)) { if (!"".equals(cron)) {
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true; processedSchedule = true;
if (embeddedValueResolver != null) { if (this.embeddedValueResolver != null) {
cron = embeddedValueResolver.resolveStringValue(cron); cron = this.embeddedValueResolver.resolveStringValue(cron);
} }
registrar.addCronTask(new CronTask(runnable, cron)); this.registrar.addCronTask(new CronTask(runnable, cron));
} }
// At this point we don't need to differentiate between initial delay set or not anymore // At this point we don't need to differentiate between initial delay set or not anymore
if (initialDelay < 0) { if (initialDelay < 0) {
initialDelay = 0; initialDelay = 0;
} }
// Check fixed delay // Check fixed delay
long fixedDelay = annotation.fixedDelay(); long fixedDelay = scheduled.fixedDelay();
if (fixedDelay >= 0) { if (fixedDelay >= 0) {
Assert.isTrue(!processedSchedule, errorMessage); Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true; processedSchedule = true;
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
} }
String fixedDelayString = annotation.fixedDelayString(); String fixedDelayString = scheduled.fixedDelayString();
if (!"".equals(fixedDelayString)) { if (!"".equals(fixedDelayString)) {
Assert.isTrue(!processedSchedule, errorMessage); Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true; processedSchedule = true;
if (embeddedValueResolver != null) { if (this.embeddedValueResolver != null) {
fixedDelayString = embeddedValueResolver.resolveStringValue(fixedDelayString); fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
} }
try { try {
fixedDelay = Integer.parseInt(fixedDelayString); fixedDelay = Integer.parseInt(fixedDelayString);
@ -193,21 +215,22 @@ public class ScheduledAnnotationBeanPostProcessor
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer"); "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
} }
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
} }
// Check fixed rate // Check fixed rate
long fixedRate = annotation.fixedRate(); long fixedRate = scheduled.fixedRate();
if (fixedRate >= 0) { if (fixedRate >= 0) {
Assert.isTrue(!processedSchedule, errorMessage); Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true; processedSchedule = true;
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
} }
String fixedRateString = annotation.fixedRateString(); String fixedRateString = scheduled.fixedRateString();
if (!"".equals(fixedRateString)) { if (!"".equals(fixedRateString)) {
Assert.isTrue(!processedSchedule, errorMessage); Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true; processedSchedule = true;
if (embeddedValueResolver != null) { if (this.embeddedValueResolver != null) {
fixedRateString = embeddedValueResolver.resolveStringValue(fixedRateString); fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
} }
try { try {
fixedRate = Integer.parseInt(fixedRateString); fixedRate = Integer.parseInt(fixedRateString);
@ -216,8 +239,9 @@ public class ScheduledAnnotationBeanPostProcessor
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer"); "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
} }
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
} }
// Check whether we had any attribute set // Check whether we had any attribute set
Assert.isTrue(processedSchedule, errorMessage); Assert.isTrue(processedSchedule, errorMessage);
} }
@ -226,28 +250,27 @@ public class ScheduledAnnotationBeanPostProcessor
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
} }
} }
}
});
return bean;
}
@Override @Override
public void onApplicationEvent(ContextRefreshedEvent event) { public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext() != this.applicationContext) { if (event.getApplicationContext() != this.applicationContext) {
return; return;
} }
Map<String, SchedulingConfigurer> configurers =
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
if (this.scheduler != null) { if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler); this.registrar.setScheduler(this.scheduler);
} }
Map<String, SchedulingConfigurer> configurers =
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
for (SchedulingConfigurer configurer : configurers.values()) { for (SchedulingConfigurer configurer : configurers.values()) {
configurer.configureTasks(this.registrar); configurer.configureTasks(this.registrar);
} }
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) { if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
Map<String, ? super Object> schedulers = new HashMap<String, Object>(); Map<String, ? super Object> schedulers = new HashMap<String, Object>();
schedulers.putAll(applicationContext.getBeansOfType(TaskScheduler.class)); schedulers.putAll(this.applicationContext.getBeansOfType(TaskScheduler.class));
schedulers.putAll(applicationContext.getBeansOfType(ScheduledExecutorService.class)); schedulers.putAll(this.applicationContext.getBeansOfType(ScheduledExecutorService.class));
if (schedulers.size() == 0) { if (schedulers.size() == 0) {
// do nothing -> fall back to default scheduler // do nothing -> fall back to default scheduler
} }
@ -263,14 +286,13 @@ public class ScheduledAnnotationBeanPostProcessor
"configureTasks() callback. Found the following beans: " + schedulers.keySet()); "configureTasks() callback. Found the following beans: " + schedulers.keySet());
} }
} }
this.registrar.afterPropertiesSet(); this.registrar.afterPropertiesSet();
} }
@Override @Override
public void destroy() throws Exception { public void destroy() {
if (this.registrar != null) {
this.registrar.destroy(); this.registrar.destroy();
} }
}
} }

View File

@ -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.
*
* <p>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();
}

View File

@ -128,6 +128,40 @@ public class ScheduledAnnotationBeanPostProcessorTests {
assertEquals(3000L, task.getInterval()); 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<IntervalTask> fixedRateTasks = (List<IntervalTask>)
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 @Test
public void cronTask() throws InterruptedException { public void cronTask() throws InterruptedException {
Assume.group(TestGroup.LONG_RUNNING); 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 { static class CronTestBean {
@Scheduled(cron="*/7 * * * * ?") @Scheduled(cron="*/7 * * * * ?")