Add RequestBody resolver and minor improvements

Support for RequestBody arguments.
List supported arguments on HttpExchange.
Improve null handling.

See gh-28386
This commit is contained in:
rstoyanchev
2022-05-03 12:41:52 +01:00
parent 7797453f28
commit 2d2726b8f7
10 changed files with 330 additions and 26 deletions

View File

@@ -24,6 +24,7 @@ import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.service.invoker.UrlArgumentResolver;
/**
@@ -49,16 +50,52 @@ import org.springframework.web.bind.annotation.Mapping;
* <tr>
* <th>Method Argument</th>
* <th>Description</th>
* <th>Resolver</th>
* </tr>
* <tr>
* <td>{@link org.springframework.http.HttpMethod}</td>
* <td>Set the HTTP method for the request, overriding the annotation
* {@link #method()} attribute value</td>
* <td>{@link java.net.URI URI}</td>
* <td>Dynamically set the URL for the request, overriding the annotation
* {@link #url()} attribute</td>
* <td>{@link UrlArgumentResolver
* HttpUrlArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.http.HttpMethod HttpMethod}</td>
* <td>Dynamically set the HTTP method for the request, overriding the annotation
* {@link #method()} attribute</td>
* <td>{@link org.springframework.web.service.invoker.HttpMethodArgumentResolver
* HttpMethodArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.web.bind.annotation.RequestHeader @RequestHeader}</td>
* <td>Add a request header</td>
* <td>{@link org.springframework.web.service.invoker.RequestHeaderArgumentResolver
* RequestHeaderArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.web.bind.annotation.PathVariable @PathVariable}</td>
* <td>Provide a path variable to expand the URI template with. This may be an
* individual value or a Map of values.</td>
* <td>Add a path variable for the URI template</td>
* <td>{@link org.springframework.web.service.invoker.PathVariableArgumentResolver
* PathVariableArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.web.bind.annotation.RequestBody @RequestBody}</td>
* <td>Set the body of the request</td>
* <td>{@link org.springframework.web.service.invoker.RequestBodyArgumentResolver
* RequestBodyArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.web.bind.annotation.RequestParam @RequestParam}</td>
* <td>Add a request parameter, either form data if {@code "Content-Type"} is
* {@code "application/x-www-form-urlencoded"} or query params otherwise</td>
* <td>{@link org.springframework.web.service.invoker.RequestParamArgumentResolver
* RequestParamArgumentResolver}</td>
* </tr>
* <tr>
* <td>{@link org.springframework.web.bind.annotation.CookieValue @CookieValue}</td>
* <td>Add a cookie</td>
* <td>{@link org.springframework.web.service.invoker.CookieValueArgumentResolver
* CookieValueArgumentResolver}</td>
* </tr>
* </table>
*

View File

@@ -40,15 +40,19 @@ public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver {
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
if (argument instanceof HttpMethod httpMethod) {
if (!parameter.getParameterType().equals(HttpMethod.class)) {
return false;
}
if (argument != null) {
HttpMethod httpMethod = (HttpMethod) argument;
requestValues.setHttpMethod(httpMethod);
if (logger.isTraceEnabled()) {
logger.trace("Resolved HTTP method to: " + httpMethod.name());
}
requestValues.setHttpMethod(httpMethod);
return true;
}
return false;
return true;
}
}

View File

@@ -343,7 +343,7 @@ public final class HttpRequestValues {
* <p>This is mutually exclusive with, and resets any previously set
* {@link #setBodyValue(Object) body value}.
*/
public <T, P extends Publisher<T>> void setBody(Publisher<P> body, ParameterizedTypeReference<?> elementTye) {
public <T, P extends Publisher<T>> void setBody(P body, ParameterizedTypeReference<T> elementTye) {
this.body = body;
this.bodyElementType = elementTye;
this.bodyValue = null;

View File

@@ -110,14 +110,22 @@ final class HttpServiceMethod {
Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch");
for (int i = 0; i < arguments.length; i++) {
Object value = arguments[i];
boolean resolved = false;
for (HttpServiceArgumentResolver resolver : this.argumentResolvers) {
if (resolver.resolve(value, this.parameters[i], requestValues)) {
resolved = true;
break;
}
}
Assert.state(resolved, formatArgumentError(this.parameters[i], "No suitable resolver"));
}
}
private static String formatArgumentError(MethodParameter param, String message) {
return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
}
/**
* Factory for an {@link HttpRequestValues} with values extracted from

View File

@@ -188,10 +188,11 @@ public final class HttpServiceProxyFactory {
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers);
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry));
resolvers.add(new PathVariableArgumentResolver(conversionService));
resolvers.add(new CookieValueArgumentResolver(conversionService));
resolvers.add(new RequestParamArgumentResolver(conversionService));
resolvers.add(new HttpUrlArgumentResolver());
resolvers.add(new CookieValueArgumentResolver(conversionService));
resolvers.add(new UrlArgumentResolver());
resolvers.add(new HttpMethodArgumentResolver());
return resolvers;
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2002-2022 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
*
* https://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.web.service.invoker;
import org.reactivestreams.Publisher;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestBody;
/**
* {@link HttpServiceArgumentResolver} for {@link RequestBody @RequestBody}
* annotated arguments.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver {
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
public RequestBodyArgumentResolver(ReactiveAdapterRegistry reactiveAdapterRegistry) {
Assert.notNull(reactiveAdapterRegistry, "ReactiveAdapterRegistry is required");
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
}
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
RequestBody annot = parameter.getParameterAnnotation(RequestBody.class);
if (annot == null) {
return false;
}
if (argument != null) {
ReactiveAdapter reactiveAdapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
if (reactiveAdapter != null) {
setBody(argument, parameter, reactiveAdapter, requestValues);
}
else {
requestValues.setBodyValue(argument);
}
}
return true;
}
private <E> void setBody(
Object argument, MethodParameter parameter, ReactiveAdapter reactiveAdapter,
HttpRequestValues.Builder requestValues) {
String message = "Async type for @RequestBody should produce value(s)";
Assert.isTrue(!reactiveAdapter.isNoValue(), message);
parameter = parameter.nested();
Class<?> elementClass = parameter.getNestedParameterType();
Assert.isTrue(elementClass != Void.class, message);
ParameterizedTypeReference<E> typeRef = ParameterizedTypeReference.forType(parameter.getNestedGenericParameterType());
Publisher<E> publisher = reactiveAdapter.toPublisher(argument);
requestValues.setBody(publisher, typeRef);
}
}

View File

@@ -19,29 +19,32 @@ package org.springframework.web.service.invoker;
import java.net.URI;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
/**
* {@link HttpServiceArgumentResolver} that resolves the target
* request's URL from an {@link HttpMethod} argument.
* {@link HttpServiceArgumentResolver} that resolves the URL for the request
* from an {@link URI} argument.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class HttpUrlArgumentResolver implements HttpServiceArgumentResolver {
public class UrlArgumentResolver implements HttpServiceArgumentResolver {
@Override
public boolean resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
if (argument instanceof URI uri) {
requestValues.setUri(uri);
if (!parameter.getParameterType().equals(URI.class)) {
return false;
}
if (argument != null) {
requestValues.setUri((URI) argument);
return true;
}
return false;
return true;
}
}