Skip to content

Commit 37da706

Browse files
committed
ScheduledAnnotationBeanPostProcessor avoids needless re-scanning of non-annotated classes
Issue: SPR-12189 (cherry picked from commit 58b22ce)
1 parent 5cf4524 commit 37da706

File tree

2 files changed

+154
-129
lines changed

2 files changed

+154
-129
lines changed

spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java

+144-120
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
import java.lang.reflect.Method;
2020
import java.util.HashMap;
21+
import java.util.LinkedHashSet;
2122
import java.util.Map;
23+
import java.util.Set;
24+
import java.util.concurrent.ConcurrentHashMap;
2225
import java.util.concurrent.ScheduledExecutorService;
2326

2427
import org.springframework.aop.support.AopUtils;
@@ -40,6 +43,7 @@
4043
import org.springframework.util.Assert;
4144
import org.springframework.util.ReflectionUtils;
4245
import org.springframework.util.ReflectionUtils.MethodCallback;
46+
import org.springframework.util.StringUtils;
4347
import org.springframework.util.StringValueResolver;
4448

4549
/**
@@ -78,7 +82,10 @@ public class ScheduledAnnotationBeanPostProcessor
7882

7983
private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
8084

85+
private final Map<Class<?>, Boolean> nonAnnotatedClasses = new ConcurrentHashMap<Class<?>, Boolean>(64);
8186

87+
88+
@Override
8289
public int getOrder() {
8390
return LOWEST_PRECEDENCE;
8491
}
@@ -127,12 +134,11 @@ else if (schedulers.size() == 1) {
127134
this.registrar.setScheduler(schedulers.values().iterator().next());
128135
}
129136
else if (schedulers.size() >= 2){
130-
throw new IllegalStateException(
131-
"More than one TaskScheduler and/or ScheduledExecutorService " +
132-
"exist within the context. Remove all but one of the beans; or " +
133-
"implement the SchedulingConfigurer interface and call " +
134-
"ScheduledTaskRegistrar#setScheduler explicitly within the " +
135-
"configureTasks() callback. Found the following beans: " + schedulers.keySet());
137+
throw new IllegalStateException("More than one TaskScheduler and/or ScheduledExecutorService " +
138+
"exist within the context. Remove all but one of the beans; or implement the " +
139+
"SchedulingConfigurer interface and call ScheduledTaskRegistrar#setScheduler " +
140+
"explicitly within the configureTasks() callback. Found the following beans: " +
141+
schedulers.keySet());
136142
}
137143
}
138144

@@ -145,126 +151,144 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) {
145151
}
146152

147153
public Object postProcessAfterInitialization(final Object bean, String beanName) {
148-
final Class<?> targetClass = AopUtils.getTargetClass(bean);
149-
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
150-
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
151-
Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class);
152-
if (annotation != null) {
153-
try {
154-
Assert.isTrue(void.class.equals(method.getReturnType()),
155-
"Only void-returning methods may be annotated with @Scheduled");
156-
Assert.isTrue(method.getParameterTypes().length == 0,
157-
"Only no-arg methods may be annotated with @Scheduled");
158-
if (AopUtils.isJdkDynamicProxy(bean)) {
159-
try {
160-
// found a @Scheduled method on the target class for this JDK proxy -> is it
161-
// also present on the proxy itself?
162-
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
163-
}
164-
catch (SecurityException ex) {
165-
ReflectionUtils.handleReflectionException(ex);
166-
}
167-
catch (NoSuchMethodException ex) {
168-
throw new IllegalStateException(String.format(
169-
"@Scheduled method '%s' found on bean target class '%s', " +
170-
"but not found in any interface(s) for bean JDK proxy. Either " +
171-
"pull the method up to an interface or switch to subclass (CGLIB) " +
172-
"proxies by setting proxy-target-class/proxyTargetClass " +
173-
"attribute to 'true'", method.getName(), targetClass.getSimpleName()));
174-
}
175-
}
176-
Runnable runnable = new ScheduledMethodRunnable(bean, method);
177-
boolean processedSchedule = false;
178-
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
179-
// Determine initial delay
180-
long initialDelay = annotation.initialDelay();
181-
String initialDelayString = annotation.initialDelayString();
182-
if (!"".equals(initialDelayString)) {
183-
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
184-
if (embeddedValueResolver != null) {
185-
initialDelayString = embeddedValueResolver.resolveStringValue(initialDelayString);
186-
}
187-
try {
188-
initialDelay = Integer.parseInt(initialDelayString);
189-
}
190-
catch (NumberFormatException ex) {
191-
throw new IllegalArgumentException(
192-
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
193-
}
194-
}
195-
// Check cron expression
196-
String cron = annotation.cron();
197-
if (!"".equals(cron)) {
198-
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
199-
processedSchedule = true;
200-
if (embeddedValueResolver != null) {
201-
cron = embeddedValueResolver.resolveStringValue(cron);
202-
}
203-
registrar.addCronTask(new CronTask(runnable, cron));
204-
}
205-
// At this point we don't need to differentiate between initial delay set or not anymore
206-
if (initialDelay < 0) {
207-
initialDelay = 0;
208-
}
209-
// Check fixed delay
210-
long fixedDelay = annotation.fixedDelay();
211-
if (fixedDelay >= 0) {
212-
Assert.isTrue(!processedSchedule, errorMessage);
213-
processedSchedule = true;
214-
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
215-
}
216-
String fixedDelayString = annotation.fixedDelayString();
217-
if (!"".equals(fixedDelayString)) {
218-
Assert.isTrue(!processedSchedule, errorMessage);
219-
processedSchedule = true;
220-
if (embeddedValueResolver != null) {
221-
fixedDelayString = embeddedValueResolver.resolveStringValue(fixedDelayString);
222-
}
223-
try {
224-
fixedDelay = Integer.parseInt(fixedDelayString);
225-
}
226-
catch (NumberFormatException ex) {
227-
throw new IllegalArgumentException(
228-
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
229-
}
230-
registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
231-
}
232-
// Check fixed rate
233-
long fixedRate = annotation.fixedRate();
234-
if (fixedRate >= 0) {
235-
Assert.isTrue(!processedSchedule, errorMessage);
236-
processedSchedule = true;
237-
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
238-
}
239-
String fixedRateString = annotation.fixedRateString();
240-
if (!"".equals(fixedRateString)) {
241-
Assert.isTrue(!processedSchedule, errorMessage);
242-
processedSchedule = true;
243-
if (embeddedValueResolver != null) {
244-
fixedRateString = embeddedValueResolver.resolveStringValue(fixedRateString);
245-
}
246-
try {
247-
fixedRate = Integer.parseInt(fixedRateString);
248-
}
249-
catch (NumberFormatException ex) {
250-
throw new IllegalArgumentException(
251-
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
252-
}
253-
registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
254-
}
255-
// Check whether we had any attribute set
256-
Assert.isTrue(processedSchedule, errorMessage);
257-
}
258-
catch (IllegalArgumentException ex) {
259-
throw new IllegalStateException(
260-
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
154+
if (!this.nonAnnotatedClasses.containsKey(bean.getClass())) {
155+
final Set<Method> annotatedMethods = new LinkedHashSet<Method>(1);
156+
Class<?> targetClass = AopUtils.getTargetClass(bean);
157+
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
158+
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
159+
Scheduled scheduled = AnnotationUtils.getAnnotation(method, Scheduled.class);
160+
if (scheduled != null) {
161+
processScheduled(scheduled, method, bean);
162+
annotatedMethods.add(method);
261163
}
262164
}
165+
});
166+
if (annotatedMethods.isEmpty()) {
167+
this.nonAnnotatedClasses.put(bean.getClass(), Boolean.TRUE);
263168
}
264-
});
169+
}
265170
return bean;
266171
}
267172

173+
private void processScheduled(Scheduled scheduled, Method method, Object bean) {
174+
try {
175+
Assert.isTrue(void.class.equals(method.getReturnType()),
176+
"Only void-returning methods may be annotated with @Scheduled");
177+
Assert.isTrue(method.getParameterTypes().length == 0,
178+
"Only no-arg methods may be annotated with @Scheduled");
179+
180+
if (AopUtils.isJdkDynamicProxy(bean)) {
181+
try {
182+
// Found a @Scheduled method on the target class for this JDK proxy ->
183+
// is it also present on the proxy itself?
184+
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
185+
}
186+
catch (SecurityException ex) {
187+
ReflectionUtils.handleReflectionException(ex);
188+
}
189+
catch (NoSuchMethodException ex) {
190+
throw new IllegalStateException(String.format(
191+
"@Scheduled method '%s' found on bean target class '%s', " +
192+
"but not found in any interface(s) for bean JDK proxy. Either " +
193+
"pull the method up to an interface or switch to subclass (CGLIB) " +
194+
"proxies by setting proxy-target-class/proxyTargetClass " +
195+
"attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()));
196+
}
197+
}
198+
199+
Runnable runnable = new ScheduledMethodRunnable(bean, method);
200+
boolean processedSchedule = false;
201+
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
202+
203+
// Determine initial delay
204+
long initialDelay = scheduled.initialDelay();
205+
String initialDelayString = scheduled.initialDelayString();
206+
if (StringUtils.hasText(initialDelayString)) {
207+
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
208+
if (this.embeddedValueResolver != null) {
209+
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
210+
}
211+
try {
212+
initialDelay = Integer.parseInt(initialDelayString);
213+
}
214+
catch (NumberFormatException ex) {
215+
throw new IllegalArgumentException(
216+
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer");
217+
}
218+
}
219+
220+
// Check cron expression
221+
String cron = scheduled.cron();
222+
if (StringUtils.hasText(cron)) {
223+
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
224+
processedSchedule = true;
225+
if (this.embeddedValueResolver != null) {
226+
cron = this.embeddedValueResolver.resolveStringValue(cron);
227+
}
228+
this.registrar.addCronTask(new CronTask(runnable, cron));
229+
}
230+
// At this point we don't need to differentiate between initial delay set or not anymore
231+
if (initialDelay < 0) {
232+
initialDelay = 0;
233+
}
234+
235+
// Check fixed delay
236+
long fixedDelay = scheduled.fixedDelay();
237+
if (fixedDelay >= 0) {
238+
Assert.isTrue(!processedSchedule, errorMessage);
239+
processedSchedule = true;
240+
this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
241+
}
242+
String fixedDelayString = scheduled.fixedDelayString();
243+
if (StringUtils.hasText(fixedDelayString)) {
244+
Assert.isTrue(!processedSchedule, errorMessage);
245+
processedSchedule = true;
246+
if (this.embeddedValueResolver != null) {
247+
fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
248+
}
249+
try {
250+
fixedDelay = Integer.parseInt(fixedDelayString);
251+
}
252+
catch (NumberFormatException ex) {
253+
throw new IllegalArgumentException(
254+
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer");
255+
}
256+
this.registrar.addFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay));
257+
}
258+
259+
// Check fixed rate
260+
long fixedRate = scheduled.fixedRate();
261+
if (fixedRate >= 0) {
262+
Assert.isTrue(!processedSchedule, errorMessage);
263+
processedSchedule = true;
264+
this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
265+
}
266+
String fixedRateString = scheduled.fixedRateString();
267+
if (StringUtils.hasText(fixedRateString)) {
268+
Assert.isTrue(!processedSchedule, errorMessage);
269+
processedSchedule = true;
270+
if (this.embeddedValueResolver != null) {
271+
fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
272+
}
273+
try {
274+
fixedRate = Integer.parseInt(fixedRateString);
275+
}
276+
catch (NumberFormatException ex) {
277+
throw new IllegalArgumentException(
278+
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer");
279+
}
280+
this.registrar.addFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay));
281+
}
282+
283+
// Check whether we had any attribute set
284+
Assert.isTrue(processedSchedule, errorMessage);
285+
}
286+
catch (IllegalArgumentException ex) {
287+
throw new IllegalStateException(
288+
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
289+
}
290+
}
291+
268292

269293
public void destroy() throws Exception {
270294
this.registrar.destroy();

spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@
3333
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
3434
import org.springframework.scheduling.support.CronTrigger;
3535
import org.springframework.util.Assert;
36+
import org.springframework.util.CollectionUtils;
3637

3738
/**
3839
* Helper bean for registering tasks with a {@link TaskScheduler}, typically using cron
@@ -268,10 +269,10 @@ public void addFixedDelayTask(IntervalTask task) {
268269
* @since 3.2
269270
*/
270271
public boolean hasTasks() {
271-
return (this.fixedRateTasks != null && !this.fixedRateTasks.isEmpty()) ||
272-
(this.fixedDelayTasks != null && !this.fixedDelayTasks.isEmpty()) ||
273-
(this.cronTasks != null && !this.cronTasks.isEmpty()) ||
274-
(this.triggerTasks != null && !this.triggerTasks.isEmpty());
272+
return (!CollectionUtils.isEmpty(this.triggerTasks) ||
273+
!CollectionUtils.isEmpty(this.cronTasks) ||
274+
!CollectionUtils.isEmpty(this.fixedRateTasks) ||
275+
!CollectionUtils.isEmpty(this.fixedDelayTasks));
275276
}
276277

277278

@@ -294,19 +295,19 @@ protected void scheduleTasks() {
294295
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
295296
}
296297
if (this.triggerTasks != null) {
297-
for (TriggerTask task : triggerTasks) {
298+
for (TriggerTask task : this.triggerTasks) {
298299
this.scheduledFutures.add(this.taskScheduler.schedule(
299300
task.getRunnable(), task.getTrigger()));
300301
}
301302
}
302303
if (this.cronTasks != null) {
303-
for (CronTask task : cronTasks) {
304+
for (CronTask task : this.cronTasks) {
304305
this.scheduledFutures.add(this.taskScheduler.schedule(
305306
task.getRunnable(), task.getTrigger()));
306307
}
307308
}
308309
if (this.fixedRateTasks != null) {
309-
for (IntervalTask task : fixedRateTasks) {
310+
for (IntervalTask task : this.fixedRateTasks) {
310311
if (task.getInitialDelay() > 0) {
311312
Date startTime = new Date(now + task.getInitialDelay());
312313
this.scheduledFutures.add(this.taskScheduler.scheduleAtFixedRate(
@@ -319,7 +320,7 @@ protected void scheduleTasks() {
319320
}
320321
}
321322
if (this.fixedDelayTasks != null) {
322-
for (IntervalTask task : fixedDelayTasks) {
323+
for (IntervalTask task : this.fixedDelayTasks) {
323324
if (task.getInitialDelay() > 0) {
324325
Date startTime = new Date(now + task.getInitialDelay());
325326
this.scheduledFutures.add(this.taskScheduler.scheduleWithFixedDelay(

0 commit comments

Comments
 (0)