Skip to content

Commit d4123d0

Browse files
committed
Support use of @scheduled against JDK proxies
Prior to this change, ScheduledAnnotationBeanPostProcessor found any @scheduled methods against the ultimate targetClass for a given bean and then attempted to invoke that method against the bean instance. In cases where the bean instance was in fact a JDK proxy, this attempt would fail because the proxy is not an instance of the target class. Now SABPP still attempts to find @scheduled methods against the target class, but subsequently checks to see if the bean is a JDK proxy, and if so attempts to find the corresponding method on the proxy itself. If it cannot be found (e.g. the @scheduled method was declared only at the concrete class level), an appropriate exception is thrown, explaining to the users their options: (a) use proxyTargetClass=true and go with subclass proxies which won't have this problem, or (b) pull the @scheduled method up into an interface. Issue: SPR-8651
1 parent 439b775 commit d4123d0

File tree

2 files changed

+199
-1
lines changed

2 files changed

+199
-1
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) {
110110
}
111111

112112
public Object postProcessAfterInitialization(final Object bean, String beanName) {
113-
Class<?> targetClass = AopUtils.getTargetClass(bean);
113+
final Class<?> targetClass = AopUtils.getTargetClass(bean);
114114
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
115115
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
116116
Scheduled annotation = AnnotationUtils.getAnnotation(method, Scheduled.class);
@@ -119,6 +119,22 @@ public void doWith(Method method) throws IllegalArgumentException, IllegalAccess
119119
"Only void-returning methods may be annotated with @Scheduled.");
120120
Assert.isTrue(method.getParameterTypes().length == 0,
121121
"Only no-arg methods may be annotated with @Scheduled.");
122+
if (AopUtils.isJdkDynamicProxy(bean)) {
123+
try {
124+
// found a @Scheduled method on the target class for this JDK proxy -> is it
125+
// also present on the proxy itself?
126+
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
127+
} catch (SecurityException ex) {
128+
ReflectionUtils.handleReflectionException(ex);
129+
} catch (NoSuchMethodException ex) {
130+
throw new IllegalStateException(String.format(
131+
"@Scheduled method '%s' found on bean target class '%s', " +
132+
"but not found in any interface(s) for bean JDK proxy. Either " +
133+
"pull the method up to an interface or switch to subclass (CGLIB) " +
134+
"proxies by setting proxy-target-class/proxyTargetClass " +
135+
"attribute to 'true'", method.getName(), targetClass.getSimpleName()));
136+
}
137+
}
122138
Runnable runnable = new ScheduledMethodRunnable(bean, method);
123139
boolean processedSchedule = false;
124140
String errorMessage = "Exactly one of 'cron', 'fixedDelay', or 'fixedRate' is required.";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright 2002-2011 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.annotation;
18+
19+
import static org.easymock.EasyMock.createMock;
20+
import static org.easymock.EasyMock.replay;
21+
import static org.hamcrest.CoreMatchers.is;
22+
import static org.hamcrest.Matchers.greaterThan;
23+
import static org.junit.Assert.assertThat;
24+
import static org.junit.Assert.assertTrue;
25+
import static org.junit.Assert.fail;
26+
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
29+
import org.junit.Test;
30+
import org.springframework.aop.support.AopUtils;
31+
import org.springframework.beans.factory.BeanCreationException;
32+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
36+
import org.springframework.dao.support.PersistenceExceptionTranslator;
37+
import org.springframework.scheduling.annotation.EnableScheduling;
38+
import org.springframework.stereotype.Repository;
39+
import org.springframework.transaction.CallCountingTransactionManager;
40+
import org.springframework.transaction.PlatformTransactionManager;
41+
import org.springframework.transaction.annotation.EnableTransactionManagement;
42+
import org.springframework.transaction.annotation.Transactional;
43+
44+
/**
45+
* Integration tests cornering bug SPR-8651, which revealed that @Scheduled methods may
46+
* not work well with beans that have already been proxied for other reasons such
47+
* as @Transactional or @Async processing.
48+
*
49+
* @author Chris Beams
50+
* @since 3.1
51+
*/
52+
public class ScheduledAndTransactionalAnnotationIntegrationTests {
53+
54+
@Test
55+
public void failsWhenJdkProxyAndScheduledMethodNotPresentOnInterface() {
56+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
57+
ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigA.class);
58+
try {
59+
ctx.refresh();
60+
fail("expected exception");
61+
} catch (BeanCreationException ex) {
62+
assertTrue(ex.getRootCause().getMessage().startsWith("@Scheduled method 'scheduled' found"));
63+
}
64+
}
65+
66+
@Test
67+
public void succeedsWhenSubclassProxyAndScheduledMethodNotPresentOnInterface() throws InterruptedException {
68+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
69+
ctx.register(Config.class, SubclassProxyTxConfig.class, RepoConfigA.class);
70+
ctx.refresh();
71+
72+
Thread.sleep(10); // allow @Scheduled method to be called several times
73+
74+
MyRepository repository = ctx.getBean(MyRepository.class);
75+
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
76+
assertThat("repository is not a proxy", AopUtils.isAopProxy(repository), is(true));
77+
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
78+
assertThat("no transactions were committed", txManager.commits, greaterThan(0));
79+
}
80+
81+
@Test
82+
public void succeedsWhenJdkProxyAndScheduledMethodIsPresentOnInterface() throws InterruptedException {
83+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
84+
ctx.register(Config.class, JdkProxyTxConfig.class, RepoConfigB.class);
85+
ctx.refresh();
86+
87+
Thread.sleep(10); // allow @Scheduled method to be called several times
88+
89+
MyRepositoryWithScheduledMethod repository = ctx.getBean(MyRepositoryWithScheduledMethod.class);
90+
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
91+
assertThat("repository is not a proxy", AopUtils.isAopProxy(repository), is(true));
92+
assertThat("@Scheduled method never called", repository.getInvocationCount(), greaterThan(0));
93+
assertThat("no transactions were committed", txManager.commits, greaterThan(0));
94+
}
95+
96+
@Configuration
97+
@EnableTransactionManagement
98+
static class JdkProxyTxConfig { }
99+
100+
@Configuration
101+
@EnableTransactionManagement(proxyTargetClass=true)
102+
static class SubclassProxyTxConfig { }
103+
104+
@Configuration
105+
static class RepoConfigA {
106+
@Bean
107+
public MyRepository repository() {
108+
return new MyRepositoryImpl();
109+
}
110+
}
111+
112+
@Configuration
113+
static class RepoConfigB {
114+
@Bean
115+
public MyRepositoryWithScheduledMethod repository() {
116+
return new MyRepositoryWithScheduledMethodImpl();
117+
}
118+
}
119+
120+
@Configuration
121+
@EnableScheduling
122+
static class Config {
123+
124+
@Bean
125+
public PersistenceExceptionTranslationPostProcessor peTranslationPostProcessor() {
126+
return new PersistenceExceptionTranslationPostProcessor();
127+
}
128+
129+
@Bean
130+
public PlatformTransactionManager txManager() {
131+
return new CallCountingTransactionManager();
132+
}
133+
134+
@Bean
135+
public PersistenceExceptionTranslator peTranslator() {
136+
PersistenceExceptionTranslator txlator = createMock(PersistenceExceptionTranslator.class);
137+
replay(txlator);
138+
return txlator;
139+
}
140+
}
141+
142+
public interface MyRepository {
143+
int getInvocationCount();
144+
}
145+
146+
@Repository
147+
static class MyRepositoryImpl implements MyRepository {
148+
149+
private final AtomicInteger count = new AtomicInteger(0);
150+
151+
@Transactional
152+
@Scheduled(fixedDelay = 5)
153+
public void scheduled() {
154+
this.count.incrementAndGet();
155+
}
156+
157+
public int getInvocationCount() {
158+
return this.count.get();
159+
}
160+
}
161+
162+
public interface MyRepositoryWithScheduledMethod {
163+
int getInvocationCount();
164+
public void scheduled();
165+
}
166+
167+
@Repository
168+
static class MyRepositoryWithScheduledMethodImpl implements MyRepositoryWithScheduledMethod {
169+
170+
private final AtomicInteger count = new AtomicInteger(0);
171+
172+
@Transactional
173+
@Scheduled(fixedDelay = 5)
174+
public void scheduled() {
175+
this.count.incrementAndGet();
176+
}
177+
178+
public int getInvocationCount() {
179+
return this.count.get();
180+
}
181+
}
182+
}

0 commit comments

Comments
 (0)