Added Java 8 compliant @Schedules container annotation for @Scheduled

Issue: SPR-10532
master
Juergen Hoeller 11 years ago committed by unknown
parent ead0124b39
commit f9325a8376
  1. 2
      spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java
  2. 258
      spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java
  3. 44
      spring-context/src/main/java/org/springframework/scheduling/annotation/Schedules.java
  4. 43
      spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.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 {
/**

@ -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<String, SchedulingConfigurer> configurers =
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
Map<String, SchedulingConfigurer> configurers =
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
for (SchedulingConfigurer configurer : configurers.values()) {
configurer.configureTasks(this.registrar);
}
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
Map<String, ? super Object> schedulers = new HashMap<String, Object>();
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();
}
}

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

@ -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<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
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 * * * * ?")

Loading…
Cancel
Save