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 ccda008fcc8..dea788ce088 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -24,11 +24,11 @@ import java.lang.annotation.Target; /** * Annotation that marks a method to be scheduled. Exactly one of the - * cron, fixedDelay, or fixedRate - * attributes must be provided. + * {@link #cron()}, {@link #fixedDelay()}, or {@link #fixedRate()} + * attributes must be specified. * *

The annotated method must expect no arguments and have a - * void return type. + * {@code void} return type. * *

Processing of {@code @Scheduled} annotations is performed by * registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be @@ -37,6 +37,7 @@ import java.lang.annotation.Target; * * @author Mark Fisher * @author Dave Syer + * @author Chris Beams * @since 3.0 * @see EnableScheduling * @see ScheduledAnnotationBeanPostProcessor @@ -49,9 +50,10 @@ 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. "0 * * * * MON-FRI" means once - * per minute on weekdays (at the top of the minute - the 0th second). + * 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). * @return an expression that can be parsed to a cron schedule + * @see org.springframework.scheduling.support.CronSequenceGenerator */ String cron() default ""; @@ -68,4 +70,12 @@ public @interface Scheduled { */ long fixedRate() default -1; + /** + * Number of milliseconds to delay before the first execution of a + * {@link #fixedRate()} or {@link #fixedDelay()} task. + * @return the initial delay in milliseconds + * @since 3.2 + */ + long initialDelay() default 0; + } 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 692270044ce..f5f236d2f9d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -17,6 +17,7 @@ package org.springframework.scheduling.annotation; import java.lang.reflect.Method; + import java.util.HashMap; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; @@ -33,6 +34,8 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.IntervalTask; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.support.ScheduledMethodRunnable; import org.springframework.util.Assert; @@ -74,13 +77,7 @@ public class ScheduledAnnotationBeanPostProcessor private ApplicationContext applicationContext; - private ScheduledTaskRegistrar registrar; - - private final Map cronTasks = new HashMap(); - - private final Map fixedDelayTasks = new HashMap(); - - private final Map fixedRateTasks = new HashMap(); + private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar(); /** @@ -104,7 +101,6 @@ public class ScheduledAnnotationBeanPostProcessor return LOWEST_PRECEDENCE; } - public Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; } @@ -124,9 +120,11 @@ public class ScheduledAnnotationBeanPostProcessor // 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) { + } + catch (SecurityException ex) { ReflectionUtils.handleReflectionException(ex); - } catch (NoSuchMethodException 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 " + @@ -137,26 +135,27 @@ public class ScheduledAnnotationBeanPostProcessor } Runnable runnable = new ScheduledMethodRunnable(bean, method); boolean processedSchedule = false; - String errorMessage = "Exactly one of 'cron', 'fixedDelay', or 'fixedRate' is required."; + String errorMessage = "Exactly one of the 'cron', 'fixedDelay', or 'fixedRate' attributes is required."; String cron = annotation.cron(); if (!"".equals(cron)) { processedSchedule = true; if (embeddedValueResolver != null) { cron = embeddedValueResolver.resolveStringValue(cron); } - cronTasks.put(runnable, cron); + registrar.addCronTask(new CronTask(runnable, cron)); } + long initialDelay = annotation.initialDelay(); long fixedDelay = annotation.fixedDelay(); if (fixedDelay >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - fixedDelayTasks.put(runnable, fixedDelay); + registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay)); } long fixedRate = annotation.fixedRate(); if (fixedRate >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; - fixedRateTasks.put(runnable, fixedRate); + registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay)); } Assert.isTrue(processedSchedule, errorMessage); } @@ -170,17 +169,8 @@ public class ScheduledAnnotationBeanPostProcessor return; } - Map configurers = applicationContext.getBeansOfType(SchedulingConfigurer.class); - - if (this.cronTasks.isEmpty() && this.fixedDelayTasks.isEmpty() && - this.fixedRateTasks.isEmpty() && configurers.isEmpty()) { - return; - } - - this.registrar = new ScheduledTaskRegistrar(); - this.registrar.setCronTasks(this.cronTasks); - this.registrar.setFixedDelayTasks(this.fixedDelayTasks); - this.registrar.setFixedRateTasks(this.fixedRateTasks); + Map configurers = + this.applicationContext.getBeansOfType(SchedulingConfigurer.class); if (this.scheduler != null) { this.registrar.setScheduler(this.scheduler); @@ -190,19 +180,23 @@ public class ScheduledAnnotationBeanPostProcessor configurer.configureTasks(this.registrar); } - if (registrar.getScheduler() == null) { + if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) { Map schedulers = new HashMap(); schedulers.putAll(applicationContext.getBeansOfType(TaskScheduler.class)); schedulers.putAll(applicationContext.getBeansOfType(ScheduledExecutorService.class)); if (schedulers.size() == 0) { // do nothing -> fall back to default scheduler - } else if (schedulers.size() == 1) { + } + else if (schedulers.size() == 1) { this.registrar.setScheduler(schedulers.values().iterator().next()); - } else if (schedulers.size() >= 2){ - throw new IllegalStateException("More than one TaskScheduler and/or ScheduledExecutorService " + - "exist within the context. Remove all but one of the beans; or implement the " + - "SchedulingConfigurer interface and call ScheduledTaskRegistrar#setScheduler " + - "explicitly within the configureTasks() callback. Found the following beans: " + schedulers.keySet()); + } + else if (schedulers.size() >= 2){ + throw new IllegalStateException( + "More than one TaskScheduler and/or ScheduledExecutorService " + + "exist within the context. Remove all but one of the beans; or " + + "implement the SchedulingConfigurer interface and call " + + "ScheduledTaskRegistrar#setScheduler explicitly within the " + + "configureTasks() callback. Found the following beans: " + schedulers.keySet()); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java index d4787c0e4cc..095ff700a7c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/SchedulingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -19,12 +19,15 @@ package org.springframework.scheduling.annotation; import org.springframework.scheduling.config.ScheduledTaskRegistrar; /** - * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration} - * classes annotated with @{@link EnableScheduling} that wish to register scheduled tasks - * in a programmatic fashion as opposed to the declarative approach of - * using the @{@link Scheduled} annotation. For example, this may be necessary when - * implementing {@link org.springframework.scheduling.Trigger Trigger}-based tasks, which - * are not supported by the {@code @Scheduled} annotation. + * Optional interface to be implemented by @{@link + * org.springframework.context.annotation.Configuration Configuration} classes annotated + * with @{@link EnableScheduling}. Typically used for setting a specific + * {@link org.springframework.scheduling.TaskScheduler TaskScheduler} bean to be used when + * executing scheduled tasks or for registering scheduled tasks in a programmatic + * fashion as opposed to the declarative approach of using the @{@link Scheduled} + * annotation. For example, this may be necessary when implementing {@link + * org.springframework.scheduling.Trigger Trigger}-based tasks, which are not supported by + * the {@code @Scheduled} annotation. * *

See @{@link EnableScheduling} for detailed usage examples. * @@ -35,6 +38,12 @@ import org.springframework.scheduling.config.ScheduledTaskRegistrar; */ public interface SchedulingConfigurer { + /** + * Callback allowing a {@link org.springframework.scheduling.TaskScheduler + * TaskScheduler} and specific {@link org.springframework.scheduling.config.Task Task} + * instances to be registered against the given the {@link ScheduledTaskRegistrar} + * @param taskRegistrar the registrar to be configured. + */ void configureTasks(ScheduledTaskRegistrar taskRegistrar); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/CronTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/CronTask.java new file mode 100644 index 00000000000..cf859ab4273 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/CronTask.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2012 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.config; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.scheduling.support.CronTrigger; + +/** + * {@link TriggerTask} implementation defining a {@code Runnable} to be executed according + * to a {@linkplain org.springframework.scheduling.support.CronSequenceGenerator standard + * cron expression}. + * + * @author Chris Beams + * @since 3.2 + * @see Scheduled#cron() + * @see ScheduledTaskRegistrar#setCronTasksList(java.util.List) + * @see org.springframework.scheduling.TaskScheduler + */ +public class CronTask extends TriggerTask { + + private String expression; + + + /** + * Create a new {@code CronTask}. + * @param runnable the underlying task to execute + * @param expression cron expression defining when the task should be executed + */ + public CronTask(Runnable runnable, String expression) { + this(runnable, new CronTrigger(expression)); + } + + /** + * Create a new {@code CronTask}. + * @param runnable the underlying task to execute + * @param cronTrigger the cron trigger defining when the task should be executed + */ + public CronTask(Runnable runnable, CronTrigger cronTrigger) { + super(runnable, cronTrigger); + this.expression = cronTrigger.getExpression(); + } + + public String getExpression() { + return expression; + } +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java new file mode 100644 index 00000000000..4b39927e8f7 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/IntervalTask.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2012 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.config; + +/** + * {@link Task} implementation defining a {@code Runnable} to be executed at a given + * millisecond interval which may be treated as fixed-rate or fixed-delay depending on + * context. + * + * @author Chris Beams + * @since 3.2 + * @see org.springframework.scheduling.annotation.Scheduled#fixedRate() + * @see org.springframework.scheduling.annotation.Scheduled#fixedDelay() + * @see ScheduledTaskRegistrar#setFixedRateTasksList(java.util.List) + * @see ScheduledTaskRegistrar#setFixedDelayTasksList(java.util.List) + * @see org.springframework.scheduling.TaskScheduler + */ +public class IntervalTask extends Task { + + private final long interval; + + private final long initialDelay; + + + /** + * Create a new {@code IntervalTask}. + * @param runnable the underlying task to execute + * @param interval how often in milliseconds the task should be executed + * @param initialDelay initial delay before first execution of the task + */ + public IntervalTask(Runnable runnable, long interval, long initialDelay) { + super(runnable); + this.initialDelay = initialDelay; + this.interval = interval; + } + + /** + * Create a new {@code IntervalTask} with no initial delay. + * @param runnable the underlying task to execute + * @param interval how often in milliseconds the task should be executed + */ + public IntervalTask(Runnable runnable, long interval) { + this(runnable, interval, 0); + } + + + public long getInterval() { + return interval; + } + + public long getInitialDelay() { + return initialDelay; + } +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java index 1898441e217..b5b0b44ddec 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -16,8 +16,10 @@ package org.springframework.scheduling.config; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Date; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Executors; @@ -33,8 +35,8 @@ import org.springframework.scheduling.support.CronTrigger; import org.springframework.util.Assert; /** - * Helper bean for registering tasks with a {@link TaskScheduler}, - * typically using cron expressions. + * Helper bean for registering tasks with a {@link TaskScheduler}, typically using cron + * expressions. * *

As of Spring 3.1, {@code ScheduledTaskRegistrar} has a more prominent user-facing * role when used in conjunction with the @{@link @@ -43,6 +45,7 @@ import org.springframework.util.Assert; * SchedulingConfigurer} callback interface. * * @author Juergen Hoeller + * @author Chris Beams * @since 3.0 * @see org.springframework.scheduling.annotation.EnableAsync * @see org.springframework.scheduling.annotation.SchedulingConfigurer @@ -53,19 +56,19 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean private ScheduledExecutorService localExecutor; - private Map triggerTasks; + private List triggerTasks; - private Map cronTasks; + private List cronTasks; - private Map fixedRateTasks; + private List fixedRateTasks; - private Map fixedDelayTasks; + private List fixedDelayTasks; private final Set> scheduledFutures = new LinkedHashSet>(); /** - * Set the TaskScheduler to register scheduled tasks with. + * Set the {@link TaskScheduler} to register scheduled tasks with. */ public void setTaskScheduler(TaskScheduler taskScheduler) { Assert.notNull(taskScheduler, "TaskScheduler must not be null"); @@ -73,9 +76,9 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean } /** - * Set the {@link org.springframework.scheduling.TaskScheduler} to register scheduled - * tasks with, or a {@link java.util.concurrent.ScheduledExecutorService} to be - * wrapped as a TaskScheduler. + * Set the {@link TaskScheduler} to register scheduled tasks with, or a + * {@link java.util.concurrent.ScheduledExecutorService} to be wrapped as a + * {@code TaskScheduler}. */ public void setScheduler(Object scheduler) { Assert.notNull(scheduler, "Scheduler object must not be null"); @@ -91,17 +94,31 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean } /** - * Return the scheduler instance for this registrar (may be null) + * Return the {@link TaskScheduler} instance for this registrar (may be {@code null}). */ public TaskScheduler getScheduler() { return this.taskScheduler; } + /** * Specify triggered tasks as a Map of Runnables (the tasks) and Trigger objects * (typically custom implementations of the {@link Trigger} interface). */ public void setTriggerTasks(Map triggerTasks) { + this.triggerTasks = new ArrayList(); + for (Map.Entry task : triggerTasks.entrySet()) { + this.triggerTasks.add(new TriggerTask(task.getKey(), task.getValue())); + } + } + + /** + * Specify triggered tasks as a list of {@link TriggerTask} objects. Primarily used + * by {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setTriggerTasksList(List triggerTasks) { this.triggerTasks = triggerTasks; } @@ -110,6 +127,19 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean * @see CronTrigger */ public void setCronTasks(Map cronTasks) { + this.cronTasks = new ArrayList(); + for (Map.Entry task : cronTasks.entrySet()) { + this.addCronTask(task.getKey(), task.getValue()); + } + } + + /** + * Specify triggered tasks as a list of {@link CronTask} objects. Primarily used by + * {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setCronTasksList(List cronTasks) { this.cronTasks = cronTasks; } @@ -118,39 +148,79 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) */ public void setFixedRateTasks(Map fixedRateTasks) { + this.fixedRateTasks = new ArrayList(); + for (Map.Entry task : fixedRateTasks.entrySet()) { + this.addFixedRateTask(task.getKey(), task.getValue()); + } + } + + /** + * Specify fixed-rate tasks as a list of {@link IntervalTask} objects. Primarily used + * by {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setFixedRateTasksList(List fixedRateTasks) { this.fixedRateTasks = fixedRateTasks; } + /** + * Specify triggered tasks as a Map of Runnables (the tasks) and fixed-delay values. + * @see TaskScheduler#scheduleWithFixedDelay(Runnable, long) + */ + public void setFixedDelayTasks(Map fixedDelayTasks) { + this.fixedDelayTasks = new ArrayList(); + for (Map.Entry task : fixedDelayTasks.entrySet()) { + this.addFixedDelayTask(task.getKey(), task.getValue()); + } + } + + /** + * Specify fixed-delay tasks as a list of {@link IntervalTask} objects. Primarily used + * by {@code } namespace parsing. + * @since 3.2 + * @see ScheduledTasksBeanDefinitionParser + */ + public void setFixedDelayTasksList(List fixedDelayTasks) { + this.fixedDelayTasks = fixedDelayTasks; + } + /** * Add a Runnable task to be triggered per the given {@link Trigger}. * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) */ public void addTriggerTask(Runnable task, Trigger trigger) { + this.addTriggerTask(new TriggerTask(task, trigger)); + } + + /** + * Add a {@code TriggerTask}. + * @since 3.2 + * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) + */ + public void addTriggerTask(TriggerTask task) { if (this.triggerTasks == null) { - this.triggerTasks = new HashMap(); + this.triggerTasks = new ArrayList(); } - this.triggerTasks.put(task, trigger); + this.triggerTasks.add(task); } /** * Add a Runnable task to be triggered per the given cron expression */ - public void addCronTask(Runnable task, String cronExpression) { - if (this.cronTasks == null) { - this.cronTasks = new HashMap(); - } - this.cronTasks.put(task, cronExpression); + public void addCronTask(Runnable task, String expression) { + this.addCronTask(new CronTask(task, expression)); } /** - * Add a Runnable task to be triggered with the given fixed delay. - * @see TaskScheduler#scheduleWithFixedDelay(Runnable, long) + * Add a {@link CronTask}. + * @since 3.2 */ - public void addFixedDelayTask(Runnable task, long delay) { - if (this.fixedDelayTasks == null) { - this.fixedDelayTasks = new HashMap(); + public void addCronTask(CronTask task) { + if (this.cronTasks == null) { + this.cronTasks = new ArrayList(); } - this.fixedDelayTasks.put(task, delay); + this.cronTasks.add(task); } /** @@ -158,49 +228,103 @@ public class ScheduledTaskRegistrar implements InitializingBean, DisposableBean * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) */ public void addFixedRateTask(Runnable task, long period) { + this.addFixedRateTask(new IntervalTask(task, period, 0)); + } + + /** + * Add a fixed-rate {@link IntervalTask}. + * @since 3.2 + * @see TaskScheduler#scheduleAtFixedRate(Runnable, long) + */ + public void addFixedRateTask(IntervalTask task) { if (this.fixedRateTasks == null) { - this.fixedRateTasks = new HashMap(); + this.fixedRateTasks = new ArrayList(); } - this.fixedRateTasks.put(task, period); + this.fixedRateTasks.add(task); } /** - * Specify triggered tasks as a Map of Runnables (the tasks) and fixed-delay values. + * Add a Runnable task to be triggered with the given fixed delay. * @see TaskScheduler#scheduleWithFixedDelay(Runnable, long) */ - public void setFixedDelayTasks(Map fixedDelayTasks) { - this.fixedDelayTasks = fixedDelayTasks; + public void addFixedDelayTask(Runnable task, long delay) { + this.addFixedDelayTask(new IntervalTask(task, delay, 0)); + } + + /** + * Add a fixed-delay {@link IntervalTask}. + * @since 3.2 + * @see TaskScheduler#scheduleWithFixedDelay(Runnable, long) + */ + public void addFixedDelayTask(IntervalTask task) { + if (this.fixedDelayTasks == null) { + this.fixedDelayTasks = new ArrayList(); + } + this.fixedDelayTasks.add(task); } + /** + * Return whether this {@code ScheduledTaskRegistrar} has any tasks registered. + * @since 3.2 + */ + public boolean hasTasks() { + return (this.fixedRateTasks != null && !this.fixedRateTasks.isEmpty()) || + (this.fixedDelayTasks != null && !this.fixedDelayTasks.isEmpty()) || + (this.cronTasks != null && !this.cronTasks.isEmpty()) || + (this.triggerTasks != null && !this.triggerTasks.isEmpty()); + } + /** + * Schedule all registered tasks against the underlying {@linkplain + * #setTaskScheduler(TaskScheduler) task scheduler. + */ public void afterPropertiesSet() { + long now = System.currentTimeMillis(); + if (this.taskScheduler == null) { this.localExecutor = Executors.newSingleThreadScheduledExecutor(); this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); } if (this.triggerTasks != null) { - for (Map.Entry entry : this.triggerTasks.entrySet()) { - this.scheduledFutures.add(this.taskScheduler.schedule(entry.getKey(), entry.getValue())); + for (TriggerTask task : triggerTasks) { + this.scheduledFutures.add(this.taskScheduler.schedule( + task.getRunnable(), task.getTrigger())); } } if (this.cronTasks != null) { - for (Map.Entry entry : this.cronTasks.entrySet()) { - this.scheduledFutures.add(this.taskScheduler.schedule(entry.getKey(), new CronTrigger(entry.getValue()))); + for (CronTask task : cronTasks) { + this.scheduledFutures.add(this.taskScheduler.schedule( + task.getRunnable(), task.getTrigger())); } } if (this.fixedRateTasks != null) { - for (Map.Entry entry : this.fixedRateTasks.entrySet()) { - this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate(entry.getKey(), entry.getValue())); + for (IntervalTask task : fixedRateTasks) { + if (task.getInitialDelay() > 0) { + Date startTime = new Date(now + task.getInitialDelay()); + this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate( + task.getRunnable(), startTime, task.getInterval())); + } + else { + this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate( + task.getRunnable(), task.getInterval())); + } } } if (this.fixedDelayTasks != null) { - for (Map.Entry entry : this.fixedDelayTasks.entrySet()) { - this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay(entry.getKey(), entry.getValue())); + for (IntervalTask task : fixedDelayTasks) { + if (task.getInitialDelay() > 0) { + Date startTime = new Date(now + task.getInitialDelay()); + this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay( + task.getRunnable(), startTime, task.getInterval())); + } + else { + this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay( + task.getRunnable(), task.getInterval())); + } } } } - public void destroy() { for (ScheduledFuture future : this.scheduledFutures) { future.cancel(true); diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParser.java index 6e29ad4f102..828528afab4 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -16,27 +16,29 @@ package org.springframework.scheduling.config; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.StringUtils; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + /** * Parser for the 'scheduled-tasks' element of the scheduling namespace. * * @author Mark Fisher + * @author Chris Beams * @since 3.0 */ public class ScheduledTasksBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { private static final String ELEMENT_SCHEDULED = "scheduled"; + private static final long ZERO_INITIAL_DELAY = 0; @Override protected boolean shouldGenerateId() { @@ -51,10 +53,10 @@ public class ScheduledTasksBeanDefinitionParser extends AbstractSingleBeanDefini @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { builder.setLazyInit(false); // lazy scheduled tasks are a contradiction in terms -> force to false - ManagedMap cronTaskMap = new ManagedMap(); - ManagedMap fixedDelayTaskMap = new ManagedMap(); - ManagedMap fixedRateTaskMap = new ManagedMap(); - ManagedMap triggerTaskMap = new ManagedMap(); + ManagedList cronTaskList = new ManagedList(); + ManagedList fixedDelayTaskList = new ManagedList(); + ManagedList fixedRateTaskList = new ManagedList(); + ManagedList triggerTaskList = new ManagedList(); NodeList childNodes = element.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); @@ -64,55 +66,67 @@ public class ScheduledTasksBeanDefinitionParser extends AbstractSingleBeanDefini Element taskElement = (Element) child; String ref = taskElement.getAttribute("ref"); String method = taskElement.getAttribute("method"); - + // Check that 'ref' and 'method' are specified if (!StringUtils.hasText(ref) || !StringUtils.hasText(method)) { parserContext.getReaderContext().error("Both 'ref' and 'method' are required", taskElement); // Continue with the possible next task element continue; } - - RuntimeBeanReference runnableBeanRef = new RuntimeBeanReference( - createRunnableBean(ref, method, taskElement, parserContext)); String cronAttribute = taskElement.getAttribute("cron"); String fixedDelayAttribute = taskElement.getAttribute("fixed-delay"); String fixedRateAttribute = taskElement.getAttribute("fixed-rate"); String triggerAttribute = taskElement.getAttribute("trigger"); + String initialDelayAttribute = taskElement.getAttribute("initial-delay"); boolean hasCronAttribute = StringUtils.hasText(cronAttribute); boolean hasFixedDelayAttribute = StringUtils.hasText(fixedDelayAttribute); boolean hasFixedRateAttribute = StringUtils.hasText(fixedRateAttribute); boolean hasTriggerAttribute = StringUtils.hasText(triggerAttribute); + boolean hasInitialDelayAttribute = StringUtils.hasText(initialDelayAttribute); - if (!(hasCronAttribute | hasFixedDelayAttribute | hasFixedRateAttribute | hasTriggerAttribute)) { + if (!(hasCronAttribute || hasFixedDelayAttribute || hasFixedRateAttribute || hasTriggerAttribute)) { parserContext.getReaderContext().error( - "exactly one of the 'cron', 'fixed-delay', 'fixed-rate', or 'trigger' attributes is required", taskElement); - // Continue with the possible next task element - continue; + "one of the 'cron', 'fixed-delay', 'fixed-rate', or 'trigger' attributes is required", taskElement); + continue; // with the possible next task element } - if (hasCronAttribute) { - cronTaskMap.put(runnableBeanRef, cronAttribute); + if (hasInitialDelayAttribute && (hasCronAttribute || hasTriggerAttribute)) { + parserContext.getReaderContext().error( + "the 'initial-delay' attribute may not be used with cron and trigger tasks", taskElement); + continue; // with the possible next task element } + + String runnableName = + runnableReference(ref, method, taskElement, parserContext).getBeanName(); + if (hasFixedDelayAttribute) { - fixedDelayTaskMap.put(runnableBeanRef, fixedDelayAttribute); + fixedDelayTaskList.add(intervalTaskReference(runnableName, + initialDelayAttribute, fixedDelayAttribute, taskElement, parserContext)); } if (hasFixedRateAttribute) { - fixedRateTaskMap.put(runnableBeanRef, fixedRateAttribute); + fixedRateTaskList.add(intervalTaskReference(runnableName, + initialDelayAttribute, fixedRateAttribute, taskElement, parserContext)); + } + if (hasCronAttribute) { + cronTaskList.add(cronTaskReference(runnableName, cronAttribute, + taskElement, parserContext)); } if (hasTriggerAttribute) { - triggerTaskMap.put(runnableBeanRef, new RuntimeBeanReference(triggerAttribute)); + String triggerName = new RuntimeBeanReference(triggerAttribute).getBeanName(); + triggerTaskList.add(triggerTaskReference(runnableName, triggerName, + taskElement, parserContext)); } } String schedulerRef = element.getAttribute("scheduler"); if (StringUtils.hasText(schedulerRef)) { builder.addPropertyReference("taskScheduler", schedulerRef); } - builder.addPropertyValue("cronTasks", cronTaskMap); - builder.addPropertyValue("fixedDelayTasks", fixedDelayTaskMap); - builder.addPropertyValue("fixedRateTasks", fixedRateTaskMap); - builder.addPropertyValue("triggerTasks", triggerTaskMap); + builder.addPropertyValue("cronTasksList", cronTaskList); + builder.addPropertyValue("fixedDelayTasksList", fixedDelayTaskList); + builder.addPropertyValue("fixedRateTasksList", fixedRateTaskList); + builder.addPropertyValue("triggerTasksList", triggerTaskList); } private boolean isScheduledElement(Node node, ParserContext parserContext) { @@ -120,16 +134,49 @@ public class ScheduledTasksBeanDefinitionParser extends AbstractSingleBeanDefini ELEMENT_SCHEDULED.equals(parserContext.getDelegate().getLocalName(node)); } - private String createRunnableBean(String ref, String method, Element taskElement, ParserContext parserContext) { + private RuntimeBeanReference runnableReference(String ref, String method, Element taskElement, ParserContext parserContext) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( "org.springframework.scheduling.support.ScheduledMethodRunnable"); builder.addConstructorArgReference(ref); builder.addConstructorArgValue(method); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference intervalTaskReference(String runnableBeanName, + String initialDelay, String interval, Element taskElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.config.IntervalTask"); + builder.addConstructorArgReference(runnableBeanName); + builder.addConstructorArgValue(interval); + builder.addConstructorArgValue(StringUtils.hasLength(initialDelay) ? initialDelay : ZERO_INITIAL_DELAY); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference cronTaskReference(String runnableBeanName, + String cronExpression, Element taskElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.config.CronTask"); + builder.addConstructorArgReference(runnableBeanName); + builder.addConstructorArgValue(cronExpression); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference triggerTaskReference(String runnableBeanName, + String triggerBeanName, Element taskElement, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.scheduling.config.TriggerTask"); + builder.addConstructorArgReference(runnableBeanName); + builder.addConstructorArgReference(triggerBeanName); + return beanReference(taskElement, parserContext, builder); + } + + private RuntimeBeanReference beanReference(Element taskElement, + ParserContext parserContext, BeanDefinitionBuilder builder) { // Extract the source of the current task builder.getRawBeanDefinition().setSource(parserContext.extractSource(taskElement)); String generatedName = parserContext.getReaderContext().generateBeanName(builder.getRawBeanDefinition()); parserContext.registerBeanComponent(new BeanComponentDefinition(builder.getBeanDefinition(), generatedName)); - return generatedName; + return new RuntimeBeanReference(generatedName); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java new file mode 100644 index 00000000000..02dac4cba46 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2012 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.config; + +/** + * Holder class defining a {@code Runnable} to be executed as a task, typically at a + * scheduled time or interval. See subclass hierarchy for various scheduling approaches. + * + * @author Chris Beams + * @since 3.2 + */ +public class Task { + + private final Runnable runnable; + + + /** + * Create a new {@code Task}. + * @param runnable the underlying task to execute. + */ + public Task(Runnable runnable) { + this.runnable = runnable; + } + + + public Runnable getRunnable() { + return runnable; + } +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TriggerTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/TriggerTask.java new file mode 100644 index 00000000000..6d0aa6c8136 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TriggerTask.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2012 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.config; + +import org.springframework.scheduling.Trigger; + +/** + * {@link Task} implementation defining a {@code Runnable} to be executed according to a + * given {@link Trigger}. + * + * @author Chris Beams + * @since 3.2 + * @see Trigger#nextExecutionTime(org.springframework.scheduling.TriggerContext) + * @see ScheduledTaskRegistrar#setTriggerTasksList(java.util.List) + * @see org.springframework.scheduling.TaskScheduler#schedule(Runnable, Trigger) + */ +public class TriggerTask extends Task { + + private final Trigger trigger; + + + /** + * Create a new {@link TriggerTask}. + * @param runnable the underlying task to execute + * @param trigger specifies when the task should be executed + */ + public TriggerTask(Runnable runnable, Trigger trigger) { + super(runnable); + this.trigger = trigger; + } + + + public Trigger getTrigger() { + return trigger; + } +} diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java index 2e1526ac66c..b1753f92757 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronSequenceGenerator.java @@ -339,6 +339,10 @@ public class CronSequenceGenerator { return result; } + String getExpression() { + return this.expression; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof CronSequenceGenerator)) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index a3c8fc8df03..f23b327cf80 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -72,6 +72,9 @@ public class CronTrigger implements Trigger { return this.sequenceGenerator.next(date); } + public String getExpression() { + return this.sequenceGenerator.getExpression(); + } @Override public boolean equals(Object obj) { diff --git a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd index 3091b0ba071..cb05f8fcd3c 100644 --- a/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd +++ b/spring-context/src/main/resources/org/springframework/scheduling/config/spring-task-3.2.xsd @@ -229,6 +229,14 @@ ]]> + + + + + fixedDelayTasks = (Map) + @SuppressWarnings("unchecked") + List fixedDelayTasks = (List) new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks"); assertEquals(1, fixedDelayTasks.size()); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) fixedDelayTasks.keySet().iterator().next(); + IntervalTask task = fixedDelayTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); Object targetObject = runnable.getTarget(); Method targetMethod = runnable.getMethod(); assertEquals(target, targetObject); assertEquals("fixedDelay", targetMethod.getName()); - assertEquals(new Long(5000), fixedDelayTasks.values().iterator().next()); + assertEquals(0L, task.getInitialDelay()); + assertEquals(5000L, task.getInterval()); } @Test @@ -80,15 +87,45 @@ public class ScheduledAnnotationBeanPostProcessorTests { Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); - Map fixedRateTasks = (Map) + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertEquals(1, fixedRateTasks.size()); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) fixedRateTasks.keySet().iterator().next(); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); Object targetObject = runnable.getTarget(); Method targetMethod = runnable.getMethod(); assertEquals(target, targetObject); assertEquals("fixedRate", targetMethod.getName()); - assertEquals(new Long(3000), fixedRateTasks.values().iterator().next()); + assertEquals(0L, task.getInitialDelay()); + assertEquals(3000L, task.getInterval()); + } + + @Test + public void fixedRateTaskWithInitialDelay() { + StaticApplicationContext context = new StaticApplicationContext(); + BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); + BeanDefinition targetDefinition = new RootBeanDefinition( + ScheduledAnnotationBeanPostProcessorTests.FixedRateWithInitialDelayTestBean.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(1, fixedRateTasks.size()); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); + Object targetObject = runnable.getTarget(); + Method targetMethod = runnable.getMethod(); + assertEquals(target, targetObject); + assertEquals("fixedRate", targetMethod.getName()); + assertEquals(1000L, task.getInitialDelay()); + assertEquals(3000L, task.getInterval()); } @Test @@ -104,15 +141,17 @@ public class ScheduledAnnotationBeanPostProcessorTests { Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); - Map cronTasks = (Map) + @SuppressWarnings("unchecked") + List cronTasks = (List) new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertEquals(1, cronTasks.size()); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) cronTasks.keySet().iterator().next(); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); Object targetObject = runnable.getTarget(); Method targetMethod = runnable.getMethod(); assertEquals(target, targetObject); assertEquals("cron", targetMethod.getName()); - assertEquals("*/7 * * * * ?", cronTasks.values().iterator().next()); + assertEquals("*/7 * * * * ?", task.getExpression()); Thread.sleep(10000); } @@ -129,15 +168,17 @@ public class ScheduledAnnotationBeanPostProcessorTests { Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); - Map fixedRateTasks = (Map) + @SuppressWarnings("unchecked") + List fixedRateTasks = (List) new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks"); assertEquals(1, fixedRateTasks.size()); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) fixedRateTasks.keySet().iterator().next(); + IntervalTask task = fixedRateTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); Object targetObject = runnable.getTarget(); Method targetMethod = runnable.getMethod(); assertEquals(target, targetObject); assertEquals("checkForUpdates", targetMethod.getName()); - assertEquals(new Long(5000), fixedRateTasks.values().iterator().next()); + assertEquals(5000L, task.getInterval()); } @Test @@ -153,15 +194,17 @@ public class ScheduledAnnotationBeanPostProcessorTests { Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); - Map cronTasks = (Map) + @SuppressWarnings("unchecked") + List cronTasks = (List) new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertEquals(1, cronTasks.size()); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) cronTasks.keySet().iterator().next(); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); Object targetObject = runnable.getTarget(); Method targetMethod = runnable.getMethod(); assertEquals(target, targetObject); assertEquals("generateReport", targetMethod.getName()); - assertEquals("0 0 * * * ?", cronTasks.values().iterator().next()); + assertEquals("0 0 * * * ?", task.getExpression()); } @Test @@ -183,15 +226,17 @@ public class ScheduledAnnotationBeanPostProcessorTests { Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); - Map cronTasks = (Map) + @SuppressWarnings("unchecked") + List cronTasks = (List) new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertEquals(1, cronTasks.size()); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) cronTasks.keySet().iterator().next(); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); Object targetObject = runnable.getTarget(); Method targetMethod = runnable.getMethod(); assertEquals(target, targetObject); assertEquals("x", targetMethod.getName()); - assertEquals(businessHoursCronExpression, cronTasks.values().iterator().next()); + assertEquals(businessHoursCronExpression, task.getExpression()); } @Test @@ -213,15 +258,17 @@ public class ScheduledAnnotationBeanPostProcessorTests { Object target = context.getBean("target"); ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar) new DirectFieldAccessor(postProcessor).getPropertyValue("registrar"); - Map cronTasks = (Map) + @SuppressWarnings("unchecked") + List cronTasks = (List) new DirectFieldAccessor(registrar).getPropertyValue("cronTasks"); assertEquals(1, cronTasks.size()); - ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) cronTasks.keySet().iterator().next(); + CronTask task = cronTasks.get(0); + ScheduledMethodRunnable runnable = (ScheduledMethodRunnable) task.getRunnable(); Object targetObject = runnable.getTarget(); Method targetMethod = runnable.getMethod(); assertEquals(target, targetObject); assertEquals("y", targetMethod.getName()); - assertEquals(businessHoursCronExpression, cronTasks.values().iterator().next()); + assertEquals(businessHoursCronExpression, task.getExpression()); } @Test(expected = BeanCreationException.class) @@ -236,14 +283,19 @@ public class ScheduledAnnotationBeanPostProcessorTests { } @Test(expected = IllegalArgumentException.class) - public void invalidCron() { + public void invalidCron() throws Throwable { StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class); BeanDefinition targetDefinition = new RootBeanDefinition( ScheduledAnnotationBeanPostProcessorTests.InvalidCronTestBean.class); context.registerBeanDefinition("postProcessor", processorDefinition); context.registerBeanDefinition("target", targetDefinition); - context.refresh(); + try { + context.refresh(); + fail("expected exception"); + } catch (BeanCreationException ex) { + throw ex.getRootCause(); + } } @Test(expected = BeanCreationException.class) @@ -269,7 +321,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - public static class FixedDelayTestBean { + static class FixedDelayTestBean { @Scheduled(fixedDelay=5000) public void fixedDelay() { @@ -277,7 +329,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - public static class FixedRateTestBean { + static class FixedRateTestBean { @Scheduled(fixedRate=3000) public void fixedRate() { @@ -285,7 +337,15 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - public static class CronTestBean { + static class FixedRateWithInitialDelayTestBean { + + @Scheduled(initialDelay=1000, fixedRate=3000) + public void fixedRate() { + } + } + + + static class CronTestBean { @Scheduled(cron="*/7 * * * * ?") public void cron() throws IOException { @@ -295,7 +355,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - private static class EmptyAnnotationTestBean { + static class EmptyAnnotationTestBean { @Scheduled public void invalid() { @@ -304,7 +364,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - private static class InvalidCronTestBean { + static class InvalidCronTestBean { @Scheduled(cron="abc") public void invalid() { @@ -313,7 +373,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - private static class NonVoidReturnTypeTestBean { + static class NonVoidReturnTypeTestBean { @Scheduled(fixedRate=3000) public String invalid() { @@ -323,7 +383,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - private static class NonEmptyParamListTestBean { + static class NonEmptyParamListTestBean { @Scheduled(fixedRate=3000) public void invalid(String oops) { @@ -344,7 +404,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { private static @interface Hourly {} - private static class MetaAnnotationFixedRateTestBean { + static class MetaAnnotationFixedRateTestBean { @EveryFiveSeconds public void checkForUpdates() { @@ -352,7 +412,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - private static class MetaAnnotationCronTestBean { + static class MetaAnnotationCronTestBean { @Hourly public void generateReport() { @@ -360,7 +420,7 @@ public class ScheduledAnnotationBeanPostProcessorTests { } - private static class PropertyPlaceholderTestBean { + static class PropertyPlaceholderTestBean { @Scheduled(cron = "${schedules.businessHours}") public void x() { @@ -370,11 +430,11 @@ public class ScheduledAnnotationBeanPostProcessorTests { @Scheduled(cron = "${schedules.businessHours}") @Target(ElementType.METHOD) - @Retention(RetentionPolicy.RUNTIME) + @Retention(RetentionPolicy.RUNTIME) private static @interface BusinessHours {} - private static class PropertyPlaceholderMetaAnnotationTestBean { + static class PropertyPlaceholderMetaAnnotationTestBean { @BusinessHours public void y() { diff --git a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java index f24355093f9..5e8f75a3587 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/config/ScheduledTasksBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -17,11 +17,9 @@ package org.springframework.scheduling.config; import java.lang.reflect.Method; -import java.util.Collection; import java.util.Date; -import java.util.Map; +import java.util.List; -import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; @@ -32,8 +30,13 @@ import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.scheduling.support.ScheduledMethodRunnable; +import static org.hamcrest.CoreMatchers.*; + +import static org.junit.Assert.*; + /** * @author Mark Fisher + * @author Chris Beams */ @SuppressWarnings("unchecked") public class ScheduledTasksBeanDefinitionParserTests { @@ -63,9 +66,9 @@ public class ScheduledTasksBeanDefinitionParserTests { @Test public void checkTarget() { - Map tasks = (Map) new DirectFieldAccessor( + List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("fixedRateTasks"); - Runnable runnable = tasks.keySet().iterator().next(); + Runnable runnable = tasks.get(0).getRunnable(); assertEquals(ScheduledMethodRunnable.class, runnable.getClass()); Object targetObject = ((ScheduledMethodRunnable) runnable).getTarget(); Method targetMethod = ((ScheduledMethodRunnable) runnable).getMethod(); @@ -75,39 +78,39 @@ public class ScheduledTasksBeanDefinitionParserTests { @Test public void fixedRateTasks() { - Map tasks = (Map) new DirectFieldAccessor( + List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("fixedRateTasks"); - assertEquals(2, tasks.size()); - Collection values = tasks.values(); - assertTrue(values.contains(new Long(1000))); - assertTrue(values.contains(new Long(2000))); + assertEquals(3, tasks.size()); + assertEquals(1000L, tasks.get(0).getInterval()); + assertEquals(2000L, tasks.get(1).getInterval()); + assertEquals(4000L, tasks.get(2).getInterval()); + assertEquals(500, tasks.get(2).getInitialDelay()); } @Test public void fixedDelayTasks() { - Map tasks = (Map) new DirectFieldAccessor( + List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("fixedDelayTasks"); - assertEquals(1, tasks.size()); - Long value = tasks.values().iterator().next(); - assertEquals(new Long(3000), value); + assertEquals(2, tasks.size()); + assertEquals(3000L, tasks.get(0).getInterval()); + assertEquals(3500L, tasks.get(1).getInterval()); + assertEquals(250, tasks.get(1).getInitialDelay()); } @Test public void cronTasks() { - Map tasks = (Map) new DirectFieldAccessor( + List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("cronTasks"); assertEquals(1, tasks.size()); - String expression = tasks.values().iterator().next(); - assertEquals("*/4 * 9-17 * * MON-FRI", expression); + assertEquals("*/4 * 9-17 * * MON-FRI", tasks.get(0).getExpression()); } @Test public void triggerTasks() { - Map tasks = (Map) new DirectFieldAccessor( + List tasks = (List) new DirectFieldAccessor( this.registrar).getPropertyValue("triggerTasks"); assertEquals(1, tasks.size()); - Trigger trigger = tasks.values().iterator().next(); - assertEquals(TestTrigger.class, trigger.getClass()); + assertThat(tasks.get(0).getTrigger(), instanceOf(TestTrigger.class)); } diff --git a/spring-context/src/test/resources/org/springframework/scheduling/config/scheduledTasksContext.xml b/spring-context/src/test/resources/org/springframework/scheduling/config/scheduledTasksContext.xml index 30e46c7bb8a..48f5ebdb072 100644 --- a/spring-context/src/test/resources/org/springframework/scheduling/config/scheduledTasksContext.xml +++ b/spring-context/src/test/resources/org/springframework/scheduling/config/scheduledTasksContext.xml @@ -11,8 +11,10 @@ + + diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 92ce99c761d..64b4ee18a98 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -31,6 +31,7 @@ Changes in version 3.2 M1 * add required flag to @RequestBody annotation * support executor qualification with @Async#value (SPR-6847) * add convenient WebAppInitializer base classes (SPR-9300) +* support initial delay attribute for scheduled tasks (SPR-7022) Changes in version 3.1.1 (2012-02-16) ------------------------------------- diff --git a/src/reference/docbook/scheduling.xml b/src/reference/docbook/scheduling.xml index 75b31ba03e6..61db3be7985 100644 --- a/src/reference/docbook/scheduling.xml +++ b/src/reference/docbook/scheduling.xml @@ -472,7 +472,7 @@ public class TaskExecutorExample { example. <task:scheduled-tasks scheduler="myScheduler"> - <task:scheduled ref="someObject" method="someMethod" fixed-delay="5000"/> + <task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/> </task:scheduled-tasks> <task:scheduler id="myScheduler" pool-size="10"/> @@ -480,13 +480,19 @@ public class TaskExecutorExample { As you can see, the scheduler is referenced by the outer element, and each individual task includes the configuration of its trigger metadata. In the preceding example, that metadata defines a periodic - trigger with a fixed delay. It could also be configured with a - "fixed-rate", or for more control, a "cron" attribute could be provided - instead. Here's an example featuring these other options. + trigger with a fixed delay indicating the number of milliseconds to wait + after each task execution has completed. Another option is 'fixed-rate', + indicating how often the method should be executed regardless of how long + any previous execution takes. Additionally, for both fixed-delay and + fixed-rate tasks an 'initial-delay' parameter may be specified indicating + the number of milliseconds to wait before the first execution of the + method. For more control, a "cron" attribute may be provided instead. + Here is an example demonstrating these other options. <task:scheduled-tasks scheduler="myScheduler"> - <task:scheduled ref="someObject" method="someMethod" fixed-rate="5000"/> - <task:scheduled ref="anotherObject" method="anotherMethod" cron="*/5 * * * * MON-FRI"/> + <task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/> + <task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/> + <task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/> </task:scheduled-tasks> <task:scheduler id="myScheduler" pool-size="10"/> @@ -523,6 +529,16 @@ public void doSomething() { // something that should execute periodically } + For fixed-delay and fixed-rate tasks, an initial delay may be + specified indicating the number of milliseconds to wait before the first + execution of the method. + + + @Scheduled(initialDelay=1000, fixedRate=5000) +public void doSomething() { + // something that should execute periodically +} + If simple periodic scheduling is not expressive enough, then a cron expression may be provided. For example, the following will only execute on weekdays.