8 changed files with 1076 additions and 44 deletions
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.messaging; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
/** |
||||
* Reactive contract for handling a {@link Message}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.2 |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface ReactiveMessageHandler { |
||||
|
||||
/** |
||||
* Handle the given message. |
||||
* @param message the message to be handled |
||||
* @return a completion {@link Mono} for the result of the message handling. |
||||
*/ |
||||
Mono<Void> handleMessage(Message<?> message); |
||||
|
||||
} |
||||
@ -0,0 +1,567 @@
@@ -0,0 +1,567 @@
|
||||
/* |
||||
* Copyright 2002-2018 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.messaging.handler.invocation.reactive; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.beans.factory.InitializingBean; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.context.ApplicationContextAware; |
||||
import org.springframework.core.MethodIntrospector; |
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.core.ReactiveAdapterRegistry; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.MessageHandlingException; |
||||
import org.springframework.messaging.MessagingException; |
||||
import org.springframework.messaging.ReactiveMessageHandler; |
||||
import org.springframework.messaging.handler.HandlerMethod; |
||||
import org.springframework.messaging.handler.MessagingAdviceBean; |
||||
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MultiValueMap; |
||||
|
||||
/** |
||||
* Abstract base class for reactive HandlerMethod-based message handling. |
||||
* Provides most of the logic required to discover handler methods at startup, |
||||
* find a matching handler method at runtime for a given message and invoke it. |
||||
* |
||||
* <p>Also supports discovering and invoking exception handling methods to process |
||||
* exceptions raised during message handling. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.2 |
||||
* @param <T> the type of the Object that contains information mapping information |
||||
*/ |
||||
public abstract class AbstractMethodMessageHandler<T> |
||||
implements ReactiveMessageHandler, ApplicationContextAware, InitializingBean { |
||||
|
||||
/** |
||||
* Bean name prefix for target beans behind scoped proxies. Used to exclude those |
||||
* targets from handler method detection, in favor of the corresponding proxies. |
||||
* <p>We're not checking the autowire-candidate status here, which is how the |
||||
* proxy target filtering problem is being handled at the autowiring level, |
||||
* since autowire-candidate may have been turned to {@code false} for other |
||||
* reasons, while still expecting the bean to be eligible for handler methods. |
||||
* <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils} |
||||
* but duplicated here to avoid a hard dependency on the spring-aop module. |
||||
*/ |
||||
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; |
||||
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass()); |
||||
|
||||
|
||||
private ArgumentResolverConfigurer argumentResolverConfigurer = new ArgumentResolverConfigurer(); |
||||
|
||||
private ReturnValueHandlerConfigurer returnValueHandlerConfigurer = new ReturnValueHandlerConfigurer(); |
||||
|
||||
private final HandlerMethodArgumentResolverComposite argumentResolvers = |
||||
new HandlerMethodArgumentResolverComposite(); |
||||
|
||||
private final HandlerMethodReturnValueHandlerComposite returnValueHandlers = |
||||
new HandlerMethodReturnValueHandlerComposite(); |
||||
|
||||
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); |
||||
|
||||
@Nullable |
||||
private ApplicationContext applicationContext; |
||||
|
||||
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<>(64); |
||||
|
||||
private final MultiValueMap<String, T> destinationLookup = new LinkedMultiValueMap<>(64); |
||||
|
||||
private final Map<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache = |
||||
new ConcurrentHashMap<>(64); |
||||
|
||||
private final Map<MessagingAdviceBean, AbstractExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = |
||||
new LinkedHashMap<>(64); |
||||
|
||||
|
||||
/** |
||||
* Configure custom resolvers for handler method arguments. |
||||
*/ |
||||
public void setArgumentResolverConfigurer(ArgumentResolverConfigurer configurer) { |
||||
Assert.notNull(configurer, "HandlerMethodArgumentResolver is required."); |
||||
this.argumentResolverConfigurer = configurer; |
||||
} |
||||
|
||||
/** |
||||
* Return the configured custom resolvers for handler method arguments. |
||||
*/ |
||||
public ArgumentResolverConfigurer getArgumentResolverConfigurer() { |
||||
return this.argumentResolverConfigurer; |
||||
} |
||||
|
||||
/** |
||||
* Configure custom return value handlers for handler metohds. |
||||
*/ |
||||
public void setReturnValueHandlerConfigurer(ReturnValueHandlerConfigurer configurer) { |
||||
Assert.notNull(configurer, "ReturnValueHandlerConfigurer is required."); |
||||
this.returnValueHandlerConfigurer = configurer; |
||||
} |
||||
|
||||
/** |
||||
* Return the configured return value handlers. |
||||
*/ |
||||
public ReturnValueHandlerConfigurer getReturnValueHandlerConfigurer() { |
||||
return this.returnValueHandlerConfigurer; |
||||
} |
||||
|
||||
/** |
||||
* Configure the registry for adapting various reactive types. |
||||
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with |
||||
* default settings. |
||||
*/ |
||||
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { |
||||
Assert.notNull(registry, "ReactiveAdapterRegistry is required"); |
||||
this.reactiveAdapterRegistry = registry; |
||||
} |
||||
|
||||
/** |
||||
* Return the configured registry for adapting reactive types. |
||||
*/ |
||||
public ReactiveAdapterRegistry getReactiveAdapterRegistry() { |
||||
return this.reactiveAdapterRegistry; |
||||
} |
||||
|
||||
@Override |
||||
public void setApplicationContext(@Nullable ApplicationContext applicationContext) { |
||||
this.applicationContext = applicationContext; |
||||
} |
||||
|
||||
@Nullable |
||||
public ApplicationContext getApplicationContext() { |
||||
return this.applicationContext; |
||||
} |
||||
|
||||
/** |
||||
* Subclasses can invoke this method to populate the MessagingAdviceBean cache |
||||
* (e.g. to support "global" {@code @MessageExceptionHandler}). |
||||
*/ |
||||
protected void registerExceptionHandlerAdvice( |
||||
MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) { |
||||
|
||||
this.exceptionHandlerAdviceCache.put(bean, resolver); |
||||
} |
||||
|
||||
/** |
||||
* Return a read-only map with all handler methods and their mappings. |
||||
*/ |
||||
public Map<T, HandlerMethod> getHandlerMethods() { |
||||
return Collections.unmodifiableMap(this.handlerMethods); |
||||
} |
||||
|
||||
/** |
||||
* Return a read-only multi-value map with a direct lookup of mappings, |
||||
* (e.g. for non-pattern destinations). |
||||
*/ |
||||
public MultiValueMap<String, T> getDestinationLookup() { |
||||
return CollectionUtils.unmodifiableMultiValueMap(this.destinationLookup); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public void afterPropertiesSet() { |
||||
|
||||
List<? extends HandlerMethodArgumentResolver> resolvers = initArgumentResolvers(); |
||||
if (resolvers.isEmpty()) { |
||||
resolvers = new ArrayList<>(this.argumentResolverConfigurer.getCustomResolvers()); |
||||
} |
||||
this.argumentResolvers.addResolvers(resolvers); |
||||
|
||||
List<? extends HandlerMethodReturnValueHandler> handlers = initReturnValueHandlers(); |
||||
if (handlers.isEmpty()) { |
||||
handlers = new ArrayList<>(this.returnValueHandlerConfigurer.getCustomHandlers()); |
||||
} |
||||
this.returnValueHandlers.addHandlers(handlers); |
||||
|
||||
initHandlerMethods(); |
||||
} |
||||
|
||||
/** |
||||
* Return the list of argument resolvers to use. |
||||
* <p>Subclasses should also take into account custom argument types configured via |
||||
* {@link #setArgumentResolverConfigurer}. |
||||
*/ |
||||
protected abstract List<? extends HandlerMethodArgumentResolver> initArgumentResolvers(); |
||||
|
||||
/** |
||||
* Return the list of return value handlers to use. |
||||
* <p>Subclasses should also take into account custom return value types configured |
||||
* via {@link #setReturnValueHandlerConfigurer}. |
||||
*/ |
||||
protected abstract List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers(); |
||||
|
||||
|
||||
private void initHandlerMethods() { |
||||
if (this.applicationContext == null) { |
||||
logger.warn("No ApplicationContext available for detecting beans with message handling methods."); |
||||
return; |
||||
} |
||||
for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) { |
||||
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { |
||||
Class<?> beanType = null; |
||||
try { |
||||
beanType = this.applicationContext.getType(beanName); |
||||
} |
||||
catch (Throwable ex) { |
||||
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
|
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); |
||||
} |
||||
} |
||||
if (beanType != null && isHandler(beanType)) { |
||||
detectHandlerMethods(beanName); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Whether the given bean could contain message handling methods. |
||||
*/ |
||||
protected abstract boolean isHandler(Class<?> beanType); |
||||
|
||||
/** |
||||
* Detect if the given handler has any methods that can handle messages and if |
||||
* so register it with the extracted mapping information. |
||||
* @param handler the handler to check, either an instance of a Spring bean name |
||||
*/ |
||||
private void detectHandlerMethods(Object handler) { |
||||
Class<?> handlerType; |
||||
if (handler instanceof String) { |
||||
ApplicationContext context = getApplicationContext(); |
||||
Assert.state(context != null, "ApplicationContext is required for resolving handler bean names"); |
||||
handlerType = context.getType((String) handler); |
||||
} |
||||
else { |
||||
handlerType = handler.getClass(); |
||||
} |
||||
if (handlerType != null) { |
||||
final Class<?> userType = ClassUtils.getUserClass(handlerType); |
||||
Map<Method, T> methods = MethodIntrospector.selectMethods(userType, |
||||
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug(methods.size() + " message handler methods found on " + userType + ": " + methods); |
||||
} |
||||
methods.forEach((key, value) -> registerHandlerMethod(handler, key, value)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Obtain the mapping for the given method, if any. |
||||
* @param method the method to check |
||||
* @param handlerType the handler type, possibly a sub-type of the method's declaring class
|
||||
* @return the mapping, or {@code null} if the method is not mapped |
||||
*/ |
||||
@Nullable |
||||
protected abstract T getMappingForMethod(Method method, Class<?> handlerType); |
||||
|
||||
/** |
||||
* Register a handler method and its unique mapping, on startup. |
||||
* @param handler the bean name of the handler or the handler instance |
||||
* @param method the method to register |
||||
* @param mapping the mapping conditions associated with the handler method |
||||
* @throws IllegalStateException if another method was already registered |
||||
* under the same mapping |
||||
*/ |
||||
protected void registerHandlerMethod(Object handler, Method method, T mapping) { |
||||
Assert.notNull(mapping, "Mapping must not be null"); |
||||
HandlerMethod newHandlerMethod = createHandlerMethod(handler, method); |
||||
HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping); |
||||
|
||||
if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) { |
||||
throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() + |
||||
"' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" + |
||||
oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped."); |
||||
} |
||||
|
||||
this.handlerMethods.put(mapping, newHandlerMethod); |
||||
if (logger.isTraceEnabled()) { |
||||
logger.trace("Mapped \"" + mapping + "\" onto " + newHandlerMethod); |
||||
} |
||||
|
||||
for (String pattern : getDirectLookupMappings(mapping)) { |
||||
this.destinationLookup.add(pattern, mapping); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Create a HandlerMethod instance from an Object handler that is either a handler |
||||
* instance or a String-based bean name. |
||||
*/ |
||||
private HandlerMethod createHandlerMethod(Object handler, Method method) { |
||||
HandlerMethod handlerMethod; |
||||
if (handler instanceof String) { |
||||
ApplicationContext context = getApplicationContext(); |
||||
Assert.state(context != null, "ApplicationContext is required for resolving handler bean names"); |
||||
String beanName = (String) handler; |
||||
handlerMethod = new HandlerMethod(beanName, context.getAutowireCapableBeanFactory(), method); |
||||
} |
||||
else { |
||||
handlerMethod = new HandlerMethod(handler, method); |
||||
} |
||||
return handlerMethod; |
||||
} |
||||
|
||||
/** |
||||
* Return String-based destinations for the given mapping, if any, that can |
||||
* be used to find matches with a direct lookup (i.e. non-patterns). |
||||
* <p><strong>Note:</strong> This is completely optional. The mapping |
||||
* metadata for a sub-class may support neither direct lookups, nor String |
||||
* based destinations. |
||||
*/ |
||||
protected abstract Set<String> getDirectLookupMappings(T mapping); |
||||
|
||||
|
||||
@Override |
||||
public Mono<Void> handleMessage(Message<?> message) throws MessagingException { |
||||
Match<T> match = getHandlerMethod(message); |
||||
if (match == null) { |
||||
return Mono.empty(); |
||||
} |
||||
HandlerMethod handlerMethod = match.getHandlerMethod().createWithResolvedBean(); |
||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); |
||||
invocable.setArgumentResolvers(this.argumentResolvers.getResolvers()); |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Invoking " + invocable.getShortLogMessage()); |
||||
} |
||||
return invocable.invoke(message) |
||||
.flatMap(value -> { |
||||
MethodParameter returnType = invocable.getReturnType(); |
||||
return this.returnValueHandlers.handleReturnValue(value, returnType, message); |
||||
}) |
||||
.onErrorResume(throwable -> { |
||||
Exception ex = (throwable instanceof Exception) ? (Exception) throwable : |
||||
new MessageHandlingException(message, "HandlerMethod invocation error", throwable); |
||||
return processHandlerException(message, handlerMethod, ex); |
||||
}); |
||||
} |
||||
|
||||
@Nullable |
||||
private Match<T> getHandlerMethod(Message<?> message) { |
||||
List<Match<T>> matches = new ArrayList<>(); |
||||
|
||||
String destination = getDestination(message); |
||||
List<T> mappingsByUrl = destination != null ? this.destinationLookup.get(destination) : null; |
||||
if (mappingsByUrl != null) { |
||||
addMatchesToCollection(mappingsByUrl, message, matches); |
||||
} |
||||
if (matches.isEmpty()) { |
||||
// No direct hits, go through all mappings
|
||||
Set<T> allMappings = this.handlerMethods.keySet(); |
||||
addMatchesToCollection(allMappings, message, matches); |
||||
} |
||||
if (matches.isEmpty()) { |
||||
return null; |
||||
} |
||||
Comparator<Match<T>> comparator = new MatchComparator(getMappingComparator(message)); |
||||
matches.sort(comparator); |
||||
if (logger.isTraceEnabled()) { |
||||
logger.trace("Found " + matches.size() + " handler methods: " + matches); |
||||
} |
||||
Match<T> bestMatch = matches.get(0); |
||||
if (matches.size() > 1) { |
||||
Match<T> secondBestMatch = matches.get(1); |
||||
if (comparator.compare(bestMatch, secondBestMatch) == 0) { |
||||
Method m1 = bestMatch.handlerMethod.getMethod(); |
||||
Method m2 = secondBestMatch.handlerMethod.getMethod(); |
||||
throw new IllegalStateException("Ambiguous handler methods mapped for destination '" + |
||||
destination + "': {" + m1 + ", " + m2 + "}"); |
||||
} |
||||
} |
||||
return bestMatch; |
||||
} |
||||
|
||||
/** |
||||
* Extract a String-based destination, if any, that can be used to perform |
||||
* a direct look up into the registered mappings. |
||||
* <p><strong>Note:</strong> This is completely optional. The mapping |
||||
* metadata for a sub-class may support neither direct lookups, nor String |
||||
* based destinations. |
||||
* @see #getDirectLookupMappings(Object) |
||||
*/ |
||||
@Nullable |
||||
protected abstract String getDestination(Message<?> message); |
||||
|
||||
private void addMatchesToCollection( |
||||
Collection<T> mappingsToCheck, Message<?> message, List<Match<T>> matches) { |
||||
|
||||
for (T mapping : mappingsToCheck) { |
||||
T match = getMatchingMapping(mapping, message); |
||||
if (match != null) { |
||||
matches.add(new Match<T>(match, this.handlerMethods.get(mapping))); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Check if a mapping matches the current message and return a possibly |
||||
* new mapping with conditions relevant to the current request. |
||||
* @param mapping the mapping to get a match for |
||||
* @param message the message being handled |
||||
* @return the match or {@code null} if there is no match |
||||
*/ |
||||
@Nullable |
||||
protected abstract T getMatchingMapping(T mapping, Message<?> message); |
||||
|
||||
/** |
||||
* Return a comparator for sorting matching mappings. |
||||
* The returned comparator should sort 'better' matches higher. |
||||
* @param message the current Message |
||||
* @return the comparator, never {@code null} |
||||
*/ |
||||
protected abstract Comparator<T> getMappingComparator(Message<?> message); |
||||
|
||||
|
||||
private Mono<Void> processHandlerException(Message<?> message, HandlerMethod handlerMethod, Exception ex) { |
||||
InvocableHandlerMethod exceptionInvocable = findExceptionHandler(handlerMethod, ex); |
||||
if (exceptionInvocable == null) { |
||||
logger.error("Unhandled exception from message handling method", ex); |
||||
return Mono.empty(); |
||||
} |
||||
exceptionInvocable.setArgumentResolvers(this.argumentResolvers.getResolvers()); |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Invoking " + exceptionInvocable.getShortLogMessage()); |
||||
} |
||||
return exceptionInvocable.invoke(message, ex) |
||||
.flatMap(value -> { |
||||
MethodParameter returnType = exceptionInvocable.getReturnType(); |
||||
return this.returnValueHandlers.handleReturnValue(value, returnType, message); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Find an exception handling method for the given exception. |
||||
* <p>The default implementation searches methods in the class hierarchy of |
||||
* the HandlerMethod first and if not found, it continues searching for |
||||
* additional handling methods registered via |
||||
* {@link #registerExceptionHandlerAdvice(MessagingAdviceBean, AbstractExceptionHandlerMethodResolver)}. |
||||
* @param handlerMethod the method where the exception was raised |
||||
* @param exception the raised exception |
||||
* @return a method to handle the exception, or {@code null} |
||||
*/ |
||||
@Nullable |
||||
protected InvocableHandlerMethod findExceptionHandler(HandlerMethod handlerMethod, Exception exception) { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Searching for methods to handle " + exception.getClass().getSimpleName()); |
||||
} |
||||
Class<?> beanType = handlerMethod.getBeanType(); |
||||
AbstractExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType); |
||||
if (resolver == null) { |
||||
resolver = createExceptionMethodResolverFor(beanType); |
||||
this.exceptionHandlerCache.put(beanType, resolver); |
||||
} |
||||
InvocableHandlerMethod exceptionHandlerMethod = null; |
||||
Method method = resolver.resolveMethod(exception); |
||||
if (method != null) { |
||||
exceptionHandlerMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method); |
||||
} |
||||
else { |
||||
for (MessagingAdviceBean advice : this.exceptionHandlerAdviceCache.keySet()) { |
||||
if (advice.isApplicableToBeanType(beanType)) { |
||||
resolver = this.exceptionHandlerAdviceCache.get(advice); |
||||
method = resolver.resolveMethod(exception); |
||||
if (method != null) { |
||||
exceptionHandlerMethod = new InvocableHandlerMethod(advice.resolveBean(), method); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
if (exceptionHandlerMethod != null) { |
||||
exceptionHandlerMethod.setArgumentResolvers(this.argumentResolvers.getResolvers()); |
||||
} |
||||
return exceptionHandlerMethod; |
||||
} |
||||
|
||||
/** |
||||
* Create a concrete instance of {@link AbstractExceptionHandlerMethodResolver} |
||||
* that finds exception handling methods based on some criteria, e.g. based |
||||
* on the presence of {@code @MessageExceptionHandler}. |
||||
* @param beanType the class in which an exception occurred during handling |
||||
* @return the resolver to use |
||||
*/ |
||||
protected abstract AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType); |
||||
|
||||
|
||||
/** |
||||
* Container for matched mapping and HandlerMethod. Used for best match |
||||
* comparison and for access to mapping information. |
||||
*/ |
||||
private static class Match<T> { |
||||
|
||||
private final T mapping; |
||||
|
||||
private final HandlerMethod handlerMethod; |
||||
|
||||
|
||||
Match(T mapping, HandlerMethod handlerMethod) { |
||||
this.mapping = mapping; |
||||
this.handlerMethod = handlerMethod; |
||||
} |
||||
|
||||
|
||||
public T getMapping() { |
||||
return this.mapping; |
||||
} |
||||
|
||||
public HandlerMethod getHandlerMethod() { |
||||
return this.handlerMethod; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.mapping.toString(); |
||||
} |
||||
} |
||||
|
||||
|
||||
private class MatchComparator implements Comparator<Match<T>> { |
||||
|
||||
private final Comparator<T> comparator; |
||||
|
||||
|
||||
MatchComparator(Comparator<T> comparator) { |
||||
this.comparator = comparator; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public int compare(Match<T> match1, Match<T> match2) { |
||||
return this.comparator.compare(match1.mapping, match2.mapping); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* |
||||
* Copyright 2002-2018 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.messaging.handler.invocation.reactive; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Assist with configuration for handler method argument resolvers. |
||||
* At present, it supports only providing a list of custom resolvers. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.2 |
||||
*/ |
||||
public class ArgumentResolverConfigurer { |
||||
|
||||
private final List<HandlerMethodArgumentResolver> customResolvers = new ArrayList<>(8); |
||||
|
||||
|
||||
/** |
||||
* Configure resolvers for custom handler method arguments. |
||||
* @param resolver the resolvers to add |
||||
*/ |
||||
public void addCustomResolver(HandlerMethodArgumentResolver... resolver) { |
||||
Assert.notNull(resolver, "'resolvers' must not be null"); |
||||
this.customResolvers.addAll(Arrays.asList(resolver)); |
||||
} |
||||
|
||||
|
||||
public List<HandlerMethodArgumentResolver> getCustomResolvers() { |
||||
return this.customResolvers; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
/* |
||||
* Copyright 2002-2018 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.messaging.handler.invocation.reactive; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Assist with configuration for handler method return value handlers. |
||||
* At present, it supports only providing a list of custom handlers. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.2 |
||||
*/ |
||||
public class ReturnValueHandlerConfigurer { |
||||
|
||||
private final List<HandlerMethodReturnValueHandler> customHandlers = new ArrayList<>(8); |
||||
|
||||
|
||||
/** |
||||
* Configure custom return value handlers for handler methods. |
||||
* @param handlers the handlers to add |
||||
*/ |
||||
public void addCustomHandler(HandlerMethodReturnValueHandler... handlers) { |
||||
Assert.notNull(handlers, "'handlers' must not be null"); |
||||
this.customHandlers.addAll(Arrays.asList(handlers)); |
||||
} |
||||
|
||||
|
||||
public List<HandlerMethodReturnValueHandler> getCustomHandlers() { |
||||
return this.customHandlers; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.messaging.handler.invocation; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.core.MethodIntrospector; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
/** |
||||
* Sub-class for {@link AbstractExceptionHandlerMethodResolver} for testing. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class TestExceptionResolver extends AbstractExceptionHandlerMethodResolver { |
||||
|
||||
private final static ReflectionUtils.MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = |
||||
method -> method.getName().matches("handle[\\w]*Exception"); |
||||
|
||||
|
||||
public TestExceptionResolver(Class<?> handlerType) { |
||||
super(initExceptionMappings(handlerType)); |
||||
} |
||||
|
||||
private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) { |
||||
Map<Class<? extends Throwable>, Method> result = new HashMap<>(); |
||||
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) { |
||||
for (Class<? extends Throwable> exception : getExceptionsFromMethodSignature(method)) { |
||||
result.put(exception, method); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,261 @@
@@ -0,0 +1,261 @@
|
||||
/* |
||||
* Copyright 2002-2018 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.messaging.handler.invocation.reactive; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.time.Duration; |
||||
import java.util.Collections; |
||||
import java.util.Comparator; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.hamcrest.Matchers; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
import reactor.test.StepVerifier; |
||||
|
||||
import org.springframework.context.support.StaticApplicationContext; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.handler.DestinationPatternsMessageCondition; |
||||
import org.springframework.messaging.handler.HandlerMethod; |
||||
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; |
||||
import org.springframework.messaging.handler.invocation.TestExceptionResolver; |
||||
import org.springframework.messaging.support.GenericMessage; |
||||
import org.springframework.util.AntPathMatcher; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.PathMatcher; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* Unit tests for {@link AbstractMethodMessageHandler}. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class MethodMessageHandlerTests { |
||||
|
||||
|
||||
@Test(expected = IllegalStateException.class) |
||||
public void duplicateMapping() { |
||||
initMethodMessageHandler(DuplicateMappingsController.class); |
||||
} |
||||
|
||||
@Test |
||||
public void registeredMappings() { |
||||
TestMethodMessageHandler messageHandler = initMethodMessageHandler(TestController.class); |
||||
Map<String, HandlerMethod> mappings = messageHandler.getHandlerMethods(); |
||||
|
||||
assertEquals(5, mappings.keySet().size()); |
||||
assertThat(mappings.keySet(), Matchers.containsInAnyOrder( |
||||
"/handleMessage", "/handleMessageWithArgument", "/handleMessageAndThrow", |
||||
"/handleMessageMatch1", "/handleMessageMatch2")); |
||||
} |
||||
|
||||
@Test |
||||
public void bestMatch() throws NoSuchMethodException { |
||||
TestMethodMessageHandler handler = new TestMethodMessageHandler(); |
||||
TestController controller = new TestController(); |
||||
handler.register(controller, TestController.class.getMethod("handleMessageMatch1"), "/bestmatch/{foo}/path"); |
||||
handler.register(controller, TestController.class.getMethod("handleMessageMatch2"), "/bestmatch/*/*"); |
||||
handler.afterPropertiesSet(); |
||||
|
||||
Message<?> message = new GenericMessage<>("body", Collections.singletonMap( |
||||
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/bestmatch/bar/path")); |
||||
|
||||
handler.handleMessage(message).block(Duration.ofSeconds(5)); |
||||
|
||||
StepVerifier.create((Mono<Object>) handler.getLastReturnValue()) |
||||
.expectNext("handleMessageMatch1") |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
@Test |
||||
public void argumentResolution() { |
||||
|
||||
ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer(); |
||||
configurer.addCustomResolver(new StubArgumentResolver(String.class, "foo")); |
||||
|
||||
TestMethodMessageHandler handler = initMethodMessageHandler( |
||||
theHandler -> theHandler.setArgumentResolverConfigurer(configurer), |
||||
TestController.class); |
||||
|
||||
Message<?> message = new GenericMessage<>("body", Collections.singletonMap( |
||||
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageWithArgument")); |
||||
|
||||
handler.handleMessage(message).block(Duration.ofSeconds(5)); |
||||
|
||||
StepVerifier.create((Mono<Object>) handler.getLastReturnValue()) |
||||
.expectNext("handleMessageWithArgument,payload=foo") |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
@Test |
||||
public void handleException() { |
||||
|
||||
TestMethodMessageHandler handler = initMethodMessageHandler(TestController.class); |
||||
|
||||
Message<?> message = new GenericMessage<>("body", Collections.singletonMap( |
||||
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageAndThrow")); |
||||
|
||||
handler.handleMessage(message).block(Duration.ofSeconds(5)); |
||||
|
||||
StepVerifier.create((Mono<Object>) handler.getLastReturnValue()) |
||||
.expectNext("handleIllegalStateException,ex=rejected") |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
|
||||
private TestMethodMessageHandler initMethodMessageHandler(Class<?>... handlerTypes) { |
||||
return initMethodMessageHandler(handler -> {}, handlerTypes); |
||||
} |
||||
|
||||
private TestMethodMessageHandler initMethodMessageHandler( |
||||
Consumer<TestMethodMessageHandler> customizer, Class<?>... handlerTypes) { |
||||
|
||||
StaticApplicationContext context = new StaticApplicationContext(); |
||||
for (Class<?> handlerType : handlerTypes) { |
||||
String beanName = ClassUtils.getShortNameAsProperty(handlerType); |
||||
context.registerPrototype(beanName, handlerType); |
||||
} |
||||
TestMethodMessageHandler messageHandler = new TestMethodMessageHandler(); |
||||
messageHandler.setApplicationContext(context); |
||||
customizer.accept(messageHandler); |
||||
messageHandler.afterPropertiesSet(); |
||||
return messageHandler; |
||||
} |
||||
|
||||
|
||||
@SuppressWarnings("unused") |
||||
private static class TestController { |
||||
|
||||
public Mono<String> handleMessage() { |
||||
return delay("handleMessage"); |
||||
} |
||||
|
||||
@SuppressWarnings("rawtypes") |
||||
public Mono<String> handleMessageWithArgument(String payload) { |
||||
return delay("handleMessageWithArgument,payload=" + payload); |
||||
} |
||||
|
||||
public Mono<Void> handleMessageAndThrow() { |
||||
return Mono.delay(Duration.ofMillis(10)) |
||||
.flatMap(aLong -> Mono.error(new IllegalStateException("rejected"))); |
||||
} |
||||
|
||||
public Mono<String> handleMessageMatch1() { |
||||
return delay("handleMessageMatch1"); |
||||
} |
||||
|
||||
public Mono<String> handleMessageMatch2() { |
||||
return delay("handleMessageMatch2"); |
||||
} |
||||
|
||||
public Mono<String> handleIllegalStateException(IllegalStateException ex) { |
||||
return delay("handleIllegalStateException,ex=" + ex.getMessage()); |
||||
} |
||||
|
||||
private Mono<String> delay(String value) { |
||||
return Mono.delay(Duration.ofMillis(10)).map(aLong -> value); |
||||
} |
||||
} |
||||
|
||||
|
||||
@SuppressWarnings("unused") |
||||
private static class DuplicateMappingsController { |
||||
|
||||
void handleMessageFoo() { } |
||||
|
||||
void handleMessageFoo(String foo) { } |
||||
} |
||||
|
||||
|
||||
private static class TestMethodMessageHandler extends AbstractMethodMessageHandler<String> { |
||||
|
||||
private final TestReturnValueHandler returnValueHandler = new TestReturnValueHandler(); |
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher(); |
||||
|
||||
|
||||
@Override |
||||
protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() { |
||||
return Collections.emptyList(); |
||||
} |
||||
|
||||
@Override |
||||
protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() { |
||||
return Collections.singletonList(this.returnValueHandler); |
||||
} |
||||
|
||||
@Nullable |
||||
public Object getLastReturnValue() { |
||||
return this.returnValueHandler.getLastReturnValue(); |
||||
} |
||||
|
||||
public void register(Object handler, Method method, String mapping) { |
||||
super.registerHandlerMethod(handler, method, mapping); |
||||
} |
||||
|
||||
@Override |
||||
protected boolean isHandler(Class<?> handlerType) { |
||||
return handlerType.getName().endsWith("Controller"); |
||||
} |
||||
|
||||
@Override |
||||
protected String getMappingForMethod(Method method, Class<?> handlerType) { |
||||
String methodName = method.getName(); |
||||
if (methodName.startsWith("handleMessage")) { |
||||
return "/" + methodName; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
protected Set<String> getDirectLookupMappings(String mapping) { |
||||
return Collections.singleton(mapping); |
||||
} |
||||
|
||||
@Override |
||||
@Nullable |
||||
protected String getDestination(Message<?> message) { |
||||
return (String) message.getHeaders().get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER); |
||||
} |
||||
|
||||
@Override |
||||
protected String getMatchingMapping(String mapping, Message<?> message) { |
||||
String destination = getDestination(message); |
||||
Assert.notNull(destination, "No destination"); |
||||
return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null; |
||||
} |
||||
|
||||
@Override |
||||
protected Comparator<String> getMappingComparator(Message<?> message) { |
||||
return (info1, info2) -> { |
||||
DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1); |
||||
DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2); |
||||
return cond1.compareTo(cond2, message); |
||||
}; |
||||
} |
||||
|
||||
@Override |
||||
protected AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType) { |
||||
return new TestExceptionResolver(beanType); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.messaging.handler.invocation.reactive; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.messaging.Message; |
||||
|
||||
/** |
||||
* Return value handler that simply stores the last return value. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class TestReturnValueHandler implements HandlerMethodReturnValueHandler { |
||||
|
||||
@Nullable |
||||
private Object lastReturnValue; |
||||
|
||||
|
||||
@Nullable |
||||
public Object getLastReturnValue() { |
||||
return this.lastReturnValue; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public boolean supportsReturnType(MethodParameter returnType) { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public Mono<Void> handleReturnValue(@Nullable Object value, MethodParameter returnType, Message<?> message) { |
||||
this.lastReturnValue = value; |
||||
return Mono.empty(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue