9 changed files with 890 additions and 0 deletions
@ -0,0 +1,52 @@ |
|||||||
|
/* |
||||||
|
* 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 reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter; |
||||||
|
import org.springframework.messaging.Message; |
||||||
|
|
||||||
|
/** |
||||||
|
* Strategy interface for resolving method parameters into argument values |
||||||
|
* in the context of a given {@link Message}. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public interface HandlerMethodArgumentResolver { |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether the given {@linkplain MethodParameter method parameter} is |
||||||
|
* supported by this resolver. |
||||||
|
* @param parameter the method parameter to check |
||||||
|
* @return {@code true} if this resolver supports the supplied parameter; |
||||||
|
* {@code false} otherwise |
||||||
|
*/ |
||||||
|
boolean supportsParameter(MethodParameter parameter); |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolves a method parameter into an argument value from a given message. |
||||||
|
* @param parameter the method parameter to resolve. |
||||||
|
* This parameter must have previously been passed to |
||||||
|
* {@link #supportsParameter(org.springframework.core.MethodParameter)} |
||||||
|
* which must have returned {@code true}. |
||||||
|
* @param message the currently processed message |
||||||
|
* @return {@code Mono} for the argument value, possibly empty |
||||||
|
*/ |
||||||
|
Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,142 @@ |
|||||||
|
/* |
||||||
|
* 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.Collections; |
||||||
|
import java.util.LinkedList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
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.core.MethodParameter; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.messaging.Message; |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolves method parameters by delegating to a list of registered |
||||||
|
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. |
||||||
|
* Previously resolved method parameters are cached for faster lookups. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { |
||||||
|
|
||||||
|
protected final Log logger = LogFactory.getLog(getClass()); |
||||||
|
|
||||||
|
private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>(); |
||||||
|
|
||||||
|
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = |
||||||
|
new ConcurrentHashMap<>(256); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add the given {@link HandlerMethodArgumentResolver}. |
||||||
|
*/ |
||||||
|
public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) { |
||||||
|
this.argumentResolvers.add(resolver); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. |
||||||
|
*/ |
||||||
|
public HandlerMethodArgumentResolverComposite addResolvers(@Nullable HandlerMethodArgumentResolver... resolvers) { |
||||||
|
if (resolvers != null) { |
||||||
|
Collections.addAll(this.argumentResolvers, resolvers); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. |
||||||
|
*/ |
||||||
|
public HandlerMethodArgumentResolverComposite addResolvers( |
||||||
|
@Nullable List<? extends HandlerMethodArgumentResolver> resolvers) { |
||||||
|
|
||||||
|
if (resolvers != null) { |
||||||
|
this.argumentResolvers.addAll(resolvers); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a read-only list with the contained resolvers, or an empty list. |
||||||
|
*/ |
||||||
|
public List<HandlerMethodArgumentResolver> getResolvers() { |
||||||
|
return Collections.unmodifiableList(this.argumentResolvers); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clear the list of configured resolvers. |
||||||
|
*/ |
||||||
|
public void clear() { |
||||||
|
this.argumentResolvers.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Whether the given {@linkplain MethodParameter method parameter} is |
||||||
|
* supported by any registered {@link HandlerMethodArgumentResolver}. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public boolean supportsParameter(MethodParameter parameter) { |
||||||
|
return getArgumentResolver(parameter) != null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Iterate over registered |
||||||
|
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} and |
||||||
|
* invoke the one that supports it. |
||||||
|
* @throws IllegalStateException if no suitable |
||||||
|
* {@link HandlerMethodArgumentResolver} is found. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { |
||||||
|
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); |
||||||
|
if (resolver == null) { |
||||||
|
throw new IllegalArgumentException( |
||||||
|
"Unsupported parameter type [" + parameter.getParameterType().getName() + "]." + |
||||||
|
" supportsParameter should be called first."); |
||||||
|
} |
||||||
|
return resolver.resolveArgument(parameter, message); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Find a registered {@link HandlerMethodArgumentResolver} that supports |
||||||
|
* the given method parameter. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { |
||||||
|
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); |
||||||
|
if (result == null) { |
||||||
|
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) { |
||||||
|
if (methodArgumentResolver.supportsParameter(parameter)) { |
||||||
|
result = methodArgumentResolver; |
||||||
|
this.argumentResolverCache.put(parameter, result); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2017 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; |
||||||
|
|
||||||
|
/** |
||||||
|
* Handle the return value from the invocation of an annotated {@link Message} |
||||||
|
* handling method. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public interface HandlerMethodReturnValueHandler { |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether the given {@linkplain MethodParameter method return type} is |
||||||
|
* supported by this handler. |
||||||
|
* @param returnType the method return type to check |
||||||
|
* @return {@code true} if this handler supports the supplied return type; |
||||||
|
* {@code false} otherwise |
||||||
|
*/ |
||||||
|
boolean supportsReturnType(MethodParameter returnType); |
||||||
|
|
||||||
|
/** |
||||||
|
* Handle the given return value. |
||||||
|
* @param returnValue the value returned from the handler method |
||||||
|
* @param returnType the type of the return value. This type must have previously |
||||||
|
* been passed to {@link #supportsReturnType(MethodParameter)} |
||||||
|
* and it must have returned {@code true}. |
||||||
|
* @return {@code Mono<Void>} to indicate when handling is complete. |
||||||
|
*/ |
||||||
|
Mono<Void> handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, Message<?> message); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
/* |
||||||
|
* 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.Collections; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.messaging.Message; |
||||||
|
|
||||||
|
/** |
||||||
|
* A HandlerMethodReturnValueHandler that wraps and delegates to others. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { |
||||||
|
|
||||||
|
protected final Log logger = LogFactory.getLog(getClass()); |
||||||
|
|
||||||
|
|
||||||
|
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Return a read-only list with the configured handlers. |
||||||
|
*/ |
||||||
|
public List<HandlerMethodReturnValueHandler> getReturnValueHandlers() { |
||||||
|
return Collections.unmodifiableList(this.returnValueHandlers); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clear the list of configured handlers. |
||||||
|
*/ |
||||||
|
public void clear() { |
||||||
|
this.returnValueHandlers.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add the given {@link HandlerMethodReturnValueHandler}. |
||||||
|
*/ |
||||||
|
public HandlerMethodReturnValueHandlerComposite addHandler(HandlerMethodReturnValueHandler returnValueHandler) { |
||||||
|
this.returnValueHandlers.add(returnValueHandler); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add the given {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers}. |
||||||
|
*/ |
||||||
|
public HandlerMethodReturnValueHandlerComposite addHandlers( |
||||||
|
@Nullable List<? extends HandlerMethodReturnValueHandler> handlers) { |
||||||
|
|
||||||
|
if (handlers != null) { |
||||||
|
this.returnValueHandlers.addAll(handlers); |
||||||
|
} |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsReturnType(MethodParameter returnType) { |
||||||
|
return getReturnValueHandler(returnType) != null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, Message<?> message) { |
||||||
|
HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType); |
||||||
|
if (handler == null) { |
||||||
|
throw new IllegalStateException("No handler for return value type: " + returnType.getParameterType()); |
||||||
|
} |
||||||
|
if (logger.isTraceEnabled()) { |
||||||
|
logger.trace("Processing return value with " + handler); |
||||||
|
} |
||||||
|
return handler.handleReturnValue(returnValue, returnType, message); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("ForLoopReplaceableByForEach") |
||||||
|
@Nullable |
||||||
|
private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { |
||||||
|
for (int i = 0; i < this.returnValueHandlers.size(); i++) { |
||||||
|
HandlerMethodReturnValueHandler handler = this.returnValueHandlers.get(i); |
||||||
|
if (handler.supportsReturnType(returnType)) { |
||||||
|
return handler; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,213 @@ |
|||||||
|
/* |
||||||
|
* 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.InvocationTargetException; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.lang.reflect.ParameterizedType; |
||||||
|
import java.lang.reflect.Type; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Stream; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.core.DefaultParameterNameDiscoverer; |
||||||
|
import org.springframework.core.MethodParameter; |
||||||
|
import org.springframework.core.ParameterNameDiscoverer; |
||||||
|
import org.springframework.core.ReactiveAdapter; |
||||||
|
import org.springframework.core.ReactiveAdapterRegistry; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.messaging.Message; |
||||||
|
import org.springframework.messaging.handler.HandlerMethod; |
||||||
|
import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; |
||||||
|
import org.springframework.util.ObjectUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Extension of {@link HandlerMethod} that invokes the underlying method with |
||||||
|
* argument values resolved from the current HTTP request through a list of |
||||||
|
* {@link HandlerMethodArgumentResolver}. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class InvocableHandlerMethod extends HandlerMethod { |
||||||
|
|
||||||
|
private static final Mono<Object[]> EMPTY_ARGS = Mono.just(new Object[0]); |
||||||
|
|
||||||
|
private static final Object NO_ARG_VALUE = new Object(); |
||||||
|
|
||||||
|
|
||||||
|
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); |
||||||
|
|
||||||
|
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); |
||||||
|
|
||||||
|
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create an instance from a {@code HandlerMethod}. |
||||||
|
*/ |
||||||
|
public InvocableHandlerMethod(HandlerMethod handlerMethod) { |
||||||
|
super(handlerMethod); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create an instance from a bean instance and a method. |
||||||
|
*/ |
||||||
|
public InvocableHandlerMethod(Object bean, Method method) { |
||||||
|
super(bean, method); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Configure the argument resolvers to use to use for resolving method |
||||||
|
* argument values against a {@code ServerWebExchange}. |
||||||
|
*/ |
||||||
|
public void setArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { |
||||||
|
this.resolvers.addResolvers(resolvers); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the configured argument resolvers. |
||||||
|
*/ |
||||||
|
public List<HandlerMethodArgumentResolver> getResolvers() { |
||||||
|
return this.resolvers.getResolvers(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Set the ParameterNameDiscoverer for resolving parameter names when needed |
||||||
|
* (e.g. default request attribute name). |
||||||
|
* <p>Default is a {@link DefaultParameterNameDiscoverer}. |
||||||
|
*/ |
||||||
|
public void setParameterNameDiscoverer(ParameterNameDiscoverer nameDiscoverer) { |
||||||
|
this.parameterNameDiscoverer = nameDiscoverer; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the configured parameter name discoverer. |
||||||
|
*/ |
||||||
|
public ParameterNameDiscoverer getParameterNameDiscoverer() { |
||||||
|
return this.parameterNameDiscoverer; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Configure a reactive registry. This is needed for cases where the response |
||||||
|
* is fully handled within the controller in combination with an async void |
||||||
|
* return value. |
||||||
|
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with |
||||||
|
* default settings. |
||||||
|
* @param registry the registry to use |
||||||
|
*/ |
||||||
|
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { |
||||||
|
this.reactiveAdapterRegistry = registry; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Invoke the method for the given exchange. |
||||||
|
* @param message the current message |
||||||
|
* @param providedArgs optional list of argument values to match by type |
||||||
|
* @return a Mono with the result from the invocation. |
||||||
|
*/ |
||||||
|
public Mono<Object> invoke(Message<?> message, Object... providedArgs) { |
||||||
|
|
||||||
|
return getMethodArgumentValues(message, providedArgs).flatMap(args -> { |
||||||
|
Object value; |
||||||
|
try { |
||||||
|
ReflectionUtils.makeAccessible(getBridgedMethod()); |
||||||
|
value = getBridgedMethod().invoke(getBean(), args); |
||||||
|
} |
||||||
|
catch (IllegalArgumentException ex) { |
||||||
|
assertTargetBean(getBridgedMethod(), getBean(), args); |
||||||
|
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument"); |
||||||
|
return Mono.error(new IllegalStateException(formatInvokeError(text, args), ex)); |
||||||
|
} |
||||||
|
catch (InvocationTargetException ex) { |
||||||
|
return Mono.error(ex.getTargetException()); |
||||||
|
} |
||||||
|
catch (Throwable ex) { |
||||||
|
// Unlikely to ever get here, but it must be handled...
|
||||||
|
return Mono.error(new IllegalStateException(formatInvokeError("Invocation failure", args), ex)); |
||||||
|
} |
||||||
|
|
||||||
|
MethodParameter returnType = getReturnType(); |
||||||
|
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(returnType.getParameterType()); |
||||||
|
return isAsyncVoidReturnType(returnType, adapter) ? |
||||||
|
Mono.from(adapter.toPublisher(value)) : Mono.justOrEmpty(value); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private Mono<Object[]> getMethodArgumentValues(Message<?> message, Object... providedArgs) { |
||||||
|
if (ObjectUtils.isEmpty(getMethodParameters())) { |
||||||
|
return EMPTY_ARGS; |
||||||
|
} |
||||||
|
MethodParameter[] parameters = getMethodParameters(); |
||||||
|
List<Mono<Object>> argMonos = new ArrayList<>(parameters.length); |
||||||
|
for (MethodParameter parameter : parameters) { |
||||||
|
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); |
||||||
|
Object providedArg = findProvidedArgument(parameter, providedArgs); |
||||||
|
if (providedArg != null) { |
||||||
|
argMonos.add(Mono.just(providedArg)); |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (!this.resolvers.supportsParameter(parameter)) { |
||||||
|
return Mono.error(new MethodArgumentResolutionException( |
||||||
|
message, parameter, formatArgumentError(parameter, "No suitable resolver"))); |
||||||
|
} |
||||||
|
try { |
||||||
|
argMonos.add(this.resolvers.resolveArgument(parameter, message) |
||||||
|
.defaultIfEmpty(NO_ARG_VALUE) |
||||||
|
.doOnError(cause -> logArgumentErrorIfNecessary(parameter, cause))); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
logArgumentErrorIfNecessary(parameter, ex); |
||||||
|
argMonos.add(Mono.error(ex)); |
||||||
|
} |
||||||
|
} |
||||||
|
return Mono.zip(argMonos, values -> |
||||||
|
Stream.of(values).map(o -> o != NO_ARG_VALUE ? o : null).toArray()); |
||||||
|
} |
||||||
|
|
||||||
|
private void logArgumentErrorIfNecessary(MethodParameter parameter, Throwable cause) { |
||||||
|
// Leave stack trace for later, if error is not handled..
|
||||||
|
String causeMessage = cause.getMessage(); |
||||||
|
if (!causeMessage.contains(parameter.getExecutable().toGenericString())) { |
||||||
|
if (logger.isDebugEnabled()) { |
||||||
|
logger.debug(formatArgumentError(parameter, causeMessage)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter reactiveAdapter) { |
||||||
|
if (reactiveAdapter != null && reactiveAdapter.supportsEmpty()) { |
||||||
|
if (reactiveAdapter.isNoValue()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
Type parameterType = returnType.getGenericParameterType(); |
||||||
|
if (parameterType instanceof ParameterizedType) { |
||||||
|
ParameterizedType type = (ParameterizedType) parameterType; |
||||||
|
if (type.getActualTypeArguments().length == 1) { |
||||||
|
return Void.class.equals(type.getActualTypeArguments()[0]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
/** |
||||||
|
* Common infrastructure for invoking message handler methods with non-blocking, |
||||||
|
* and reactive contracts. |
||||||
|
*/ |
||||||
|
@NonNullApi |
||||||
|
@NonNullFields |
||||||
|
package org.springframework.messaging.handler.invocation.reactive; |
||||||
|
|
||||||
|
import org.springframework.lang.NonNullApi; |
||||||
|
import org.springframework.lang.NonNullFields; |
||||||
@ -0,0 +1,237 @@ |
|||||||
|
/* |
||||||
|
* 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.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.atomic.AtomicReference; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
import reactor.test.StepVerifier; |
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.messaging.Message; |
||||||
|
import org.springframework.messaging.handler.ResolvableMethod; |
||||||
|
import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; |
||||||
|
|
||||||
|
import static org.junit.Assert.*; |
||||||
|
import static org.mockito.Mockito.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* Unit tests for {@link InvocableHandlerMethod}. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @author Juergen Hoeller |
||||||
|
*/ |
||||||
|
public class InvocableHandlerMethodTests { |
||||||
|
|
||||||
|
private final Message<?> message = mock(Message.class); |
||||||
|
|
||||||
|
private final List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void resolveArg() { |
||||||
|
this.resolvers.add(new StubArgumentResolver(99)); |
||||||
|
this.resolvers.add(new StubArgumentResolver("value")); |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); |
||||||
|
Object value = invokeAndBlock(new Handler(), method); |
||||||
|
|
||||||
|
assertEquals(1, getStubResolver(0).getResolvedParameters().size()); |
||||||
|
assertEquals(1, getStubResolver(1).getResolvedParameters().size()); |
||||||
|
assertEquals("99-value", value); |
||||||
|
assertEquals("intArg", getStubResolver(0).getResolvedParameters().get(0).getParameterName()); |
||||||
|
assertEquals("stringArg", getStubResolver(1).getResolvedParameters().get(0).getParameterName()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void resolveNoArgValue() { |
||||||
|
this.resolvers.add(new StubArgumentResolver(Integer.class)); |
||||||
|
this.resolvers.add(new StubArgumentResolver(String.class)); |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); |
||||||
|
Object value = invokeAndBlock(new Handler(), method); |
||||||
|
|
||||||
|
assertEquals(1, getStubResolver(0).getResolvedParameters().size()); |
||||||
|
assertEquals(1, getStubResolver(1).getResolvedParameters().size()); |
||||||
|
assertEquals("null-null", value); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void cannotResolveArg() { |
||||||
|
try { |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); |
||||||
|
invokeAndBlock(new Handler(), method); |
||||||
|
fail("Expected exception"); |
||||||
|
} |
||||||
|
catch (MethodArgumentResolutionException ex) { |
||||||
|
assertNotNull(ex.getMessage()); |
||||||
|
assertTrue(ex.getMessage().contains("Could not resolve parameter [0]")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void resolveProvidedArg() { |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); |
||||||
|
Object value = invokeAndBlock(new Handler(), method, 99, "value"); |
||||||
|
|
||||||
|
assertNotNull(value); |
||||||
|
assertEquals(String.class, value.getClass()); |
||||||
|
assertEquals("99-value", value); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void resolveProvidedArgFirst() { |
||||||
|
this.resolvers.add(new StubArgumentResolver(1)); |
||||||
|
this.resolvers.add(new StubArgumentResolver("value1")); |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); |
||||||
|
Object value = invokeAndBlock(new Handler(), method, 2, "value2"); |
||||||
|
|
||||||
|
assertEquals("2-value2", value); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void exceptionInResolvingArg() { |
||||||
|
this.resolvers.add(new InvocableHandlerMethodTests.ExceptionRaisingArgumentResolver()); |
||||||
|
try { |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); |
||||||
|
invokeAndBlock(new Handler(), method); |
||||||
|
fail("Expected exception"); |
||||||
|
} |
||||||
|
catch (IllegalArgumentException ex) { |
||||||
|
// expected - allow HandlerMethodArgumentResolver exceptions to propagate
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void illegalArgumentException() { |
||||||
|
this.resolvers.add(new StubArgumentResolver(Integer.class, "__not_an_int__")); |
||||||
|
this.resolvers.add(new StubArgumentResolver("value")); |
||||||
|
try { |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); |
||||||
|
invokeAndBlock(new Handler(), method); |
||||||
|
fail("Expected exception"); |
||||||
|
} |
||||||
|
catch (IllegalStateException ex) { |
||||||
|
assertNotNull("Exception not wrapped", ex.getCause()); |
||||||
|
assertTrue(ex.getCause() instanceof IllegalArgumentException); |
||||||
|
assertTrue(ex.getMessage().contains("Endpoint [")); |
||||||
|
assertTrue(ex.getMessage().contains("Method [")); |
||||||
|
assertTrue(ex.getMessage().contains("with argument values:")); |
||||||
|
assertTrue(ex.getMessage().contains("[0] [type=java.lang.String] [value=__not_an_int__]")); |
||||||
|
assertTrue(ex.getMessage().contains("[1] [type=java.lang.String] [value=value")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void invocationTargetException() { |
||||||
|
Method method = ResolvableMethod.on(Handler.class).argTypes(Throwable.class).resolveMethod(); |
||||||
|
|
||||||
|
Throwable expected = new Throwable("error"); |
||||||
|
Mono<Object> result = invoke(new Handler(), method, expected); |
||||||
|
StepVerifier.create(result).expectErrorSatisfies(actual -> assertSame(expected, actual)).verify(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void voidMethod() { |
||||||
|
this.resolvers.add(new StubArgumentResolver(double.class, 5.25)); |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0.0d)).method(); |
||||||
|
Handler handler = new Handler(); |
||||||
|
Object value = invokeAndBlock(handler, method); |
||||||
|
|
||||||
|
assertNull(value); |
||||||
|
assertEquals(1, getStubResolver(0).getResolvedParameters().size()); |
||||||
|
assertEquals("5.25", handler.getResult()); |
||||||
|
assertEquals("amount", getStubResolver(0).getResolvedParameters().get(0).getParameterName()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void voidMonoMethod() { |
||||||
|
Method method = ResolvableMethod.on(Handler.class).mockCall(Handler::handleAsync).method(); |
||||||
|
Handler handler = new Handler(); |
||||||
|
Object value = invokeAndBlock(handler, method); |
||||||
|
|
||||||
|
assertNull(value); |
||||||
|
assertEquals("success", handler.getResult()); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Nullable |
||||||
|
private Object invokeAndBlock(Object handler, Method method, Object... providedArgs) { |
||||||
|
return invoke(handler, method, providedArgs).block(Duration.ofSeconds(5)); |
||||||
|
} |
||||||
|
|
||||||
|
private Mono<Object> invoke(Object handler, Method method, Object... providedArgs) { |
||||||
|
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); |
||||||
|
handlerMethod.setArgumentResolvers(this.resolvers); |
||||||
|
return handlerMethod.invoke(this.message, providedArgs); |
||||||
|
} |
||||||
|
|
||||||
|
private StubArgumentResolver getStubResolver(int index) { |
||||||
|
return (StubArgumentResolver) this.resolvers.get(index); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({"unused", "UnusedReturnValue", "SameParameterValue"}) |
||||||
|
private static class Handler { |
||||||
|
|
||||||
|
private AtomicReference<String> result = new AtomicReference<>(); |
||||||
|
|
||||||
|
|
||||||
|
public String getResult() { |
||||||
|
return this.result.get(); |
||||||
|
} |
||||||
|
|
||||||
|
String handle(Integer intArg, String stringArg) { |
||||||
|
return intArg + "-" + stringArg; |
||||||
|
} |
||||||
|
|
||||||
|
void handle(double amount) { |
||||||
|
this.result.set(String.valueOf(amount)); |
||||||
|
} |
||||||
|
|
||||||
|
void handleWithException(Throwable ex) throws Throwable { |
||||||
|
throw ex; |
||||||
|
} |
||||||
|
|
||||||
|
Mono<Void> handleAsync() { |
||||||
|
return Mono.delay(Duration.ofMillis(100)).thenEmpty(Mono.defer(() -> { |
||||||
|
this.result.set("success"); |
||||||
|
return Mono.empty(); |
||||||
|
})); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsParameter(MethodParameter parameter) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { |
||||||
|
return Mono.error(new IllegalArgumentException("oops, can't read")); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
/* |
||||||
|
* 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.List; |
||||||
|
|
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter; |
||||||
|
import org.springframework.lang.Nullable; |
||||||
|
import org.springframework.messaging.Message; |
||||||
|
|
||||||
|
/** |
||||||
|
* Stub resolver for a fixed value type and/or value. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class StubArgumentResolver implements HandlerMethodArgumentResolver { |
||||||
|
|
||||||
|
private final Class<?> valueType; |
||||||
|
|
||||||
|
@Nullable |
||||||
|
private final Object value; |
||||||
|
|
||||||
|
private List<MethodParameter> resolvedParameters = new ArrayList<>(); |
||||||
|
|
||||||
|
|
||||||
|
public StubArgumentResolver(Object value) { |
||||||
|
this(value.getClass(), value); |
||||||
|
} |
||||||
|
|
||||||
|
public StubArgumentResolver(Class<?> valueType) { |
||||||
|
this(valueType, null); |
||||||
|
} |
||||||
|
|
||||||
|
public StubArgumentResolver(Class<?> valueType, Object value) { |
||||||
|
this.valueType = valueType; |
||||||
|
this.value = value; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public List<MethodParameter> getResolvedParameters() { |
||||||
|
return resolvedParameters; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean supportsParameter(MethodParameter parameter) { |
||||||
|
return parameter.getParameterType().equals(this.valueType); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
@Override |
||||||
|
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) { |
||||||
|
this.resolvedParameters.add(parameter); |
||||||
|
return Mono.justOrEmpty(this.value); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue