Skip to content

Commit 47ed4d6

Browse files
committed
Explicitly detect (and log) private @scheduled methods on CGLIB proxies
Issue: SPR-12308 (cherry picked from commit 01724d3)
1 parent da2c30c commit 47ed4d6

File tree

2 files changed

+52
-12
lines changed

2 files changed

+52
-12
lines changed

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

Lines changed: 19 additions & 6 deletions
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;
@@ -26,6 +27,8 @@
2627
import java.util.concurrent.ConcurrentHashMap;
2728
import java.util.concurrent.ScheduledExecutorService;
2829

30+
import org.apache.commons.logging.LogFactory;
31+
2932
import org.springframework.aop.support.AopUtils;
3033
import org.springframework.beans.factory.DisposableBean;
3134
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -198,17 +201,27 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
198201
}
199202
catch (NoSuchMethodException ex) {
200203
throw new IllegalStateException(String.format(
201-
"@Scheduled method '%s' found on bean target class '%s', " +
202-
"but not found in any interface(s) for bean JDK proxy. Either " +
203-
"pull the method up to an interface or switch to subclass (CGLIB) " +
204-
"proxies by setting proxy-target-class/proxyTargetClass " +
205-
"attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName()));
204+
"@Scheduled method '%s' found on bean target class '%s' but not " +
205+
"found in any interface(s) for a dynamic proxy. Either pull the " +
206+
"method up to a declared interface or switch to subclass (CGLIB) " +
207+
"proxies by setting proxy-target-class/proxyTargetClass to 'true'",
208+
method.getName(), method.getDeclaringClass().getSimpleName()));
209+
}
210+
}
211+
else if (AopUtils.isCglibProxy(bean)) {
212+
// Common problem: private methods end up in the proxy instance, not getting delegated.
213+
if (Modifier.isPrivate(method.getModifiers())) {
214+
LogFactory.getLog(ScheduledAnnotationBeanPostProcessor.class).warn(String.format(
215+
"@Scheduled method '%s' found on CGLIB proxy for target class '%s' but cannot " +
216+
"be delegated to target bean. Switch its visibility to package or protected.",
217+
method.getName(), method.getDeclaringClass().getSimpleName()));
206218
}
207219
}
208220

209221
Runnable runnable = new ScheduledMethodRunnable(bean, method);
210222
boolean processedSchedule = false;
211-
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
223+
String errorMessage =
224+
"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
212225

213226
// Determine initial delay
214227
long initialDelay = scheduled.initialDelay();

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

Lines changed: 33 additions & 6 deletions
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 // throws BeanCreationException in 4.1, just logs a warning before
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)