HandlerMethodArgumentResolver is now asynchronous

This commit is contained in:
Rossen Stoyanchev 2015-10-30 16:04:49 -04:00
parent 6b73993a38
commit 0989c8b3c2
6 changed files with 110 additions and 48 deletions

View File

@ -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);
} }

View File

@ -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 {

View File

@ -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) {

View File

@ -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);
}
} }
} }

View File

@ -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));
} }
} }

View File

@ -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) {