Skip to content

Commit 470ea97

Browse files
committed
Consistent validation of annotated methods behind AOP proxies
Issue: SPR-13816
1 parent 63958ac commit 470ea97

File tree

6 files changed

+59
-35
lines changed

6 files changed

+59
-35
lines changed

spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.reflect.InvocationTargetException;
2020
import java.lang.reflect.Method;
21+
import java.lang.reflect.Modifier;
2122
import java.lang.reflect.Proxy;
2223
import java.util.LinkedHashSet;
2324
import java.util.LinkedList;
@@ -34,6 +35,7 @@
3435
import org.springframework.aop.SpringProxy;
3536
import org.springframework.aop.TargetClassAware;
3637
import org.springframework.core.BridgeMethodResolver;
38+
import org.springframework.core.MethodIntrospector;
3739
import org.springframework.util.Assert;
3840
import org.springframework.util.ClassUtils;
3941
import org.springframework.util.ReflectionUtils;
@@ -112,6 +114,30 @@ public static Class<?> getTargetClass(Object candidate) {
112114
return result;
113115
}
114116

117+
/**
118+
* Select an invocable method on the target type: either the given method itself
119+
* if actually exposed on the target type, or otherwise a corresponding method
120+
* on one of the target type's interfaces or on the target type itself.
121+
* @param method the method to check
122+
* @param targetType the target type to search methods on (typically an AOP proxy)
123+
* @return a corresponding invocable method on the target type
124+
* @throws IllegalStateException if the given method is not invocable on the given
125+
* target type (typically due to a proxy mismatch)
126+
* @since 4.3
127+
* @see MethodIntrospector#selectInvocableMethod(Method, Class)
128+
*/
129+
public static Method selectInvocableMethod(Method method, Class<?> targetType) {
130+
Method methodToUse = MethodIntrospector.selectInvocableMethod(method, targetType);
131+
if (Modifier.isPrivate(methodToUse.getModifiers()) && !Modifier.isStatic(methodToUse.getModifiers()) &&
132+
SpringProxy.class.isAssignableFrom(targetType)) {
133+
throw new IllegalStateException(String.format(
134+
"Need to invoke method '%s' found on proxy for target class '%s' but cannot " +
135+
"be delegated to target bean. Switch its visibility to package or protected.",
136+
method.getName(), method.getDeclaringClass().getSimpleName()));
137+
}
138+
return methodToUse;
139+
}
140+
115141
/**
116142
* Determine whether the given method is an "equals" method.
117143
* @see java.lang.Object#equals

spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
3131
import org.springframework.aop.scope.ScopedObject;
3232
import org.springframework.aop.scope.ScopedProxyUtils;
33+
import org.springframework.aop.support.AopUtils;
3334
import org.springframework.beans.BeansException;
3435
import org.springframework.beans.factory.BeanInitializationException;
3536
import org.springframework.beans.factory.SmartInitializingSingleton;
@@ -142,7 +143,7 @@ public EventListener inspect(Method method) {
142143
for (Method method : annotatedMethods.keySet()) {
143144
for (EventListenerFactory factory : factories) {
144145
if (factory.supportsMethod(method)) {
145-
Method methodToUse = MethodIntrospector.selectInvocableMethod(
146+
Method methodToUse = AopUtils.selectInvocableMethod(
146147
method, this.applicationContext.getType(beanName));
147148
ApplicationListener<?> applicationListener =
148149
factory.createApplicationListener(beanName, targetType, methodToUse);

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

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.scheduling.annotation;
1818

1919
import java.lang.reflect.Method;
20-
import java.lang.reflect.Modifier;
2120
import java.util.Collections;
2221
import java.util.Map;
2322
import java.util.Set;
@@ -53,7 +52,6 @@
5352
import org.springframework.scheduling.support.CronTrigger;
5453
import org.springframework.scheduling.support.ScheduledMethodRunnable;
5554
import org.springframework.util.Assert;
56-
import org.springframework.util.ReflectionUtils;
5755
import org.springframework.util.StringUtils;
5856
import org.springframework.util.StringValueResolver;
5957

@@ -274,35 +272,8 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean)
274272
Assert.isTrue(method.getParameterTypes().length == 0,
275273
"Only no-arg methods may be annotated with @Scheduled");
276274

277-
if (AopUtils.isJdkDynamicProxy(bean)) {
278-
try {
279-
// Found a @Scheduled method on the target class for this JDK proxy ->
280-
// is it also present on the proxy itself?
281-
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
282-
}
283-
catch (SecurityException ex) {
284-
ReflectionUtils.handleReflectionException(ex);
285-
}
286-
catch (NoSuchMethodException ex) {
287-
throw new IllegalStateException(String.format(
288-
"@Scheduled method '%s' found on bean target class '%s' but not " +
289-
"found in any interface(s) for a dynamic proxy. Either pull the " +
290-
"method up to a declared interface or switch to subclass (CGLIB) " +
291-
"proxies by setting proxy-target-class/proxyTargetClass to 'true'.",
292-
method.getName(), method.getDeclaringClass().getSimpleName()));
293-
}
294-
}
295-
else if (AopUtils.isCglibProxy(bean)) {
296-
// Common problem: private methods end up in the proxy instance, not getting delegated.
297-
if (Modifier.isPrivate(method.getModifiers())) {
298-
throw new IllegalStateException(String.format(
299-
"@Scheduled method '%s' found on CGLIB proxy for target class '%s' but cannot " +
300-
"be delegated to target bean. Switch its visibility to package or protected.",
301-
method.getName(), method.getDeclaringClass().getSimpleName()));
302-
}
303-
}
304-
305-
Runnable runnable = new ScheduledMethodRunnable(bean, method);
275+
Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass());
276+
Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod);
306277
boolean processedSchedule = false;
307278
String errorMessage =
308279
"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,17 @@ public void eventListenerWorksWithCglibProxy() throws Exception {
284284
this.eventCollector.assertTotalEventsCount(1);
285285
}
286286

287+
@Test
288+
public void privateMethodOnCglibProxyFails() throws Exception {
289+
try {
290+
load(CglibProxyWithPrivateMethod.class);
291+
fail("Should have thrown BeanInitializationException");
292+
}
293+
catch (BeanInitializationException ex) {
294+
assertTrue(ex.getCause() instanceof IllegalStateException);
295+
}
296+
}
297+
287298
@Test
288299
public void eventListenerWorksWithCustomScope() throws Exception {
289300
load(CustomScopeTestBean.class);
@@ -790,6 +801,17 @@ public void handleIt(TestEvent event) {
790801
}
791802

792803

804+
@Component
805+
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
806+
static class CglibProxyWithPrivateMethod extends AbstractTestEventListener {
807+
808+
@EventListener
809+
private void handleIt(TestEvent event) {
810+
collectEvent(event);
811+
}
812+
}
813+
814+
793815
@Component
794816
@Scope(scopeName = "custom", proxyMode = ScopedProxyMode.TARGET_CLASS)
795817
static class CustomScopeTestBean extends AbstractTestEventListener {

spring-core/src/main/java/org/springframework/core/MethodIntrospector.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,22 +111,26 @@ public Boolean inspect(Method method) {
111111
* @param targetType the target type to search methods on
112112
* (typically an interface-based JDK proxy)
113113
* @return a corresponding invocable method on the target type
114+
* @throws IllegalStateException if the given method is not invocable on the given
115+
* target type (typically due to a proxy mismatch)
114116
*/
115117
public static Method selectInvocableMethod(Method method, Class<?> targetType) {
116118
if (method.getDeclaringClass().isAssignableFrom(targetType)) {
117119
return method;
118120
}
119121
try {
122+
String methodName = method.getName();
123+
Class<?>[] parameterTypes = method.getParameterTypes();
120124
for (Class<?> ifc : targetType.getInterfaces()) {
121125
try {
122-
return ifc.getMethod(method.getName(), method.getParameterTypes());
126+
return ifc.getMethod(methodName, parameterTypes);
123127
}
124128
catch (NoSuchMethodException ex) {
125129
// Alright, not on this interface then...
126130
}
127131
}
128132
// A final desperate attempt on the proxy class itself...
129-
return targetType.getMethod(method.getName(), method.getParameterTypes());
133+
return targetType.getMethod(methodName, parameterTypes);
130134
}
131135
catch (NoSuchMethodException ex) {
132136
throw new IllegalStateException(String.format(

spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ public Set<JmsListener> inspect(Method method) {
236236
* @see JmsListenerEndpointRegistrar#registerEndpoint
237237
*/
238238
protected void processJmsListener(JmsListener jmsListener, Method mostSpecificMethod, Object bean) {
239-
Method invocableMethod = MethodIntrospector.selectInvocableMethod(mostSpecificMethod, bean.getClass());
239+
Method invocableMethod = AopUtils.selectInvocableMethod(mostSpecificMethod, bean.getClass());
240240

241241
MethodJmsListenerEndpoint endpoint = createMethodJmsListenerEndpoint();
242242
endpoint.setBean(bean);

0 commit comments

Comments
 (0)