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.Documented;
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Repeatable;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
@ -45,6 +46,7 @@ import java.lang.annotation.Target;
|
||||||
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Documented
|
@Documented
|
||||||
|
@Repeatable(Schedules.class)
|
||||||
public @interface Scheduled {
|
public @interface Scheduled {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -110,17 +110,34 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object postProcessAfterInitialization(final Object bean, String beanName) {
|
public Object postProcessAfterInitialization(final Object bean, String beanName) {
|
||||||
final Class<?> targetClass = AopUtils.getTargetClass(bean);
|
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||||
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
|
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
||||||
Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class);
|
Schedules schedules = AnnotationUtils.getAnnotation(method, Schedules.class);
|
||||||
if (annotation != null) {
|
if (schedules != null) {
|
||||||
|
for (Scheduled scheduled : schedules.value()) {
|
||||||
|
processScheduled(scheduled, method, bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Scheduled scheduled = AnnotationUtils.getAnnotation(method, Scheduled.class);
|
||||||
|
if (scheduled != null) {
|
||||||
|
processScheduled(scheduled, method, bean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return bean;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
|
||||||
try {
|
try {
|
||||||
Assert.isTrue(void.class.equals(method.getReturnType()),
|
Assert.isTrue(void.class.equals(method.getReturnType()),
|
||||||
"Only void-returning methods may be annotated with @Scheduled");
|
"Only void-returning methods may be annotated with @Scheduled");
|
||||||
Assert.isTrue(method.getParameterTypes().length == 0,
|
Assert.isTrue(method.getParameterTypes().length == 0,
|
||||||
"Only no-arg methods may be annotated with @Scheduled");
|
"Only no-arg methods may be annotated with @Scheduled");
|
||||||
|
|
||||||
if (AopUtils.isJdkDynamicProxy(bean)) {
|
if (AopUtils.isJdkDynamicProxy(bean)) {
|
||||||
try {
|
try {
|
||||||
// found a @Scheduled method on the target class for this JDK proxy -> is it
|
// found a @Scheduled method on the target class for this JDK proxy -> is it
|
||||||
|
|
@ -136,19 +153,21 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
"but not found in any interface(s) for bean JDK proxy. Either " +
|
"but not found in any interface(s) for bean JDK proxy. Either " +
|
||||||
"pull the method up to an interface or switch to subclass (CGLIB) " +
|
"pull the method up to an interface or switch to subclass (CGLIB) " +
|
||||||
"proxies by setting proxy-target-class/proxyTargetClass " +
|
"proxies by setting proxy-target-class/proxyTargetClass " +
|
||||||
"attribute to 'true'", method.getName(), targetClass.getSimpleName()));
|
"attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Runnable runnable = new ScheduledMethodRunnable(bean, method);
|
Runnable runnable = new ScheduledMethodRunnable(bean, method);
|
||||||
boolean processedSchedule = false;
|
boolean processedSchedule = false;
|
||||||
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
|
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
|
||||||
|
|
||||||
// Determine initial delay
|
// Determine initial delay
|
||||||
long initialDelay = annotation.initialDelay();
|
long initialDelay = scheduled.initialDelay();
|
||||||
String initialDelayString = annotation.initialDelayString();
|
String initialDelayString = scheduled.initialDelayString();
|
||||||
if (!"".equals(initialDelayString)) {
|
if (!"".equals(initialDelayString)) {
|
||||||
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
|
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
|
||||||
if (embeddedValueResolver != null) {
|
if (this.embeddedValueResolver != null) {
|
||||||
initialDelayString = embeddedValueResolver.resolveStringValue(initialDelayString);
|
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
initialDelay = Integer.parseInt(initialDelayString);
|
initialDelay = Integer.parseInt(initialDelayString);
|
||||||
|
|
@ -158,33 +177,36 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
|
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cron expression
|
// Check cron expression
|
||||||
String cron = annotation.cron();
|
String cron = scheduled.cron();
|
||||||
if (!"".equals(cron)) {
|
if (!"".equals(cron)) {
|
||||||
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
|
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
|
||||||
processedSchedule = true;
|
processedSchedule = true;
|
||||||
if (embeddedValueResolver != null) {
|
if (this.embeddedValueResolver != null) {
|
||||||
cron = embeddedValueResolver.resolveStringValue(cron);
|
cron = this.embeddedValueResolver.resolveStringValue(cron);
|
||||||
}
|
}
|
||||||
registrar.addCronTask(new CronTask(runnable, cron));
|
this.registrar.addCronTask(new CronTask(runnable, cron));
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point we don't need to differentiate between initial delay set or not anymore
|
// At this point we don't need to differentiate between initial delay set or not anymore
|
||||||
if (initialDelay < 0) {
|
if (initialDelay < 0) {
|
||||||
initialDelay = 0;
|
initialDelay = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check fixed delay
|
// Check fixed delay
|
||||||
long fixedDelay = annotation.fixedDelay();
|
long fixedDelay = scheduled.fixedDelay();
|
||||||
if (fixedDelay >= 0) {
|
if (fixedDelay >= 0) {
|
||||||
Assert.isTrue(!processedSchedule, errorMessage);
|
Assert.isTrue(!processedSchedule, errorMessage);
|
||||||
processedSchedule = true;
|
processedSchedule = true;
|
||||||
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
||||||
}
|
}
|
||||||
String fixedDelayString = annotation.fixedDelayString();
|
String fixedDelayString = scheduled.fixedDelayString();
|
||||||
if (!"".equals(fixedDelayString)) {
|
if (!"".equals(fixedDelayString)) {
|
||||||
Assert.isTrue(!processedSchedule, errorMessage);
|
Assert.isTrue(!processedSchedule, errorMessage);
|
||||||
processedSchedule = true;
|
processedSchedule = true;
|
||||||
if (embeddedValueResolver != null) {
|
if (this.embeddedValueResolver != null) {
|
||||||
fixedDelayString = embeddedValueResolver.resolveStringValue(fixedDelayString);
|
fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
fixedDelay = Integer.parseInt(fixedDelayString);
|
fixedDelay = Integer.parseInt(fixedDelayString);
|
||||||
|
|
@ -193,21 +215,22 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
|
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
|
||||||
}
|
}
|
||||||
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check fixed rate
|
// Check fixed rate
|
||||||
long fixedRate = annotation.fixedRate();
|
long fixedRate = scheduled.fixedRate();
|
||||||
if (fixedRate >= 0) {
|
if (fixedRate >= 0) {
|
||||||
Assert.isTrue(!processedSchedule, errorMessage);
|
Assert.isTrue(!processedSchedule, errorMessage);
|
||||||
processedSchedule = true;
|
processedSchedule = true;
|
||||||
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
||||||
}
|
}
|
||||||
String fixedRateString = annotation.fixedRateString();
|
String fixedRateString = scheduled.fixedRateString();
|
||||||
if (!"".equals(fixedRateString)) {
|
if (!"".equals(fixedRateString)) {
|
||||||
Assert.isTrue(!processedSchedule, errorMessage);
|
Assert.isTrue(!processedSchedule, errorMessage);
|
||||||
processedSchedule = true;
|
processedSchedule = true;
|
||||||
if (embeddedValueResolver != null) {
|
if (this.embeddedValueResolver != null) {
|
||||||
fixedRateString = embeddedValueResolver.resolveStringValue(fixedRateString);
|
fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
fixedRate = Integer.parseInt(fixedRateString);
|
fixedRate = Integer.parseInt(fixedRateString);
|
||||||
|
|
@ -216,8 +239,9 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
|
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
|
||||||
}
|
}
|
||||||
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether we had any attribute set
|
// Check whether we had any attribute set
|
||||||
Assert.isTrue(processedSchedule, errorMessage);
|
Assert.isTrue(processedSchedule, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
@ -226,28 +250,27 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
|
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
return bean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||||
if (event.getApplicationContext() != this.applicationContext) {
|
if (event.getApplicationContext() != this.applicationContext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Map<String, SchedulingConfigurer> configurers =
|
|
||||||
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
|
|
||||||
if (this.scheduler != null) {
|
if (this.scheduler != null) {
|
||||||
this.registrar.setScheduler(this.scheduler);
|
this.registrar.setScheduler(this.scheduler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, SchedulingConfigurer> configurers =
|
||||||
|
this.applicationContext.getBeansOfType(SchedulingConfigurer.class);
|
||||||
for (SchedulingConfigurer configurer : configurers.values()) {
|
for (SchedulingConfigurer configurer : configurers.values()) {
|
||||||
configurer.configureTasks(this.registrar);
|
configurer.configureTasks(this.registrar);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
|
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
|
||||||
Map<String, ? super Object> schedulers = new HashMap<String, Object>();
|
Map<String, ? super Object> schedulers = new HashMap<String, Object>();
|
||||||
schedulers.putAll(applicationContext.getBeansOfType(TaskScheduler.class));
|
schedulers.putAll(this.applicationContext.getBeansOfType(TaskScheduler.class));
|
||||||
schedulers.putAll(applicationContext.getBeansOfType(ScheduledExecutorService.class));
|
schedulers.putAll(this.applicationContext.getBeansOfType(ScheduledExecutorService.class));
|
||||||
if (schedulers.size() == 0) {
|
if (schedulers.size() == 0) {
|
||||||
// do nothing -> fall back to default scheduler
|
// do nothing -> fall back to default scheduler
|
||||||
}
|
}
|
||||||
|
|
@ -263,14 +286,13 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||||
"configureTasks() callback. Found the following beans: " + schedulers.keySet());
|
"configureTasks() callback. Found the following beans: " + schedulers.keySet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.registrar.afterPropertiesSet();
|
this.registrar.afterPropertiesSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void destroy() throws Exception {
|
public void destroy() {
|
||||||
if (this.registrar != null) {
|
|
||||||
this.registrar.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());
|
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
|
@Test
|
||||||
public void cronTask() throws InterruptedException {
|
public void cronTask() throws InterruptedException {
|
||||||
Assume.group(TestGroup.LONG_RUNNING);
|
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 {
|
static class CronTestBean {
|
||||||
|
|
||||||
@Scheduled(cron="*/7 * * * * ?")
|
@Scheduled(cron="*/7 * * * * ?")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue