Skip to content

Commit bc7bcab

Browse files
committed
Consistent method selection for listeners and endpoint mappings
Issue: SPR-13654
1 parent 5b06150 commit bc7bcab

File tree

10 files changed

+264
-160
lines changed

10 files changed

+264
-160
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2002-2015 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.core;
18+
19+
import java.lang.reflect.Method;
20+
import java.lang.reflect.Proxy;
21+
import java.util.Arrays;
22+
import java.util.LinkedHashMap;
23+
import java.util.LinkedHashSet;
24+
import java.util.Map;
25+
import java.util.Set;
26+
27+
import org.springframework.util.ClassUtils;
28+
import org.springframework.util.ReflectionUtils;
29+
30+
/**
31+
* Defines the algorithm for searching for metadata-associated methods exhaustively
32+
* including interfaces and parent classes while also dealing with parameterized methods
33+
* as well as common scenarios encountered with interface and class-based proxies.
34+
*
35+
* <p>Typically, but not necessarily, used for finding annotated handler methods.
36+
*
37+
* @author Juergen Hoeller
38+
* @author Rossen Stoyanchev
39+
* @since 4.2.3
40+
*/
41+
public abstract class MethodIntrospector {
42+
43+
/**
44+
* Select methods on the given target type based on the lookup of associated metadata.
45+
* <p>Callers define methods of interest through the {@link MetadataLookup} parameter,
46+
* allowing to collect the associated metadata into the result map.
47+
* @param targetType the target type to search methods on
48+
* @param metadataLookup a {@link MetadataLookup} callback to inspect methods of interest,
49+
* returning non-null metadata to be associated with a given method if there is a match,
50+
* or {@code null} for no match
51+
* @return the selected methods associated with their metadata (in the order of retrieval),
52+
* or an empty map in case of no match
53+
*/
54+
public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
55+
final Map<Method, T> methodMap = new LinkedHashMap<Method, T>();
56+
Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
57+
Class<?> specificHandlerType = null;
58+
59+
if (!Proxy.isProxyClass(targetType)) {
60+
handlerTypes.add(targetType);
61+
specificHandlerType = targetType;
62+
}
63+
handlerTypes.addAll(Arrays.asList(targetType.getInterfaces()));
64+
65+
for (Class<?> currentHandlerType : handlerTypes) {
66+
final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
67+
68+
ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
69+
@Override
70+
public void doWith(Method method) {
71+
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
72+
T result = metadataLookup.inspect(specificMethod);
73+
if (result != null) {
74+
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
75+
if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
76+
methodMap.put(specificMethod, result);
77+
}
78+
}
79+
}
80+
}, ReflectionUtils.USER_DECLARED_METHODS);
81+
}
82+
83+
return methodMap;
84+
}
85+
86+
/**
87+
* Select methods on the given target type based on a filter.
88+
* <p>Callers define methods of interest through the
89+
* {@link ReflectionUtils.MethodFilter} parameter.
90+
* @param targetType the target type to search methods on
91+
* @param methodFilter a {@link ReflectionUtils.MethodFilter} to help
92+
* recognize handler methods of interest
93+
* @return the selected methods, or an empty set in case of no match
94+
*/
95+
public static Set<Method> selectMethods(Class<?> targetType, final ReflectionUtils.MethodFilter methodFilter) {
96+
return selectMethods(targetType, new MetadataLookup<Boolean>() {
97+
@Override
98+
public Boolean inspect(Method method) {
99+
return (methodFilter.matches(method) ? Boolean.TRUE : null);
100+
}
101+
}).keySet();
102+
}
103+
104+
/**
105+
* Select an invocable method on the target type: either the given method itself
106+
* if actually exposed on the target type, or otherwise a corresponding method
107+
* on one of the target type's interfaces or on the target type itself.
108+
* <p>Matches on user-declared interfaces will be preferred since they are likely
109+
* to contain relevant metadata that corresponds to the method on the target class.
110+
* @param method the method to check
111+
* @param targetType the target type to search methods on
112+
* (typically an interface-based JDK proxy)
113+
* @return a corresponding invocable method on the target type
114+
*/
115+
public static Method selectInvocableMethod(Method method, Class<?> targetType) {
116+
if (method.getDeclaringClass().isAssignableFrom(targetType)) {
117+
return method;
118+
}
119+
try {
120+
for (Class<?> ifc : targetType.getInterfaces()) {
121+
try {
122+
return ifc.getMethod(method.getName(), method.getParameterTypes());
123+
}
124+
catch (NoSuchMethodException ex) {
125+
// Alright, not on this interface then...
126+
}
127+
}
128+
// A final desperate attempt on the proxy class itself...
129+
return targetType.getMethod(method.getName(), method.getParameterTypes());
130+
}
131+
catch (NoSuchMethodException ex) {
132+
throw new IllegalStateException(String.format(
133+
"Need to invoke method '%s' declared on target class '%s', " +
134+
"but not found in any interface(s) of the exposed proxy type. " +
135+
"Either pull the method up to an interface or switch to CGLIB " +
136+
"proxies by enforcing proxy-target-class mode in your configuration.",
137+
method.getName(), method.getDeclaringClass().getSimpleName()));
138+
}
139+
}
140+
141+
142+
/**
143+
* A callback interface for metadata lookup on a given method.
144+
* @param <T> the type of metadata returned
145+
*/
146+
public interface MetadataLookup<T> {
147+
148+
/**
149+
* Perform a lookup on the given method and return associated metadata, if any.
150+
* @param method the method to inspect
151+
* @return non-null metadata to be associated with a method if there is a match,
152+
* or {@code null} for no match
153+
*/
154+
T inspect(Method method);
155+
}
156+
157+
}

spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethodSelector.java

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,9 @@
1717
package org.springframework.messaging.handler;
1818

1919
import java.lang.reflect.Method;
20-
import java.lang.reflect.Proxy;
21-
import java.util.Arrays;
22-
import java.util.LinkedHashSet;
2320
import java.util.Set;
2421

25-
import org.springframework.core.BridgeMethodResolver;
26-
import org.springframework.util.ClassUtils;
27-
import org.springframework.util.ReflectionUtils;
22+
import org.springframework.core.MethodIntrospector;
2823
import org.springframework.util.ReflectionUtils.MethodFilter;
2924

3025
/**
@@ -33,7 +28,9 @@
3328
*
3429
* @author Rossen Stoyanchev
3530
* @since 4.0
31+
* @deprecated as of Spring 4.2.3, in favor of the generalized and refined {@link MethodIntrospector}
3632
*/
33+
@Deprecated
3734
public abstract class HandlerMethodSelector {
3835

3936
/**
@@ -42,31 +39,10 @@ public abstract class HandlerMethodSelector {
4239
* @param handlerType the handler type to search handler methods on
4340
* @param handlerMethodFilter a {@link MethodFilter} to help recognize handler methods of interest
4441
* @return the selected methods, or an empty set
42+
* @see MethodIntrospector#selectMethods(Class, MethodFilter)
4543
*/
46-
public static Set<Method> selectMethods(final Class<?> handlerType, final MethodFilter handlerMethodFilter) {
47-
final Set<Method> handlerMethods = new LinkedHashSet<Method>();
48-
Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
49-
Class<?> specificHandlerType = null;
50-
if (!Proxy.isProxyClass(handlerType)) {
51-
handlerTypes.add(handlerType);
52-
specificHandlerType = handlerType;
53-
}
54-
handlerTypes.addAll(Arrays.asList(handlerType.getInterfaces()));
55-
for (Class<?> currentHandlerType : handlerTypes) {
56-
final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
57-
ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
58-
@Override
59-
public void doWith(Method method) {
60-
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
61-
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
62-
if (handlerMethodFilter.matches(specificMethod) &&
63-
(bridgedMethod == specificMethod || !handlerMethodFilter.matches(bridgedMethod))) {
64-
handlerMethods.add(specificMethod);
65-
}
66-
}
67-
}, ReflectionUtils.USER_DECLARED_METHODS);
68-
}
69-
return handlerMethods;
44+
public static Set<Method> selectMethods(Class<?> handlerType, MethodFilter handlerMethodFilter) {
45+
return MethodIntrospector.selectMethods(handlerType, handlerMethodFilter);
7046
}
7147

7248
}

spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AnnotationExceptionHandlerMethodResolver.java

Lines changed: 25 additions & 18 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-2015 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.
@@ -23,8 +23,8 @@
2323
import java.util.List;
2424
import java.util.Map;
2525

26+
import org.springframework.core.MethodIntrospector;
2627
import org.springframework.core.annotation.AnnotationUtils;
27-
import org.springframework.messaging.handler.HandlerMethodSelector;
2828
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
2929
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
3030
import org.springframework.util.ReflectionUtils.MethodFilter;
@@ -49,32 +49,39 @@ public AnnotationExceptionHandlerMethodResolver(Class<?> handlerType) {
4949
}
5050

5151
private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) {
52+
Map<Method, MessageExceptionHandler> methods = MethodIntrospector.selectMethods(handlerType,
53+
new MethodIntrospector.MetadataLookup<MessageExceptionHandler>() {
54+
@Override
55+
public MessageExceptionHandler inspect(Method method) {
56+
return AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class);
57+
}
58+
});
59+
5260
Map<Class<? extends Throwable>, Method> result = new HashMap<Class<? extends Throwable>, Method>();
53-
for (Method method : HandlerMethodSelector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) {
54-
for (Class<? extends Throwable> exceptionType : getMappedExceptions(method)) {
61+
for (Map.Entry<Method, MessageExceptionHandler> entry : methods.entrySet()) {
62+
Method method = entry.getKey();
63+
List<Class<? extends Throwable>> exceptionTypes = new ArrayList<Class<? extends Throwable>>();
64+
exceptionTypes.addAll(Arrays.asList(entry.getValue().value()));
65+
if (exceptionTypes.isEmpty()) {
66+
exceptionTypes.addAll(getExceptionsFromMethodSignature(method));
67+
}
68+
for (Class<? extends Throwable> exceptionType : exceptionTypes) {
5569
Method oldMethod = result.put(exceptionType, method);
5670
if (oldMethod != null && !oldMethod.equals(method)) {
57-
throw new IllegalStateException(
58-
"Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" +
59-
oldMethod + ", " + method + "}.");
71+
throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" +
72+
exceptionType + "]: {" + oldMethod + ", " + method + "}");
6073
}
6174
}
6275
}
6376
return result;
6477
}
6578

66-
private static List<Class<? extends Throwable>> getMappedExceptions(Method method) {
67-
List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
68-
MessageExceptionHandler annot = AnnotationUtils.findAnnotation(method, MessageExceptionHandler.class);
69-
result.addAll(Arrays.asList(annot.value()));
70-
if (result.isEmpty()) {
71-
result.addAll(getExceptionsFromMethodSignature(method));
72-
}
73-
return result;
74-
}
75-
7679

77-
/** A filter for selecting annotated exception handling methods. */
80+
/**
81+
* A filter for selecting annotated exception handling methods.
82+
* @deprecated as of Spring 4.2.3, since it isn't used anymore
83+
*/
84+
@Deprecated
7885
public final static MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = new MethodFilter() {
7986

8087
@Override

spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,20 @@
3434
import org.springframework.context.ApplicationContext;
3535
import org.springframework.context.ApplicationContextAware;
3636
import org.springframework.core.MethodParameter;
37+
import org.springframework.core.MethodIntrospector;
3738
import org.springframework.messaging.Message;
3839
import org.springframework.messaging.MessageHandler;
3940
import org.springframework.messaging.MessagingException;
4041
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
4142
import org.springframework.messaging.handler.HandlerMethod;
42-
import org.springframework.messaging.handler.HandlerMethodSelector;
4343
import org.springframework.messaging.handler.MessagingAdviceBean;
4444
import org.springframework.messaging.support.MessageBuilder;
4545
import org.springframework.messaging.support.MessageHeaderAccessor;
46+
import org.springframework.util.Assert;
4647
import org.springframework.util.ClassUtils;
4748
import org.springframework.util.CollectionUtils;
4849
import org.springframework.util.LinkedMultiValueMap;
4950
import org.springframework.util.MultiValueMap;
50-
import org.springframework.util.ReflectionUtils;
5151
import org.springframework.util.concurrent.ListenableFuture;
5252
import org.springframework.util.concurrent.ListenableFutureCallback;
5353

@@ -60,6 +60,7 @@
6060
* exceptions raised during message handling.
6161
*
6262
* @author Rossen Stoyanchev
63+
* @author Juergen Hoeller
6364
* @since 4.0
6465
* @param <T> the type of the Object that contains information mapping a
6566
* {@link org.springframework.messaging.handler.HandlerMethod} to incoming messages
@@ -250,22 +251,24 @@ public void afterPropertiesSet() {
250251
* so register it with the extracted mapping information.
251252
* @param handler the handler to check, either an instance of a Spring bean name
252253
*/
253-
protected final void detectHandlerMethods(Object handler) {
254+
protected final void detectHandlerMethods(final Object handler) {
254255
Class<?> handlerType = (handler instanceof String ?
255256
this.applicationContext.getType((String) handler) : handler.getClass());
256-
257257
final Class<?> userType = ClassUtils.getUserClass(handlerType);
258258

259-
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new ReflectionUtils.MethodFilter() {
260-
@Override
261-
public boolean matches(Method method) {
262-
return getMappingForMethod(method, userType) != null;
263-
}
264-
});
259+
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
260+
new MethodIntrospector.MetadataLookup<T>() {
261+
@Override
262+
public T inspect(Method method) {
263+
return getMappingForMethod(method, userType);
264+
}
265+
});
265266

266-
for (Method method : methods) {
267-
T mapping = getMappingForMethod(method, userType);
268-
registerHandlerMethod(handler, method, mapping);
267+
if (logger.isDebugEnabled()) {
268+
logger.debug(methods.size() + " message handler methods found on " + userType + ": " + methods);
269+
}
270+
for (Map.Entry<Method, T> entry : methods.entrySet()) {
271+
registerHandlerMethod(handler, entry.getKey(), entry.getValue());
269272
}
270273
}
271274

@@ -277,7 +280,6 @@ public boolean matches(Method method) {
277280
*/
278281
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
279282

280-
281283
/**
282284
* Register a handler method and its unique mapping.
283285
* @param handler the bean name of the handler or the handler instance
@@ -287,6 +289,7 @@ public boolean matches(Method method) {
287289
* under the same mapping
288290
*/
289291
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
292+
Assert.notNull(mapping, "Mapping must bot be null");
290293
HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
291294
HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping);
292295

0 commit comments

Comments
 (0)