Skip to content

Commit 01724d3

Browse files
committed
Explicitly detect (and prevent) private @scheduled methods on CGLIB proxies
Issue: SPR-12308
1 parent e56559f commit 01724d3

File tree

2 files changed

+52
-15
lines changed

2 files changed

+52
-15
lines changed

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

+19-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.scheduling.annotation;
1818

1919
import java.lang.reflect.Method;
20+
import java.lang.reflect.Modifier;
2021
import java.util.Collections;
2122
import java.util.HashMap;
2223
import java.util.LinkedHashSet;
@@ -77,9 +78,8 @@
7778
* @see org.springframework.scheduling.TaskScheduler
7879
* @see org.springframework.scheduling.config.ScheduledTaskRegistrar
7980
*/
80-
public class ScheduledAnnotationBeanPostProcessor
81-
implements BeanPostProcessor, Ordered, EmbeddedValueResolverAware, BeanFactoryAware,
82-
SmartInitializingSingleton, DisposableBean {
81+
public class ScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered,
82+
EmbeddedValueResolverAware, BeanFactoryAware, SmartInitializingSingleton, DisposableBean {
8383

8484
protected final Log logger = LogFactory.getLog(getClass());
8585

@@ -227,17 +227,27 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
227227
}
228228
catch (NoSuchMethodException ex) {
229229
throw new IllegalStateException(String.format(
230-
"@Scheduled method '%s' found on bean target class '%s', " +
231-
"but not found in any interface(s) for bean JDK proxy. Either " +
232-
"pull the method up to an interface or switch to subclass (CGLIB) " +
233-
"proxies by setting proxy-target-class/proxyTargetClass " +
234-
"attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()));
230+
"@Scheduled method '%s' found on bean target class '%s' but not " +
231+
"found in any interface(s) for a dynamic proxy. Either pull the " +
232+
"method up to a declared interface or switch to subclass (CGLIB) " +
233+
"proxies by setting proxy-target-class/proxyTargetClass to 'true'",
234+
method.getName(), method.getDeclaringClass().getSimpleName()));
235+
}
236+
}
237+
else if (AopUtils.isCglibProxy(bean)) {
238+
// Common problem: private methods end up in the proxy instance, not getting delegated.
239+
if (Modifier.isPrivate(method.getModifiers())) {
240+
throw new IllegalStateException(String.format(
241+
"@Scheduled method '%s' found on CGLIB proxy for target class '%s' but cannot " +
242+
"be delegated to target bean. Switch its visibility to package or protected.",
243+
method.getName(), method.getDeclaringClass().getSimpleName()));
235244
}
236245
}
237246

238247
Runnable runnable = new ScheduledMethodRunnable(bean, method);
239248
boolean processedSchedule = false;
240-
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
249+
String errorMessage =
250+
"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
241251

242252
// Determine initial delay
243253
long initialDelay = scheduled.initialDelay();

Diff for: spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java

+33-6
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.
@@ -28,6 +28,7 @@
2828
import java.util.Properties;
2929
import java.util.TimeZone;
3030

31+
import org.junit.After;
3132
import org.junit.Test;
3233

3334
import org.springframework.beans.DirectFieldAccessor;
@@ -46,6 +47,8 @@
4647
import org.springframework.scheduling.support.SimpleTriggerContext;
4748
import org.springframework.tests.Assume;
4849
import org.springframework.tests.TestGroup;
50+
import org.springframework.validation.annotation.Validated;
51+
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
4952

5053
import static org.junit.Assert.*;
5154

@@ -60,6 +63,13 @@ public class ScheduledAnnotationBeanPostProcessorTests {
6063

6164
private final StaticApplicationContext context = new StaticApplicationContext();
6265

66+
67+
@After
68+
public void closeContextAfterTest() {
69+
context.close();
70+
}
71+
72+
6373
@Test
6474
public void fixedDelayTask() {
6575
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
@@ -140,7 +150,8 @@ public void fixedRateTaskWithInitialDelay() {
140150
public void severalFixedRatesWithRepeatedScheduledAnnotation() {
141151
BeanDefinition processorDefinition = new
142152
RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
143-
BeanDefinition targetDefinition = new RootBeanDefinition(SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean.class);
153+
BeanDefinition targetDefinition = new RootBeanDefinition(
154+
SeveralFixedRatesWithRepeatedScheduledAnnotationTestBean.class);
144155
severalFixedRates(context, processorDefinition, targetDefinition);
145156
}
146157

@@ -155,6 +166,7 @@ public void severalFixedRatesWithSchedulesContainerAnnotation() {
155166

156167
private void severalFixedRates(StaticApplicationContext context,
157168
BeanDefinition processorDefinition, BeanDefinition targetDefinition) {
169+
158170
context.registerBeanDefinition("postProcessor", processorDefinition);
159171
context.registerBeanDefinition("target", targetDefinition);
160172
context.refresh();
@@ -248,7 +260,8 @@ public void cronTaskWithZone() throws InterruptedException {
248260
Date lastActualExecutionTime = cal.getTime();
249261
cal.add(Calendar.MINUTE, 30); // 4:30
250262
Date lastCompletionTime = cal.getTime();
251-
TriggerContext triggerContext = new SimpleTriggerContext(lastScheduledExecutionTime, lastActualExecutionTime, lastCompletionTime);
263+
TriggerContext triggerContext = new SimpleTriggerContext(
264+
lastScheduledExecutionTime, lastActualExecutionTime, lastCompletionTime);
252265
cal.add(Calendar.MINUTE, 30);
253266
cal.add(Calendar.HOUR_OF_DAY, 1); // 6:00
254267
Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);
@@ -269,6 +282,18 @@ public void cronTaskWithInvalidZone() throws InterruptedException {
269282
Thread.sleep(10000);
270283
}
271284

285+
@Test(expected = BeanCreationException.class)
286+
public void cronTaskWithMethodValidation() throws InterruptedException {
287+
BeanDefinition validationDefinition = new RootBeanDefinition(MethodValidationPostProcessor.class);
288+
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
289+
BeanDefinition targetDefinition = new RootBeanDefinition(
290+
ScheduledAnnotationBeanPostProcessorTests.CronTestBean.class);
291+
context.registerBeanDefinition("methodValidation", validationDefinition);
292+
context.registerBeanDefinition("postProcessor", processorDefinition);
293+
context.registerBeanDefinition("target", targetDefinition);
294+
context.refresh();
295+
}
296+
272297
@Test
273298
public void metaAnnotationWithFixedRate() {
274299
BeanDefinition processorDefinition = new RootBeanDefinition(ScheduledAnnotationBeanPostProcessor.class);
@@ -527,10 +552,11 @@ public void fixedRate() {
527552
}
528553

529554

555+
@Validated
530556
static class CronTestBean {
531557

532558
@Scheduled(cron="*/7 * * * * ?")
533-
public void cron() throws IOException {
559+
private void cron() throws IOException {
534560
throw new IOException("no no no");
535561
}
536562
}
@@ -539,7 +565,7 @@ public void cron() throws IOException {
539565
static class CronWithTimezoneTestBean {
540566

541567
@Scheduled(cron="0 0 0-4,6-23 * * ?", zone = "GMT+10")
542-
public void cron() throws IOException {
568+
protected void cron() throws IOException {
543569
throw new IOException("no no no");
544570
}
545571
}
@@ -642,7 +668,8 @@ public void fixedRate() {
642668
@Scheduled(cron="${schedules.businessHours}")
643669
@Target(ElementType.METHOD)
644670
@Retention(RetentionPolicy.RUNTIME)
645-
private static @interface BusinessHours {}
671+
private static @interface BusinessHours {
672+
}
646673

647674

648675
static class PropertyPlaceholderMetaAnnotationTestBean {

0 commit comments

Comments
 (0)