From 79d5127bb7ead4414b9a09243baad8d2bcb634c1 Mon Sep 17 00:00:00 2001 From: Mark Fisher Date: Sat, 6 Jun 2009 01:57:45 +0000 Subject: [PATCH] Initial commit of @Scheduled annotation and ScheduledAnnotationBeanPostProcessor. git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1315 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../scheduling/annotation/Scheduled.java | 48 ++++ .../ScheduledAnnotationBeanPostProcessor.java | 147 ++++++++++++ ...duledAnnotationBeanPostProcessorTests.java | 219 ++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 org.springframework.context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java create mode 100644 org.springframework.context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java create mode 100644 org.springframework.context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java 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) { + } + + } + +}