Browse Source

Add RequestBody resolver and minor improvements

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

See gh-28386
pull/28411/head
rstoyanchev 4 years ago
parent
commit
2d2726b8f7
  1. 47
      spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java
  2. 12
      spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java
  3. 2
      spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java
  4. 8
      spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java
  5. 5
      spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java
  6. 86
      spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java
  7. 17
      spring-web/src/main/java/org/springframework/web/service/invoker/UrlArgumentResolver.java
  8. 15
      spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java
  9. 141
      spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java
  10. 23
      spring-web/src/test/java/org/springframework/web/service/invoker/UrlArgumentResolverTests.java

47
spring-web/src/main/java/org/springframework/web/service/annotation/HttpExchange.java

@ -24,6 +24,7 @@ import java.lang.annotation.Target; @@ -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; @@ -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>
*

12
spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java

@ -40,15 +40,19 @@ public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver { @@ -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;
}
}

2
spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java

@ -343,7 +343,7 @@ public final class HttpRequestValues { @@ -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;

8
spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java

@ -110,14 +110,22 @@ final class HttpServiceMethod { @@ -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

5
spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java

@ -188,10 +188,11 @@ public final class HttpServiceProxyFactory { @@ -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;
}

86
spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java

@ -0,0 +1,86 @@ @@ -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);
}
}

17
spring-web/src/main/java/org/springframework/web/service/invoker/HttpUrlArgumentResolver.java → spring-web/src/main/java/org/springframework/web/service/invoker/UrlArgumentResolver.java

@ -19,29 +19,32 @@ package org.springframework.web.service.invoker; @@ -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;
}
}

15
spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java

@ -22,6 +22,7 @@ import org.springframework.http.HttpMethod; @@ -22,6 +22,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.web.service.annotation.GetExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
@ -44,14 +45,18 @@ public class HttpMethodArgumentResolverTests { @@ -44,14 +45,18 @@ public class HttpMethodArgumentResolverTests {
}
@Test
void ignoreOtherArgumentTypes() {
this.service.execute("test");
assertThat(getActualMethod()).isEqualTo(HttpMethod.GET);
void notHttpMethod() {
assertThatIllegalStateException()
.isThrownBy(() -> this.service.executeNotHttpMethod("test"))
.withMessage("Could not resolve parameter [0] in " +
"public abstract void org.springframework.web.service.invoker." +
"HttpMethodArgumentResolverTests$Service.executeNotHttpMethod(java.lang.String): " +
"No suitable resolver");
}
@Test
void ignoreNull() {
this.service.execute((HttpMethod) null);
this.service.execute(null);
assertThat(getActualMethod()).isEqualTo(HttpMethod.GET);
}
@ -66,7 +71,7 @@ public class HttpMethodArgumentResolverTests { @@ -66,7 +71,7 @@ public class HttpMethodArgumentResolverTests {
void execute(HttpMethod method);
@GetExchange
void execute(String test);
void executeNotHttpMethod(String test);
}

141
spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java

@ -0,0 +1,141 @@ @@ -0,0 +1,141 @@
/*
* 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 io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Single;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Unit tests for {@link RequestBodyArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class RequestBodyArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
@Test
void stringBody() {
String body = "bodyValue";
this.service.execute(body);
assertThat(getRequestValues().getBodyValue()).isEqualTo(body);
assertThat(getRequestValues().getBody()).isNull();
}
@Test
void monoBody() {
Mono<String> bodyMono = Mono.just("bodyValue");
this.service.executeMono(bodyMono);
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBody()).isSameAs(bodyMono);
assertThat(getRequestValues().getBodyElementType()).isEqualTo(new ParameterizedTypeReference<String>() {});
}
@Test
void singleBody() {
String bodyValue = "bodyValue";
this.service.executeSingle(Single.just(bodyValue));
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBodyElementType()).isEqualTo(new ParameterizedTypeReference<String>() {});
Publisher<?> body = getRequestValues().getBody();
assertThat(body).isNotNull();
assertThat(((Mono<String>) body).block()).isEqualTo(bodyValue);
}
@Test
void monoVoid() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.service.executeMonoVoid(Mono.empty()))
.withMessage("Async type for @RequestBody should produce value(s)");
}
@Test
void completable() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.service.executeCompletable(Completable.complete()))
.withMessage("Async type for @RequestBody should produce value(s)");
}
@Test
void notRequestBody() {
assertThatIllegalStateException()
.isThrownBy(() -> this.service.executeNotRequestBody("value"))
.withMessage("Could not resolve parameter [0] in " +
"public abstract void org.springframework.web.service.invoker." +
"RequestBodyArgumentResolverTests$Service.executeNotRequestBody(java.lang.String): " +
"No suitable resolver");
}
@Test
void ignoreNull() {
this.service.execute(null);
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBody()).isNull();
this.service.executeMono(null);
assertThat(getRequestValues().getBodyValue()).isNull();
assertThat(getRequestValues().getBody()).isNull();
}
private HttpRequestValues getRequestValues() {
return this.client.getRequestValues();
}
private interface Service {
@GetExchange
void execute(@RequestBody String body);
@GetExchange
void executeMono(@RequestBody Mono<String> body);
@GetExchange
void executeSingle(@RequestBody Single<String> body);
@GetExchange
void executeMonoVoid(@RequestBody Mono<Void> body);
@GetExchange
void executeCompletable(@RequestBody Completable body);
@GetExchange
void executeNotRequestBody(String body);
}
}

23
spring-web/src/test/java/org/springframework/web/service/invoker/HttpUrlArgumentResolverTests.java → spring-web/src/test/java/org/springframework/web/service/invoker/UrlArgumentResolverTests.java

@ -23,14 +23,15 @@ import org.junit.jupiter.api.Test; @@ -23,14 +23,15 @@ import org.junit.jupiter.api.Test;
import org.springframework.web.service.annotation.GetExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Unit tests for {@link HttpUrlArgumentResolver}.
* Unit tests for {@link UrlArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class HttpUrlArgumentResolverTests {
public class UrlArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
@ -46,6 +47,22 @@ public class HttpUrlArgumentResolverTests { @@ -46,6 +47,22 @@ public class HttpUrlArgumentResolverTests {
assertThat(getRequestValues().getUriTemplate()).isNull();
}
@Test
void notUrl() {
assertThatIllegalStateException()
.isThrownBy(() -> this.service.executeNotUri("test"))
.withMessage("Could not resolve parameter [0] in " +
"public abstract void org.springframework.web.service.invoker." +
"UrlArgumentResolverTests$Service.executeNotUri(java.lang.String): " +
"No suitable resolver");
}
@Test
void ignoreNull() {
this.service.execute(null);
assertThat(getRequestValues().getUri()).isNull();
}
private HttpRequestValues getRequestValues() {
return this.client.getRequestValues();
}
@ -56,6 +73,8 @@ public class HttpUrlArgumentResolverTests { @@ -56,6 +73,8 @@ public class HttpUrlArgumentResolverTests {
@GetExchange("/path")
void execute(URI uri);
@GetExchange
void executeNotUri(String other);
}
}
Loading…
Cancel
Save