Added Java 8 compliant @Schedules container annotation for @Scheduled
Issue: SPR-10532
This commit is contained in:
parent
ead0124b39
commit
f9325a8376
|
|
@ -18,6 +18,7 @@ package org.springframework.scheduling.annotation;
|
|||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
|
@ -45,6 +46,7 @@ import java.lang.annotation.Target;
|
|||
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Repeatable(Schedules.class)
|
||||
public @interface Scheduled {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -110,120 +110,20 @@ public class ScheduledAnnotationBeanPostProcessor
|
|||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(final Object bean, String beanName) {
|
||||
final Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
|
||||
@Override
|
||||
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
||||
Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class);
|
||||
if (annotation != null) {
|
||||
try {
|
||||
Assert.isTrue(void.class.equals(method.getReturnType()),
|
||||
"Only void-returning methods may be annotated with @Scheduled");
|
||||
Assert.isTrue(method.getParameterTypes().length == 0,
|
||||
"Only no-arg methods may be annotated with @Scheduled");
|
||||
if (AopUtils.isJdkDynamicProxy(bean)) {
|
||||
try {
|
||||
// found a @Scheduled method on the target class for this JDK proxy -> is it
|
||||
// also present on the proxy itself?
|
||||
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
|
||||
}
|
||||
catch (SecurityException ex) {
|
||||
ReflectionUtils.handleReflectionException(ex);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"@Scheduled method '%s' found on bean target class '%s', " +
|
||||
"but not found in any interface(s) for bean JDK proxy. Either " +
|
||||
"pull the method up to an interface or switch to subclass (CGLIB) " +
|
||||
"proxies by setting proxy-target-class/proxyTargetClass " +
|
||||
"attribute to 'true'", method.getName(), targetClass.getSimpleName()));
|
||||
}
|
||||
}
|
||||
Runnable runnable = new ScheduledMethodRunnable(bean, method);
|
||||
boolean processedSchedule = false;
|
||||
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
|
||||
// Determine initial delay
|
||||
long initialDelay = annotation.initialDelay();
|
||||
String initialDelayString = annotation.initialDelayString();
|
||||
if (!"".equals(initialDelayString)) {
|
||||
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
|
||||
if (embeddedValueResolver != null) {
|
||||
initialDelayString = embeddedValueResolver.resolveStringValue(initialDelayString);
|
||||
}
|
||||
try {
|
||||
initialDelay = Integer.parseInt(initialDelayString);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
|
||||
}
|
||||
}
|
||||
// Check cron expression
|
||||
String cron = annotation.cron();
|
||||
if (!"".equals(cron)) {
|
||||
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
|
||||
processedSchedule = true;
|
||||
if (embeddedValueResolver != null) {
|
||||
cron = embeddedValueResolver.resolveStringValue(cron);
|
||||
}
|
||||
registrar.addCronTask(new CronTask(runnable, cron));
|
||||
}
|
||||
// At this point we don't need to differentiate between initial delay set or not anymore
|
||||
if (initialDelay < 0) {
|
||||
initialDelay = 0;
|
||||
}
|
||||
// Check fixed delay
|
||||
long fixedDelay = annotation.fixedDelay();
|
||||
if (fixedDelay >= 0) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
||||
}
|
||||
String fixedDelayString = annotation.fixedDelayString();
|
||||
if (!"".equals(fixedDelayString)) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
if (embeddedValueResolver != null) {
|
||||
fixedDelayString = embeddedValueResolver.resolveStringValue(fixedDelayString);
|
||||
}
|
||||
try {
|
||||
fixedDelay = Integer.parseInt(fixedDelayString);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
|
||||
}
|
||||
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
||||
}
|
||||
// Check fixed rate
|
||||
long fixedRate = annotation.fixedRate();
|
||||
if (fixedRate >= 0) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
||||
}
|
||||
String fixedRateString = annotation.fixedRateString();
|
||||
if (!"".equals(fixedRateString)) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
if (embeddedValueResolver != null) {
|
||||
fixedRateString = embeddedValueResolver.resolveStringValue(fixedRateString);
|
||||
}
|
||||
try {
|
||||
fixedRate = Integer.parseInt(fixedRateString);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
|
||||
}
|
||||
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
||||
}
|
||||
// Check whether we had any attribute set
|
||||
Assert.isTrue(processedSchedule, errorMessage);
|
||||
Schedules schedules = AnnotationUtils.getAnnotation(method, Schedules.class);
|
||||
if (schedules != null) {
|
||||
for (Scheduled scheduled : schedules.value()) {
|
||||
processScheduled(scheduled, method, bean);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException(
|
||||
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
|
||||
}
|
||||
else {
|
||||
Scheduled scheduled = AnnotationUtils.getAnnotation(method, Scheduled.class);
|
||||
if (scheduled != null) {
|
||||
processScheduled(scheduled, method, bean);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -231,23 +131,146 @@ public class ScheduledAnnotationBeanPostProcessor
|
|||
return bean;
|
||||
}
|
||||
|
||||
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
|
||||
try {
|
||||
Assert.isTrue(void.class.equals(method.getReturnType()),
|
||||
"Only void-returning methods may be annotated with @Scheduled");
|
||||
Assert.isTrue(method.getParameterTypes().length == 0,
|
||||
"Only no-arg methods may be annotated with @Scheduled");
|
||||
|
||||
if (AopUtils.isJdkDynamicProxy(bean)) {
|
||||
try {
|
||||
// found a @Scheduled method on the target class for this JDK proxy -> is it
|
||||
// also present on the proxy itself?
|
||||
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
|
||||
}
|
||||
catch (SecurityException ex) {
|
||||
ReflectionUtils.handleReflectionException(ex);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"@Scheduled method '%s' found on bean target class '%s', " +
|
||||
"but not found in any interface(s) for bean JDK proxy. Either " +
|
||||
"pull the method up to an interface or switch to subclass (CGLIB) " +
|
||||
"proxies by setting proxy-target-class/proxyTargetClass " +
|
||||
"attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()));
|
||||
}
|
||||
}
|
||||
|
||||
Runnable runnable = new ScheduledMethodRunnable(bean, method);
|
||||
boolean processedSchedule = false;
|
||||
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
|
||||
|
||||
// Determine initial delay
|
||||
long initialDelay = scheduled.initialDelay();
|
||||
String initialDelayString = scheduled.initialDelayString();
|
||||
if (!"".equals(initialDelayString)) {
|
||||
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
|
||||
if (this.embeddedValueResolver != null) {
|
||||
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
|
||||
}
|
||||
try {
|
||||
initialDelay = Integer.parseInt(initialDelayString);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
|
||||
}
|
||||
}
|
||||
|
||||
// Check cron expression
|
||||
String cron = scheduled.cron();
|
||||
if (!"".equals(cron)) {
|
||||
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
|
||||
processedSchedule = true;
|
||||
if (this.embeddedValueResolver != null) {
|
||||
cron = this.embeddedValueResolver.resolveStringValue(cron);
|
||||
}
|
||||
this.registrar.addCronTask(new CronTask(runnable, cron));
|
||||
}
|
||||
|
||||
// At this point we don't need to differentiate between initial delay set or not anymore
|
||||
if (initialDelay < 0) {
|
||||
initialDelay = 0;
|
||||
}
|
||||
|
||||
// Check fixed delay
|
||||
long fixedDelay = scheduled.fixedDelay();
|
||||
if (fixedDelay >= 0) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
||||
}
|
||||
String fixedDelayString = scheduled.fixedDelayString();
|
||||
if (!"".equals(fixedDelayString)) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
if (this.embeddedValueResolver != null) {
|
||||
fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
|
||||
}
|
||||
try {
|
||||
fixedDelay = Integer.parseInt(fixedDelayString);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
|
||||
}
|
||||
this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
||||
}
|
||||
|
||||
// Check fixed rate
|
||||
long fixedRate = scheduled.fixedRate();
|
||||
if (fixedRate >= 0) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
||||
}
|
||||
String fixedRateString = scheduled.fixedRateString();
|
||||
if (!"".equals(fixedRateString)) {
|
||||
Assert.isTrue(!processedSchedule, errorMessage);
|
||||
processedSchedule = true;
|
||||
if (this.embeddedValueResolver != null) {
|
||||
fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
|
||||
}
|
||||
try {
|
||||
fixedRate = Integer.parseInt(fixedRateString);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
|
||||
}
|
||||
this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
||||
}
|
||||
|
||||
// Check whether we had any attribute set
|
||||
Assert.isTrue(processedSchedule, errorMessage);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException(
|
||||
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (event.getApplicationContext() != this.applicationContext) {
|
||||
return;
|
||||
}
|
||||
Map<String, SchedulingConfigurer> configurers =
|
||||
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
|
||||
|
||||
if (this.scheduler != null) {
|
||||
this.registrar.setScheduler(this.scheduler);
|
||||
}
|
||||
|
||||
Map<String, SchedulingConfigurer> configurers =
|
||||
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
|
||||
for (SchedulingConfigurer configurer : configurers.values()) {
|
||||
configurer.configureTasks(this.registrar);
|
||||
}
|
||||
|
||||
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
|
||||
Map<String, ? super Object> schedulers = new HashMap<String, Object>();
|
||||
schedulers.putAll(applicationContext.getBeansOfType(TaskScheduler.class));
|
||||
schedulers.putAll(applicationContext.getBeansOfType(ScheduledExecutorService.class));
|
||||
schedulers.putAll(this.applicationContext.getBeansOfType(TaskScheduler.class));
|
||||
schedulers.putAll(this.applicationContext.getBeansOfType(ScheduledExecutorService.class));
|
||||
if (schedulers.size() == 0) {
|
||||
// do nothing -> fall back to default scheduler
|
||||
}
|
||||
|
|
@ -263,14 +286,13 @@ public class ScheduledAnnotationBeanPostProcessor
|
|||
"configureTasks() callback. Found the following beans: " + schedulers.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
this.registrar.afterPropertiesSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if (this.registrar != null) {
|
||||
this.registrar.destroy();
|
||||
}
|
||||
public void destroy() {
|
||||
this.registrar.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.scheduling.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Container annotation that aggregates several {@link Scheduled} annotations.
|
||||
*
|
||||
* <p>Can be used natively, declaring several nested {@link Scheduled} annotations.
|
||||
* Can also be used in conjunction with Java 8's support for repeatable annotations,
|
||||
* where {@link Scheduled} can simply be declared several times on the same method,
|
||||
* implicitly generating this container annotation.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.0
|
||||
* @see Scheduled
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Schedules {
|
||||
|
||||
Scheduled[] value();
|
||||
|
||||
}
|
||||
|
|
@ -128,6 +128,40 @@ public class ScheduledAnnotationBeanPostProcessorTests {
|
|||
assertEquals(3000L, task.getInterval());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void severalFixedRates() {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
|
||||
BeanDefinition targetDefinition = new RootBeanDefinition(SeveralFixedRatesTestBean.class);
|
||||
context.registerBeanDefinition("postProcessor", processorDefinition);
|
||||
context.registerBeanDefinition("target", targetDefinition);
|
||||
context.refresh();
|
||||
Object postProcessor = context.getBean("postProcessor");
|
||||
Object target = context.getBean("target");
|
||||
ScheduledTaskRegistrar registrar = (ScheduledTaskRegistrar)
|
||||
new DirectFieldAccessor(postProcessor).getPropertyValue("registrar");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<IntervalTask> fixedRateTasks = (List<IntervalTask>)
|
||||
new DirectFieldAccessor(registrar).getPropertyValue("fixedRateTasks");
|
||||
assertEquals(2, fixedRateTasks.size());
|
||||
IntervalTask task1 = fixedRateTasks.get(0);
|
||||
ScheduledMethodRunnable runnable1 = (ScheduledMethodRunnable) task1.getRunnable();
|
||||
Object targetObject = runnable1.getTarget();
|
||||
Method targetMethod = runnable1.getMethod();
|
||||
assertEquals(target, targetObject);
|
||||
assertEquals("fixedRate", targetMethod.getName());
|
||||
assertEquals(0, task1.getInitialDelay());
|
||||
assertEquals(4000L, task1.getInterval());
|
||||
IntervalTask task2 = fixedRateTasks.get(1);
|
||||
ScheduledMethodRunnable runnable2 = (ScheduledMethodRunnable) task2.getRunnable();
|
||||
targetObject = runnable2.getTarget();
|
||||
targetMethod = runnable2.getMethod();
|
||||
assertEquals(target, targetObject);
|
||||
assertEquals("fixedRate", targetMethod.getName());
|
||||
assertEquals(2000L, task2.getInitialDelay());
|
||||
assertEquals(4000L, task2.getInterval());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cronTask() throws InterruptedException {
|
||||
Assume.group(TestGroup.LONG_RUNNING);
|
||||
|
|
@ -404,6 +438,15 @@ public class ScheduledAnnotationBeanPostProcessorTests {
|
|||
}
|
||||
|
||||
|
||||
static class SeveralFixedRatesTestBean {
|
||||
|
||||
@Scheduled(fixedRate=4000)
|
||||
@Scheduled(fixedRate=4000, initialDelay=2000)
|
||||
public void fixedRate() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class CronTestBean {
|
||||
|
||||
@Scheduled(cron="*/7 * * * * ?")
|
||||
|
|
|
|||
Loading…
Reference in New Issue