HandlerMethodArgumentResolver is now asynchronous
This commit is contained in:
parent
6b73993a38
commit
0989c8b3c2
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package org.springframework.reactive.web.dispatch.method;
|
package org.springframework.reactive.web.dispatch.method;
|
||||||
|
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.http.server.ReactiveServerHttpRequest;
|
import org.springframework.http.server.ReactiveServerHttpRequest;
|
||||||
|
|
||||||
|
@ -27,6 +29,12 @@ public interface HandlerMethodArgumentResolver {
|
||||||
|
|
||||||
boolean supportsParameter(MethodParameter parameter);
|
boolean supportsParameter(MethodParameter parameter);
|
||||||
|
|
||||||
Object resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request);
|
/**
|
||||||
|
* The returned Publisher must produce a single value. As Reactive Streams
|
||||||
|
* does not allow publishing null values, if the value may be {@code null}
|
||||||
|
* use {@link java.util.Optional#ofNullable(Object)} to wrap it.
|
||||||
|
*/
|
||||||
|
Publisher<Object> resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,14 @@ package org.springframework.reactive.web.dispatch.method;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.Publishers;
|
||||||
|
import reactor.fn.tuple.Tuple;
|
||||||
|
import reactor.rx.Streams;
|
||||||
|
|
||||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||||
import org.springframework.core.GenericTypeResolver;
|
import org.springframework.core.GenericTypeResolver;
|
||||||
|
@ -32,9 +38,6 @@ import org.springframework.web.method.HandlerMethod;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 90% overlap with the existing one in spring-web except for the different
|
|
||||||
* HandlerMethodArgumentResolver contract.
|
|
||||||
*
|
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
*/
|
*/
|
||||||
public class InvocableHandlerMethod extends HandlerMethod {
|
public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
|
@ -55,53 +58,74 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Object invokeForRequest(ReactiveServerHttpRequest request, Object... providedArgs) throws Exception {
|
public Publisher<Object> invokeForRequest(ReactiveServerHttpRequest request,
|
||||||
Object[] args = getMethodArgumentValues(request, providedArgs);
|
Object... providedArgs) {
|
||||||
if (logger.isTraceEnabled()) {
|
|
||||||
logger.trace("Invoking [" + getBeanType().getSimpleName() + "." +
|
List<Publisher<Object>> argPublishers = getMethodArguments(request, providedArgs);
|
||||||
getMethod().getName() + "] method with arguments " + Arrays.asList(args));
|
|
||||||
}
|
Publisher<Object[]> argValues = (!argPublishers.isEmpty() ?
|
||||||
Object returnValue = doInvoke(args);
|
Streams.zip(argPublishers, this::unwrapOptionalArgValues) : Publishers.just(new Object[0]));
|
||||||
if (logger.isTraceEnabled()) {
|
|
||||||
logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
|
return Publishers.map(argValues, args -> {
|
||||||
}
|
if (logger.isTraceEnabled()) {
|
||||||
return returnValue;
|
logger.trace("Invoking [" + getBeanType().getSimpleName() + "." +
|
||||||
|
getMethod().getName() + "] method with arguments " +
|
||||||
|
Collections.singletonList(argPublishers));
|
||||||
|
}
|
||||||
|
Object returnValue = null;
|
||||||
|
try {
|
||||||
|
returnValue = doInvoke(args);
|
||||||
|
if (logger.isTraceEnabled()) {
|
||||||
|
logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
// TODO: how to best handle error inside map? (also wrapping hides original ex)
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
return returnValue;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object[] getMethodArgumentValues(ReactiveServerHttpRequest request, Object... providedArgs) throws Exception {
|
private List<Publisher<Object>> getMethodArguments(ReactiveServerHttpRequest request,
|
||||||
|
Object... providedArgs) {
|
||||||
|
|
||||||
MethodParameter[] parameters = getMethodParameters();
|
MethodParameter[] parameters = getMethodParameters();
|
||||||
Object[] args = new Object[parameters.length];
|
List<Publisher<Object>> valuePublishers = new ArrayList<>(parameters.length);
|
||||||
for (int i = 0; i < parameters.length; i++) {
|
for (int i = 0; i < parameters.length; i++) {
|
||||||
MethodParameter parameter = parameters[i];
|
MethodParameter parameter = parameters[i];
|
||||||
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
|
||||||
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
|
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
|
||||||
args[i] = resolveProvidedArgument(parameter, providedArgs);
|
Object value = resolveProvidedArgument(parameter, providedArgs);
|
||||||
if (args[i] != null) {
|
if (value != null) {
|
||||||
|
valuePublishers.add(Publishers.just(value));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
boolean resolved = false;
|
||||||
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
|
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
|
||||||
if (resolver.supportsParameter(parameter)) {
|
if (resolver.supportsParameter(parameter)) {
|
||||||
try {
|
try {
|
||||||
args[i] = resolver.resolveArgument(parameter, request);
|
valuePublishers.add(resolver.resolveArgument(parameter, request));
|
||||||
|
resolved = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
if (logger.isDebugEnabled()) {
|
String msg = buildArgErrorMessage("Error resolving argument", i);
|
||||||
logger.debug(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
|
valuePublishers.add(Publishers.error(new IllegalStateException(msg, ex)));
|
||||||
}
|
break;
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (args[i] == null) {
|
if (!resolved) {
|
||||||
String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
|
String msg = buildArgErrorMessage("No suitable resolver for argument", i);
|
||||||
throw new IllegalStateException(msg);
|
valuePublishers.add(Publishers.error(new IllegalStateException(msg)));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return args;
|
return valuePublishers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getArgumentResolutionErrorMessage(String message, int index) {
|
private String buildArgErrorMessage(String message, int index) {
|
||||||
MethodParameter param = getMethodParameters()[index];
|
MethodParameter param = getMethodParameters()[index];
|
||||||
message += " [" + index + "] [type=" + param.getParameterType().getName() + "]";
|
message += " [" + index + "] [type=" + param.getParameterType().getName() + "]";
|
||||||
return getDetailedErrorMessage(message);
|
return getDetailedErrorMessage(message);
|
||||||
|
@ -125,6 +149,27 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void unwrapOptionalArgValues(Object[] args) {
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] instanceof Optional) {
|
||||||
|
Optional optional = (Optional) args[i];
|
||||||
|
args[i] = optional.isPresent() ? optional.get() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object[] unwrapOptionalArgValues(Tuple tuple) {
|
||||||
|
Object[] args = new Object[tuple.size()];
|
||||||
|
for (int i = 0; i < tuple.size(); i++) {
|
||||||
|
args[i] = tuple.get(i);
|
||||||
|
if (args[i] instanceof Optional) {
|
||||||
|
Optional optional = (Optional) args[i];
|
||||||
|
args[i] = optional.isPresent() ? optional.get() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
protected Object doInvoke(Object... args) throws Exception {
|
protected Object doInvoke(Object... args) throws Exception {
|
||||||
ReflectionUtils.makeAccessible(getBridgedMethod());
|
ReflectionUtils.makeAccessible(getBridgedMethod());
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.Publishers;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
|
@ -66,9 +67,7 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
public Publisher<Object> resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) {
|
||||||
public Object resolveArgument(MethodParameter parameter, ReactiveServerHttpRequest request) {
|
|
||||||
|
|
||||||
MediaType mediaType = resolveMediaType(request);
|
MediaType mediaType = resolveMediaType(request);
|
||||||
ResolvableType type = ResolvableType.forMethodParameter(parameter);
|
ResolvableType type = ResolvableType.forMethodParameter(parameter);
|
||||||
List<Object> hints = new ArrayList<>();
|
List<Object> hints = new ArrayList<>();
|
||||||
|
@ -85,12 +84,10 @@ public class RequestBodyArgumentResolver implements HandlerMethodArgumentResolve
|
||||||
}
|
}
|
||||||
elementStream = deserializer.decode(inputStream, elementType, mediaType, hints.toArray());
|
elementStream = deserializer.decode(inputStream, elementType, mediaType, hints.toArray());
|
||||||
}
|
}
|
||||||
if (conversionService.canConvert(Publisher.class, type.getRawClass())) {
|
if (this.conversionService.canConvert(Publisher.class, type.getRawClass())) {
|
||||||
return conversionService.convert(elementStream, type.getRawClass());
|
return Publishers.just(this.conversionService.convert(elementStream, type.getRawClass()));
|
||||||
}
|
|
||||||
else {
|
|
||||||
return elementStream;
|
|
||||||
}
|
}
|
||||||
|
return Publishers.map(elementStream, element -> element);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaType resolveMediaType(ReactiveServerHttpRequest request) {
|
private MediaType resolveMediaType(ReactiveServerHttpRequest request) {
|
||||||
|
|
|
@ -79,14 +79,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Initializin
|
||||||
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler);
|
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod((HandlerMethod) handler);
|
||||||
handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
|
handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
|
||||||
|
|
||||||
try {
|
Publisher<Object> resultPublisher = handlerMethod.invokeForRequest(request);
|
||||||
Object result = handlerMethod.invokeForRequest(request);
|
return Publishers.map(resultPublisher, result -> new HandlerResult(handlerMethod, result));
|
||||||
return Publishers.just(new HandlerResult(handlerMethod, result));
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
// TODO: remove throws declaration from InvocableHandlerMethod
|
|
||||||
return Publishers.error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -17,6 +17,11 @@
|
||||||
package org.springframework.reactive.web.dispatch.method.annotation;
|
package org.springframework.reactive.web.dispatch.method.annotation;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.Publishers;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.http.server.ReactiveServerHttpRequest;
|
import org.springframework.http.server.ReactiveServerHttpRequest;
|
||||||
import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver;
|
import org.springframework.reactive.web.dispatch.method.HandlerMethodArgumentResolver;
|
||||||
|
@ -40,11 +45,12 @@ public class RequestParamArgumentResolver implements HandlerMethodArgumentResolv
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object resolveArgument(MethodParameter param, ReactiveServerHttpRequest request) {
|
public Publisher<Object> resolveArgument(MethodParameter param, ReactiveServerHttpRequest request) {
|
||||||
RequestParam annotation = param.getParameterAnnotation(RequestParam.class);
|
RequestParam annotation = param.getParameterAnnotation(RequestParam.class);
|
||||||
String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName());
|
String name = (annotation.value().length() != 0 ? annotation.value() : param.getParameterName());
|
||||||
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build();
|
UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()).build();
|
||||||
return uriComponents.getQueryParams().getFirst(name);
|
String value = uriComponents.getQueryParams().getFirst(name);
|
||||||
|
return Publishers.just(Optional.ofNullable(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,6 +180,11 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||||
capitalizeCollection("http://localhost:" + port + "/stream-capitalize");
|
capitalizeCollection("http://localhost:" + port + "/stream-capitalize");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void personCapitalize() throws Exception {
|
||||||
|
capitalizePojo("http://localhost:" + port + "/person-capitalize");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void completableFutureCapitalize() throws Exception {
|
public void completableFutureCapitalize() throws Exception {
|
||||||
capitalizePojo("http://localhost:" + port + "/completable-future-capitalize");
|
capitalizePojo("http://localhost:" + port + "/completable-future-capitalize");
|
||||||
|
@ -376,6 +381,13 @@ public class RequestMappingIntegrationTests extends AbstractHttpHandlerIntegrati
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequestMapping("/person-capitalize")
|
||||||
|
@ResponseBody
|
||||||
|
public Person personCapitalize(@RequestBody Person person) {
|
||||||
|
person.setName(person.getName().toUpperCase());
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
@RequestMapping("/completable-future-capitalize")
|
@RequestMapping("/completable-future-capitalize")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public CompletableFuture<Person> completableFutureCapitalize(@RequestBody CompletableFuture<Person> personFuture) {
|
public CompletableFuture<Person> completableFutureCapitalize(@RequestBody CompletableFuture<Person> personFuture) {
|
||||||
|
|
Loading…
Reference in New Issue