diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java
new file mode 100644
index 00000000000..8edbda06ae7
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2002-2009 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;
+
+/**
+ * Annotation that marks a method to be scheduled. Exactly one of the
+ * cron, fixedDelay, or fixedRate
+ * attributes must be provided.
+ *
+ *
The annotated method must expect no arguments and have a
+ * void return type.
+ *
+ * @author Mark Fisher
+ * @since 3.0
+ * @see ScheduledAnnotationBeanPostProcessor
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Scheduled {
+
+ String cron() default "";
+
+ long fixedDelay() default -1;
+
+ long fixedRate() default -1;
+
+}
diff --git a/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java
new file mode 100644
index 00000000000..adbcfaac268
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2002-2009 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.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.aop.support.AopUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+import org.springframework.scheduling.support.MethodInvokingRunnable;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.ReflectionUtils.MethodCallback;
+
+/**
+ * Bean post-processor that registers methods annotated with
+ * {@link Scheduled @Scheduled} to be invoked by a TaskScheduler according
+ * to the fixedRate, fixedDelay, or cron expression provided via the annotation.
+ *
+ * @author Mark Fisher
+ * @since 3.0
+ * @see Scheduled
+ */
+public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered,
+ ApplicationListener, DisposableBean {
+
+ private Object scheduler;
+
+ private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
+
+ private final Map cronTasks = new HashMap();
+
+ private final Map fixedDelayTasks = new HashMap();
+
+ private final Map fixedRateTasks = new HashMap();
+
+
+ /**
+ * Set the {@link org.springframework.scheduling.TaskScheduler} that will
+ * invoke the scheduled methods or a
+ * {@link java.util.concurrent.ScheduledExecutorService} to be wrapped
+ * within an instance of
+ * {@link org.springframework.scheduling.concurrent.ConcurrentTaskScheduler}.
+ */
+ public void setScheduler(Object scheduler) {
+ this.scheduler = scheduler;
+ }
+
+ public int getOrder() {
+ return LOWEST_PRECEDENCE;
+ }
+
+ public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+ return bean;
+ }
+
+ public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException {
+ Class> targetClass = AopUtils.getTargetClass(bean);
+ if (targetClass == null) {
+ return 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) {
+ 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.");
+ MethodInvokingRunnable runnable = new MethodInvokingRunnable();
+ runnable.setTargetObject(bean);
+ runnable.setTargetMethod(method.getName());
+ runnable.setArguments(new Object[0]);
+ try {
+ runnable.prepare();
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("failed to prepare task", e);
+ }
+ boolean processedSchedule = false;
+ String errorMessage = "Exactly one of 'cron', 'fixedDelay', or 'fixedRate' is required.";
+ String cron = annotation.cron();
+ if (!"".equals(cron)) {
+ processedSchedule = true;
+ cronTasks.put(runnable, cron);
+ }
+ long fixedDelay = annotation.fixedDelay();
+ if (fixedDelay >= 0) {
+ Assert.isTrue(!processedSchedule, errorMessage);
+ processedSchedule = true;
+ fixedDelayTasks.put(runnable, new Long(fixedDelay));
+ }
+ long fixedRate = annotation.fixedRate();
+ if (fixedRate >= 0) {
+ Assert.isTrue(!processedSchedule, errorMessage);
+ processedSchedule = true;
+ fixedRateTasks.put(runnable, new Long(fixedRate));
+ }
+ Assert.isTrue(processedSchedule, errorMessage);
+ }
+ }
+ });
+ return bean;
+ }
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ if (scheduler != null) {
+ this.registrar.setScheduler(scheduler);
+ }
+ this.registrar.setCronTasks(this.cronTasks);
+ this.registrar.setFixedDelayTasks(this.fixedDelayTasks);
+ this.registrar.setFixedRateTasks(this.fixedRateTasks);
+ this.registrar.afterPropertiesSet();
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ if (this.registrar != null) {
+ this.registrar.destroy();
+ }
+ }
+
+}
diff --git a/org.springframework.context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java b/org.springframework.context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java
new file mode 100644
index 00000000000..40aa8fed043
--- /dev/null
+++ b/org.springframework.context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2002-2009 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 static org.junit.Assert.assertEquals;
+
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.beans.DirectFieldAccessor;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+import org.springframework.scheduling.support.MethodInvokingRunnable;
+
+/**
+ * @author Mark Fisher
+ */
+@SuppressWarnings("unchecked")
+public class ScheduledAnnotationBeanPostProcessorTests {
+
+ @Test
+ public void fixedDelayTask() {
+ StaticApplicationContext context = new StaticApplicationContext();
+ BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
+ BeanDefinition targetDefinition = new RootBeanDefinition(
+ ScheduledAnnotationBeanPostProcessorTests.FixedDelayTestBean.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");
+ Map fixedDelayTasks = (Map)
+ new DirectFieldAccessor(registrar).getPropertyValue("fixedDelayTasks");
+ assertEquals(1, fixedDelayTasks.size());
+ MethodInvokingRunnable runnable = (MethodInvokingRunnable) fixedDelayTasks.keySet().iterator().next();
+ Object targetObject = runnable.getTargetObject();
+ String targetMethod = runnable.getTargetMethod();
+ assertEquals(target, targetObject);
+ assertEquals("fixedDelay", targetMethod);
+ assertEquals(new Long(5000), fixedDelayTasks.values().iterator().next());
+ }
+
+ @Test
+ public void fixedRateTask() {
+ StaticApplicationContext context = new StaticApplicationContext();
+ BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
+ BeanDefinition targetDefinition = new RootBeanDefinition(
+ ScheduledAnnotationBeanPostProcessorTests.FixedRateTestBean.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");
+ Map fixedRateTasks = (Map)
+ new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
+ assertEquals(1, fixedRateTasks.size());
+ MethodInvokingRunnable runnable = (MethodInvokingRunnable) fixedRateTasks.keySet().iterator().next();
+ Object targetObject = runnable.getTargetObject();
+ String targetMethod = runnable.getTargetMethod();
+ assertEquals(target, targetObject);
+ assertEquals("fixedRate", targetMethod);
+ assertEquals(new Long(3000), fixedRateTasks.values().iterator().next());
+ }
+
+ @Test
+ public void cronTask() {
+ StaticApplicationContext context = new StaticApplicationContext();
+ BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
+ BeanDefinition targetDefinition = new RootBeanDefinition(
+ ScheduledAnnotationBeanPostProcessorTests.CronTestBean.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");
+ Map cronTasks = (Map)
+ new DirectFieldAccessor(registrar).getPropertyValue("cronTasks");
+ assertEquals(1, cronTasks.size());
+ MethodInvokingRunnable runnable = (MethodInvokingRunnable) cronTasks.keySet().iterator().next();
+ Object targetObject = runnable.getTargetObject();
+ String targetMethod = runnable.getTargetMethod();
+ assertEquals(target, targetObject);
+ assertEquals("cron", targetMethod);
+ assertEquals("*/7 * * * * ?", cronTasks.values().iterator().next());
+ }
+
+ @Test(expected = BeanCreationException.class)
+ public void emptyAnnotation() {
+ StaticApplicationContext context = new StaticApplicationContext();
+ BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
+ BeanDefinition targetDefinition = new RootBeanDefinition(
+ ScheduledAnnotationBeanPostProcessorTests.EmptyAnnotationTestBean.class);
+ context.registerBeanDefinition("postProcessor", processorDefinition);
+ context.registerBeanDefinition("target", targetDefinition);
+ context.refresh();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void invalidCron() {
+ 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();
+ }
+
+ @Test(expected = BeanCreationException.class)
+ public void nonVoidReturnType() {
+ StaticApplicationContext context = new StaticApplicationContext();
+ BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
+ BeanDefinition targetDefinition = new RootBeanDefinition(
+ ScheduledAnnotationBeanPostProcessorTests.NonVoidReturnTypeTestBean.class);
+ context.registerBeanDefinition("postProcessor", processorDefinition);
+ context.registerBeanDefinition("target", targetDefinition);
+ context.refresh();
+ }
+
+ @Test(expected = BeanCreationException.class)
+ public void nonEmptyParamList() {
+ StaticApplicationContext context = new StaticApplicationContext();
+ BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
+ BeanDefinition targetDefinition = new RootBeanDefinition(
+ ScheduledAnnotationBeanPostProcessorTests.NonEmptyParamListTestBean.class);
+ context.registerBeanDefinition("postProcessor", processorDefinition);
+ context.registerBeanDefinition("target", targetDefinition);
+ context.refresh();
+ }
+
+
+ private static class FixedDelayTestBean {
+
+ @Scheduled(fixedDelay=5000)
+ public void fixedDelay() {
+ }
+
+ }
+
+
+ private static class FixedRateTestBean {
+
+ @Scheduled(fixedRate=3000)
+ public void fixedRate() {
+ }
+
+ }
+
+
+ private static class CronTestBean {
+
+ @Scheduled(cron="*/7 * * * * ?")
+ public void cron() {
+ }
+
+ }
+
+
+ private static class EmptyAnnotationTestBean {
+
+ @Scheduled
+ public void invalid() {
+ }
+
+ }
+
+
+ private static class InvalidCronTestBean {
+
+ @Scheduled(cron="abc")
+ public void invalid() {
+ }
+
+ }
+
+
+ private static class NonVoidReturnTypeTestBean {
+
+ @Scheduled(fixedRate=3000)
+ public String invalid() {
+ return "oops";
+ }
+
+ }
+
+
+ private static class NonEmptyParamListTestBean {
+
+ @Scheduled(fixedRate=3000)
+ public void invalid(String oops) {
+ }
+
+ }
+
+}