Refactor HttpRequestSpec to HttpRequestValues

HttpRequestValues is immutable and exposes a builder.

See gh-28386
This commit is contained in:
rstoyanchev 2022-04-27 09:25:23 +01:00
parent 564f8ba7a0
commit d7ab5b4132
12 changed files with 484 additions and 348 deletions

View File

@ -33,18 +33,18 @@ import org.springframework.http.ResponseEntity;
*/
public interface HttpClientAdapter {
Mono<Void> requestToVoid(HttpRequestSpec spec);
Mono<Void> requestToVoid(HttpRequestValues requestValues);
Mono<HttpHeaders> requestToHeaders(HttpRequestSpec spec);
Mono<HttpHeaders> requestToHeaders(HttpRequestValues requestValues);
<T> Mono<T> requestToBody(HttpRequestSpec spec, ParameterizedTypeReference<T> bodyType);
<T> Mono<T> requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
<T> Flux<T> requestToBodyFlux(HttpRequestSpec spec, ParameterizedTypeReference<T> bodyType);
<T> Flux<T> requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestSpec spec);
Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestValues requestValues);
<T> Mono<ResponseEntity<T>> requestToEntity(HttpRequestSpec spec, ParameterizedTypeReference<T> bodyType);
<T> Mono<ResponseEntity<T>> requestToEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
<T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestSpec spec, ParameterizedTypeReference<T> bodyType);
<T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
}

View File

@ -38,7 +38,7 @@ public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver {
@Override
public void resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestSpec requestSpec) {
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
if (argument == null) {
return;
@ -47,7 +47,7 @@ public class HttpMethodArgumentResolver implements HttpServiceArgumentResolver {
if (logger.isTraceEnabled()) {
logger.trace("Resolved HTTP method to: " + httpMethod.name());
}
requestSpec.setHttpMethod(httpMethod);
requestValues.setHttpMethod(httpMethod);
}
}

View File

@ -1,196 +0,0 @@
/*
* 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 java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.reactivestreams.Publisher;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Container for HTTP request values extracted from an
* {@link org.springframework.web.service.annotation.HttpExchange @HttpExchange}-annotated
* method and argument values passed to it. This is then given to
* {@link HttpClientAdapter} to adapt to the underlying HTTP client.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public class HttpRequestSpec {
private static final MultiValueMap<String, String> EMPTY_COOKIES_MAP =
CollectionUtils.toMultiValueMap(Collections.emptyMap());
@Nullable
private URI uri;
@Nullable
private String uriTemplate;
@Nullable
private Map<String, String> uriVariables;
@Nullable
private List<String> uriVariablesList;
@Nullable
private HttpMethod httpMethod;
@Nullable
private HttpHeaders headers;
@Nullable
private MultiValueMap<String, String> cookies;
@Nullable
private Object bodyValue;
@Nullable
private Publisher<?> bodyPublisher;
@Nullable
private ParameterizedTypeReference<?> bodyPublisherElementType;
private boolean complete;
public HttpRequestSpec() {
}
public void setUri(URI uri) {
checkComplete();
this.uri = uri;
}
@Nullable
public URI getUri() {
return this.uri;
}
public void setUriTemplate(String uriTemplate) {
checkComplete();
this.uriTemplate = uriTemplate;
}
@Nullable
public String getUriTemplate() {
return this.uriTemplate;
}
public Map<String, String> getUriVariables() {
this.uriVariables = (this.uriVariables != null ? this.uriVariables : new LinkedHashMap<>());
return this.uriVariables;
}
public List<String> getUriVariableValues() {
this.uriVariablesList = (this.uriVariablesList != null ? this.uriVariablesList : new ArrayList<>());
return this.uriVariablesList;
}
public void setHttpMethod(HttpMethod httpMethod) {
checkComplete();
this.httpMethod = httpMethod;
}
@Nullable
public HttpMethod getHttpMethod() {
return this.httpMethod;
}
public HttpMethod getHttpMethodRequired() {
Assert.notNull(this.httpMethod, "No HttpMethod");
return this.httpMethod;
}
public HttpHeaders getHeaders() {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
return this.headers;
}
public MultiValueMap<String, String> getCookies() {
this.cookies = (this.cookies != null ? this.cookies : new LinkedMultiValueMap<>());
return this.cookies;
}
public void setBodyValue(Object bodyValue) {
checkComplete();
this.bodyValue = bodyValue;
}
@Nullable
public Object getBodyValue() {
return this.bodyValue;
}
public <T, P extends Publisher<T>> void setBodyPublisher(Publisher<P> bodyPublisher, MethodParameter parameter) {
checkComplete();
// Adapt to Mono/Flux and nest MethodParameter for element type
this.bodyPublisher = bodyPublisher;
this.bodyPublisherElementType = ParameterizedTypeReference.forType(parameter.nested().getGenericParameterType());
}
@Nullable
public Publisher<?> getBodyPublisher() {
return this.bodyPublisher;
}
public ParameterizedTypeReference<?> getBodyPublisherElementType() {
Assert.state(this.bodyPublisherElementType != null, "No body Publisher");
return this.bodyPublisherElementType;
}
private void checkComplete() {
Assert.isTrue(!this.complete, "setComplete already called");
}
void setComplete() {
this.complete = true;
this.uriVariables = (this.uriVariables != null ?
Collections.unmodifiableMap(this.uriVariables) : Collections.emptyMap());
this.uriVariablesList = (this.uriVariablesList != null ?
Collections.unmodifiableList(this.uriVariablesList) : Collections.emptyList());
this.headers = (this.headers != null ?
HttpHeaders.readOnlyHttpHeaders(this.headers) : HttpHeaders.EMPTY);
this.cookies = (this.cookies != null ?
CollectionUtils.unmodifiableMultiValueMap(this.cookies) : EMPTY_COOKIES_MAP);
}
}

View File

@ -0,0 +1,337 @@
/*
* 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 java.net.URI;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.reactivestreams.Publisher;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Container for HTTP request values extracted from an
* {@link org.springframework.web.service.annotation.HttpExchange @HttpExchange}-annotated
* method and argument values passed to it. This is then given to
* {@link HttpClientAdapter} to adapt to the underlying HTTP client.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public final class HttpRequestValues {
private static final MultiValueMap<String, String> EMPTY_COOKIES_MAP =
CollectionUtils.toMultiValueMap(Collections.emptyMap());
private final HttpMethod httpMethod;
@Nullable
private final URI uri;
@Nullable
private final String uriTemplate;
private final Map<String, String> uriVariables;
private final HttpHeaders headers;
private final MultiValueMap<String, String> cookies;
@Nullable
private final Object bodyValue;
@Nullable
private final Publisher<?> body;
@Nullable
private final ParameterizedTypeReference<?> bodyElementType;
private HttpRequestValues(HttpMethod httpMethod, @Nullable URI uri,
@Nullable String uriTemplate, @Nullable Map<String, String> uriVariables,
@Nullable HttpHeaders headers, @Nullable MultiValueMap<String, String> cookies,
@Nullable Object bodyValue,
@Nullable Publisher<?> body,
@Nullable ParameterizedTypeReference<?> bodyElementType) {
Assert.isTrue(uri == null || uriTemplate == null, "Expected either URI or URI template, not both");
this.httpMethod = httpMethod;
this.uri = uri;
this.uriTemplate = (uri != null || uriTemplate != null ? uriTemplate : "");
this.uriVariables = (uriVariables != null ? uriVariables : Collections.emptyMap());
this.headers = (headers != null ? headers : HttpHeaders.EMPTY);
this.cookies = (cookies != null ? cookies : EMPTY_COOKIES_MAP);
this.bodyValue = bodyValue;
this.body = body;
this.bodyElementType = bodyElementType;
}
/**
* Return the HTTP method to use for the request.
*/
public HttpMethod getHttpMethod() {
return this.httpMethod;
}
/**
* Return the full URL to use, if set.
* <p>This is mutually exclusive with {@link #getUriTemplate() uriTemplate}.
* One of the two has a value but not both.
*/
@Nullable
public URI getUri() {
return this.uri;
}
/**
* Return the URL template for the request, if set.
* <p>This is mutually exclusive with a {@link #getUri() full URL}.
* One of the two has a value but not both.
*/
@Nullable
public String getUriTemplate() {
return this.uriTemplate;
}
/**
* Return the URL template variables, or an empty map.
*/
public Map<String, String> getUriVariables() {
return this.uriVariables;
}
/**
* Return the headers for the request, if any.
*/
public HttpHeaders getHeaders() {
return this.headers;
}
/**
* Return the cookies for the request, if any.
*/
public MultiValueMap<String, String> getCookies() {
return this.cookies;
}
/**
* Return the request body as a value to be serialized, if set.
* <p>This is mutually exclusive with {@link #getBody()}.
* Only one of the two or neither is set.
*/
@Nullable
public Object getBodyValue() {
return this.bodyValue;
}
/**
* Return the request body as a Publisher.
* <p>This is mutually exclusive with {@link #getBodyValue()}.
* Only one of the two or neither is set.
*/
@Nullable
public Publisher<?> getBody() {
return this.body;
}
/**
* Return the element type for a {@link #getBody() Publisher body}.
*/
@Nullable
public ParameterizedTypeReference<?> getBodyElementType() {
return this.bodyElementType;
}
public static Builder builder(HttpMethod httpMethod) {
return new Builder(httpMethod);
}
/**
* Builder for {@link HttpRequestValues}.
*/
public final static class Builder {
private HttpMethod httpMethod;
@Nullable
private URI uri;
@Nullable
private String uriTemplate;
@Nullable
private Map<String, String> uriVariables;
@Nullable
private HttpHeaders headers;
@Nullable
private MultiValueMap<String, String> cookies;
@Nullable
private Object bodyValue;
@Nullable
private Publisher<?> body;
@Nullable
private ParameterizedTypeReference<?> bodyElementType;
private Builder(HttpMethod httpMethod) {
Assert.notNull(httpMethod, "HttpMethod is required");
this.httpMethod = httpMethod;
}
/**
* Set the HTTP method for the request.
*/
public Builder setHttpMethod(HttpMethod httpMethod) {
Assert.notNull(httpMethod, "HttpMethod is required");
this.httpMethod = httpMethod;
return this;
}
/**
* Set the request URL as a full URL.
* <p>This is mutually exclusive with, and resets any previously set
* {@link #setUriTemplate(String)}.
*/
public Builder setUri(URI uri) {
this.uri = uri;
this.uriTemplate = null;
return this;
}
/**
* Set the request URL as a String template.
* <p>This is mutually exclusive with, and resets any previously set
* {@link #setUri(URI) full URI}.
*/
public Builder setUriTemplate(String uriTemplate) {
this.uriTemplate = uriTemplate;
this.uri = null;
return this;
}
/**
* Add a URI variable name-value pair.
* <p>This is mutually exclusive with, and resets any previously set
* {@link #setUri(URI) full URI}.
*/
public Builder setUriVariable(String name, String value) {
this.uriVariables = (this.uriVariables != null ? this.uriVariables : new LinkedHashMap<>());
this.uriVariables.put(name, value);
this.uri = null;
return this;
}
/**
* Set the media types for the request {@code Accept} header.
*/
public Builder setAccept(List<MediaType> acceptableMediaTypes) {
initHeaders().setAccept(acceptableMediaTypes);
return this;
}
/**
* Set the media type for the request {@code Content-Type} header.
*/
public Builder setContentType(MediaType contentType) {
initHeaders().setContentType(contentType);
return this;
}
/**
* Add the given header name and values.
*/
public Builder addHeader(String headerName, String... headerValues) {
for (String headerValue : headerValues) {
initHeaders().add(headerName, headerValue);
}
return this;
}
private HttpHeaders initHeaders() {
this.headers = (this.headers != null ? this.headers : new HttpHeaders());
return this.headers;
}
/**
* Add the given cookie name and values.
*/
public Builder addCookie(String name, String... values) {
this.cookies = (this.cookies != null ? this.cookies : new LinkedMultiValueMap<>());
for (String value : values) {
this.cookies.add(name, value);
}
return this;
}
/**
* Set the request body as a concrete value to be serialized.
* <p>This is mutually exclusive with, and resets any previously set
* {@link #setBody(Publisher, ParameterizedTypeReference) body Publisher}.
*/
public void setBodyValue(Object bodyValue) {
this.bodyValue = bodyValue;
this.body = null;
this.bodyElementType = null;
}
/**
* Set the request body as a concrete value to be serialized.
* <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) {
this.body = body;
this.bodyElementType = elementTye;
this.bodyValue = null;
}
/**
* Builder the {@link HttpRequestValues} instance.
*/
public HttpRequestValues build() {
return new HttpRequestValues(
this.httpMethod, this.uri, this.uriTemplate, this.uriVariables,
this.headers, this.cookies,
this.bodyValue, this.body, this.bodyElementType);
}
}
}

View File

@ -34,8 +34,8 @@ public interface HttpServiceArgumentResolver {
* Resolve the argument value.
* @param argument the argument value
* @param parameter the method parameter for the argument
* @param requestSpec container to add HTTP request values to
* @param requestValues builder to add HTTP request values to
*/
void resolve(@Nullable Object argument, MethodParameter parameter, HttpRequestSpec requestSpec);
void resolve(@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues);
}

View File

@ -62,7 +62,7 @@ final class HttpServiceMethod {
private final List<HttpServiceArgumentResolver> argumentResolvers;
private final HttpRequestSpecFactory requestSpecFactory;
private final HttpRequestValuesInitializer requestValuesInitializer;
private final ResponseFunction responseFunction;
@ -75,7 +75,7 @@ final class HttpServiceMethod {
this.method = method;
this.parameters = initMethodParameters(method);
this.argumentResolvers = argumentResolvers;
this.requestSpecFactory = HttpRequestSpecFactory.create(method, containingClass);
this.requestValuesInitializer = HttpRequestValuesInitializer.create(method, containingClass);
this.responseFunction = ResponseFunction.create(client, method, reactiveRegistry, blockTimeout);
}
@ -96,35 +96,34 @@ final class HttpServiceMethod {
@Nullable
public Object invoke(Object[] arguments) {
HttpRequestSpec requestSpec = this.requestSpecFactory.initializeRequestSpec();
applyArguments(requestSpec, arguments);
requestSpec.setComplete();
return this.responseFunction.execute(requestSpec);
HttpRequestValues.Builder requestValues = this.requestValuesInitializer.initializeRequestValuesBuilder();
applyArguments(requestValues, arguments);
return this.responseFunction.execute(requestValues.build());
}
private void applyArguments(HttpRequestSpec requestSpec, Object[] arguments) {
private void applyArguments(HttpRequestValues.Builder requestValues, Object[] arguments) {
Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch");
for (int i = 0; i < this.parameters.length; i++) {
Object argumentValue = arguments[i];
ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
this.parameters[i].initParameterNameDiscovery(nameDiscoverer);
for (HttpServiceArgumentResolver resolver : this.argumentResolvers) {
resolver.resolve(argumentValue, this.parameters[i], requestSpec);
resolver.resolve(argumentValue, this.parameters[i], requestValues);
}
}
}
/**
* Factory for an {@link HttpRequestSpec} with values extracted from
* Factory for an {@link HttpRequestValues} with values extracted from
* the type and method-level {@link HttpExchange @HttpRequest} annotations.
*/
private record HttpRequestSpecFactory(
@Nullable HttpMethod httpMethod, @Nullable String url,
private record HttpRequestValuesInitializer(
HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes) {
private HttpRequestSpecFactory(
@Nullable HttpMethod httpMethod, @Nullable String url,
private HttpRequestValuesInitializer(
HttpMethod httpMethod, @Nullable String url,
@Nullable MediaType contentType, @Nullable List<MediaType> acceptMediaTypes) {
this.url = url;
@ -133,28 +132,25 @@ final class HttpServiceMethod {
this.acceptMediaTypes = acceptMediaTypes;
}
public HttpRequestSpec initializeRequestSpec() {
HttpRequestSpec requestSpec = new HttpRequestSpec();
if (this.httpMethod != null) {
requestSpec.setHttpMethod(this.httpMethod);
}
public HttpRequestValues.Builder initializeRequestValuesBuilder() {
HttpRequestValues.Builder requestValues = HttpRequestValues.builder(this.httpMethod);
if (this.url != null) {
requestSpec.setUriTemplate(this.url);
requestValues.setUriTemplate(this.url);
}
if (this.contentType != null) {
requestSpec.getHeaders().setContentType(this.contentType);
requestValues.setContentType(this.contentType);
}
if (this.acceptMediaTypes != null) {
requestSpec.getHeaders().setAccept(this.acceptMediaTypes);
requestValues.setAccept(this.acceptMediaTypes);
}
return requestSpec;
return requestValues;
}
/**
* Introspect the method and create the request factory for it.
*/
public static HttpRequestSpecFactory create(Method method, Class<?> containingClass) {
public static HttpRequestValuesInitializer create(Method method, Class<?> containingClass) {
HttpExchange annot1 = AnnotatedElementUtils.findMergedAnnotation(containingClass, HttpExchange.class);
HttpExchange annot2 = AnnotatedElementUtils.findMergedAnnotation(method, HttpExchange.class);
@ -166,11 +162,10 @@ final class HttpServiceMethod {
MediaType contentType = initContentType(annot1, annot2);
List<MediaType> acceptableMediaTypes = initAccept(annot1, annot2);
return new HttpRequestSpecFactory(httpMethod, url, contentType, acceptableMediaTypes);
return new HttpRequestValuesInitializer(httpMethod, url, contentType, acceptableMediaTypes);
}
@Nullable
private static HttpMethod initHttpMethod(@Nullable HttpExchange typeAnnot, HttpExchange annot) {
String value1 = (typeAnnot != null ? typeAnnot.method() : null);
@ -184,7 +179,7 @@ final class HttpServiceMethod {
return HttpMethod.valueOf(value1);
}
return null;
throw new IllegalStateException("HttpMethod is required");
}
@Nullable
@ -249,12 +244,12 @@ final class HttpServiceMethod {
* return type blocking if necessary.
*/
private record ResponseFunction(
Function<HttpRequestSpec, Publisher<?>> responseFunction,
Function<HttpRequestValues, Publisher<?>> responseFunction,
@Nullable ReactiveAdapter returnTypeAdapter,
boolean blockForOptional, Duration blockTimeout) {
private ResponseFunction(
Function<HttpRequestSpec, Publisher<?>> responseFunction,
Function<HttpRequestValues, Publisher<?>> responseFunction,
@Nullable ReactiveAdapter returnTypeAdapter,
boolean blockForOptional, Duration blockTimeout) {
@ -265,9 +260,9 @@ final class HttpServiceMethod {
}
@Nullable
public Object execute(HttpRequestSpec requestSpec) {
public Object execute(HttpRequestValues requestValues) {
Publisher<?> responsePublisher = this.responseFunction.apply(requestSpec);
Publisher<?> responsePublisher = this.responseFunction.apply(requestValues);
if (this.returnTypeAdapter != null) {
return this.returnTypeAdapter.fromPublisher(responsePublisher);
@ -293,7 +288,7 @@ final class HttpServiceMethod {
MethodParameter actualParam = (reactiveAdapter != null ? returnParam.nested() : returnParam.nestedIfOptional());
Class<?> actualType = actualParam.getNestedParameterType();
Function<HttpRequestSpec, Publisher<?>> responseFunction;
Function<HttpRequestValues, Publisher<?>> responseFunction;
if (actualType.equals(void.class) || actualType.equals(Void.class)) {
responseFunction = client::requestToVoid;
}
@ -323,7 +318,7 @@ final class HttpServiceMethod {
}
@SuppressWarnings("ConstantConditions")
private static Function<HttpRequestSpec, Publisher<?>> initResponseEntityFunction(
private static Function<HttpRequestValues, Publisher<?>> initResponseEntityFunction(
HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) {
if (reactiveAdapter == null) {
@ -349,7 +344,7 @@ final class HttpServiceMethod {
});
}
private static Function<HttpRequestSpec, Publisher<?>> initBodyFunction(
private static Function<HttpRequestValues, Publisher<?>> initBodyFunction(
HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) {
ParameterizedTypeReference<?> bodyType =

View File

@ -56,7 +56,7 @@ public class PathVariableArgumentResolver implements HttpServiceArgumentResolver
@SuppressWarnings("unchecked")
@Override
public void resolve(
@Nullable Object argument, MethodParameter parameter, HttpRequestSpec requestSpec) {
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
PathVariable annotation = parameter.getParameterAnnotation(PathVariable.class);
if (annotation == null) {
@ -67,38 +67,38 @@ public class PathVariableArgumentResolver implements HttpServiceArgumentResolver
if (argument != null) {
Assert.isInstanceOf(Map.class, argument);
((Map<String, ?>) argument).forEach((key, value) ->
addUriParameter(key, value, annotation.required(), requestSpec));
addUriParameter(key, value, annotation.required(), requestValues));
}
}
else {
String name = StringUtils.hasText(annotation.value()) ? annotation.value() : annotation.name();
name = StringUtils.hasText(name) ? name : parameter.getParameterName();
Assert.notNull(name, "Failed to determine path variable name for parameter: " + parameter);
addUriParameter(name, argument, annotation.required(), requestSpec);
addUriParameter(name, argument, annotation.required(), requestValues);
}
}
private void addUriParameter(
String name, @Nullable Object value, boolean required, HttpRequestSpec requestSpec) {
String name, @Nullable Object value, boolean required, HttpRequestValues.Builder requestValues) {
if (value instanceof Optional) {
value = ((Optional<?>) value).orElse(null);
}
if (!(value instanceof String)) {
value = this.conversionService.convert(value, String.class);
}
if (value == null) {
Assert.isTrue(!required, "Missing required path variable '" + name + "'");
return;
}
if (!(value instanceof String)) {
value = this.conversionService.convert(value, String.class);
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved path variable '" + name + "' to " + value);
}
requestSpec.getUriVariables().put(name, (String) value);
requestValues.setUriVariable(name, (String) value);
}
}

View File

@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import static org.assertj.core.api.Assertions.assertThat;
@ -47,7 +46,7 @@ public class HttpMethodArgumentResolverTests {
@Test
void shouldIgnoreArgumentsNotMatchingType() {
this.service.execute("test");
assertThat(getActualMethod()).isNull();
assertThat(getActualMethod()).isEqualTo(HttpMethod.GET);
}
@Test
@ -59,30 +58,29 @@ public class HttpMethodArgumentResolverTests {
@Test
void shouldIgnoreNullValue() {
this.service.executeForNull(null);
assertThat(getActualMethod()).isNull();
assertThat(getActualMethod()).isEqualTo(HttpMethod.GET);
}
@Nullable
private HttpMethod getActualMethod() {
return this.clientAdapter.getRequestSpec().getHttpMethod();
return this.clientAdapter.getRequestValues().getHttpMethod();
}
private interface Service {
@HttpExchange
@GetExchange
void execute(HttpMethod method);
@GetExchange
void executeGet(HttpMethod method);
@HttpExchange
@GetExchange
void execute(String test);
@HttpExchange
@GetExchange
void execute(HttpMethod firstMethod, HttpMethod secondMethod);
@HttpExchange
@GetExchange
void executeForNull(@Nullable HttpMethod method);
}

View File

@ -141,45 +141,45 @@ public class HttpServiceMethodTests {
@Test
void methodAnnotatedService() {
MethodAnnotatedService service = this.clientAdapter.createService(MethodAnnotatedService.class);
MethodLevelAnnotatedService service = this.clientAdapter.createService(MethodLevelAnnotatedService.class);
service.performGet();
HttpRequestSpec request = this.clientAdapter.getRequestSpec();
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(request.getUriTemplate()).isNull();
assertThat(request.getHeaders().getContentType()).isNull();
assertThat(request.getHeaders().getAccept()).isEmpty();
HttpRequestValues requestValues = this.clientAdapter.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(requestValues.getUriTemplate()).isEqualTo("");
assertThat(requestValues.getHeaders().getContentType()).isNull();
assertThat(requestValues.getHeaders().getAccept()).isEmpty();
service.performPost();
request = this.clientAdapter.getRequestSpec();
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(request.getUriTemplate()).isEqualTo("/url");
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(request.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
requestValues = this.clientAdapter.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(requestValues.getUriTemplate()).isEqualTo("/url");
assertThat(requestValues.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(requestValues.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
}
@Test
void typeAndMethodAnnotatedService() {
MethodAnnotatedService service = this.clientAdapter.createService(TypeAndMethodAnnotatedService.class);
MethodLevelAnnotatedService service = this.clientAdapter.createService(TypeAndMethodLevelAnnotatedService.class);
service.performGet();
HttpRequestSpec request = this.clientAdapter.getRequestSpec();
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(request.getUriTemplate()).isEqualTo("/base");
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_CBOR);
assertThat(request.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_CBOR);
HttpRequestValues requestValues = this.clientAdapter.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(requestValues.getUriTemplate()).isEqualTo("/base");
assertThat(requestValues.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_CBOR);
assertThat(requestValues.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_CBOR);
service.performPost();
request = this.clientAdapter.getRequestSpec();
assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(request.getUriTemplate()).isEqualTo("/base/url");
assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(request.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
requestValues = this.clientAdapter.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(requestValues.getUriTemplate()).isEqualTo("/base/url");
assertThat(requestValues.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(requestValues.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
}
private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference<?> expectedBodyType) {
@ -191,7 +191,7 @@ public class HttpServiceMethodTests {
@SuppressWarnings("unused")
private interface ReactorService {
@HttpExchange
@GetExchange
Mono<Void> execute();
@GetExchange
@ -217,7 +217,7 @@ public class HttpServiceMethodTests {
@SuppressWarnings("unused")
private interface RxJavaService {
@HttpExchange
@GetExchange
Completable execute();
@GetExchange
@ -243,7 +243,7 @@ public class HttpServiceMethodTests {
@SuppressWarnings("unused")
private interface BlockingService {
@HttpExchange
@GetExchange
void execute();
@GetExchange
@ -261,7 +261,7 @@ public class HttpServiceMethodTests {
@SuppressWarnings("unused")
private interface MethodAnnotatedService {
private interface MethodLevelAnnotatedService {
@GetExchange
void performGet();
@ -274,7 +274,7 @@ public class HttpServiceMethodTests {
@SuppressWarnings("unused")
@HttpExchange(url = "/base", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE)
private interface TypeAndMethodAnnotatedService extends MethodAnnotatedService {
private interface TypeAndMethodLevelAnnotatedService extends MethodLevelAnnotatedService {
}
}

View File

@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.GetExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -143,44 +143,44 @@ class PathVariableArgumentResolverTests {
}
private Map<String, String> getActualUriVariables() {
return this.clientAdapter.getRequestSpec().getUriVariables();
return this.clientAdapter.getRequestValues().getUriVariables();
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private interface Service {
@HttpExchange
@GetExchange
void execute(@PathVariable String id);
@HttpExchange
@GetExchange
void executeNotRequired(@Nullable @PathVariable(required = false) String id);
@HttpExchange
@GetExchange
void executeOptional(@PathVariable Optional<Boolean> id);
@HttpExchange
@GetExchange
void executeOptionalNotRequired(@PathVariable(required = false) Optional<String> id);
@HttpExchange
@GetExchange
void executeNamedWithValue(@Nullable @PathVariable(name = "test", value = "id") String employeeId);
@HttpExchange
@GetExchange
void executeNamed(@PathVariable(name = "id") String employeeId);
@HttpExchange
@GetExchange
void executeValueNamed(@PathVariable("id") String employeeId);
@HttpExchange
@GetExchange
void execute(@PathVariable Object id);
@HttpExchange
@GetExchange
void execute(@PathVariable Boolean id);
@HttpExchange
@GetExchange
void executeValueMap(@Nullable @PathVariable Map<String, String> map);
@HttpExchange
@GetExchange
void executeOptionalValueMap(@PathVariable Map<String, Optional<String>> map);
}

View File

@ -44,7 +44,7 @@ class TestHttpClientAdapter implements HttpClientAdapter {
private String invokedMethodName;
@Nullable
private HttpRequestSpec requestSpec;
private HttpRequestValues requestValues;
@Nullable
private ParameterizedTypeReference<?> bodyType;
@ -67,9 +67,9 @@ class TestHttpClientAdapter implements HttpClientAdapter {
return this.invokedMethodName;
}
public HttpRequestSpec getRequestSpec() {
assertThat(this.requestSpec).isNotNull();
return this.requestSpec;
public HttpRequestValues getRequestValues() {
assertThat(this.requestValues).isNotNull();
return this.requestValues;
}
@Nullable
@ -81,56 +81,56 @@ class TestHttpClientAdapter implements HttpClientAdapter {
// HttpClientAdapter implementation
@Override
public Mono<Void> requestToVoid(HttpRequestSpec requestSpec) {
saveInput("requestToVoid", requestSpec, null);
public Mono<Void> requestToVoid(HttpRequestValues requestValues) {
saveInput("requestToVoid", requestValues, null);
return Mono.empty();
}
@Override
public Mono<HttpHeaders> requestToHeaders(HttpRequestSpec requestSpec) {
saveInput("requestToHeaders", requestSpec, null);
public Mono<HttpHeaders> requestToHeaders(HttpRequestValues requestValues) {
saveInput("requestToHeaders", requestValues, null);
return Mono.just(new HttpHeaders());
}
@Override
public <T> Mono<T> requestToBody(HttpRequestSpec requestSpec, ParameterizedTypeReference<T> bodyType) {
saveInput("requestToBody", requestSpec, bodyType);
public <T> Mono<T> requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
saveInput("requestToBody", requestValues, bodyType);
return (Mono<T>) Mono.just(getInvokedMethodName());
}
@Override
public <T> Flux<T> requestToBodyFlux(HttpRequestSpec requestSpec, ParameterizedTypeReference<T> bodyType) {
saveInput("requestToBodyFlux", requestSpec, bodyType);
public <T> Flux<T> requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
saveInput("requestToBodyFlux", requestValues, bodyType);
return (Flux<T>) Flux.just("request", "To", "Body", "Flux");
}
@Override
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestSpec requestSpec) {
saveInput("requestToBodilessEntity", requestSpec, null);
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestValues requestValues) {
saveInput("requestToBodilessEntity", requestValues, null);
return Mono.just(ResponseEntity.ok().build());
}
@Override
public <T> Mono<ResponseEntity<T>> requestToEntity(
HttpRequestSpec requestSpec, ParameterizedTypeReference<T> type) {
HttpRequestValues requestValues, ParameterizedTypeReference<T> type) {
saveInput("requestToEntity", requestSpec, type);
saveInput("requestToEntity", requestValues, type);
return Mono.just((ResponseEntity<T>) ResponseEntity.ok("requestToEntity"));
}
@Override
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(
HttpRequestSpec requestSpec, ParameterizedTypeReference<T> bodyType) {
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
saveInput("requestToEntityFlux", requestSpec, bodyType);
saveInput("requestToEntityFlux", requestValues, bodyType);
return Mono.just(ResponseEntity.ok((Flux<T>) Flux.just("request", "To", "Entity", "Flux")));
}
private <T> void saveInput(
String methodName, HttpRequestSpec requestSpec, @Nullable ParameterizedTypeReference<T> bodyType) {
String methodName, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference<T> bodyType) {
this.invokedMethodName = methodName;
this.requestSpec = requestSpec;
this.requestValues = requestValues;
this.bodyType = bodyType;
}

View File

@ -24,10 +24,11 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.invoker.HttpClientAdapter;
import org.springframework.web.service.invoker.HttpRequestSpec;
import org.springframework.web.service.invoker.HttpRequestValues;
/**
@ -47,67 +48,68 @@ public class WebClientAdapter implements HttpClientAdapter {
@Override
public Mono<Void> requestToVoid(HttpRequestSpec requestSpec) {
return toBodySpec(requestSpec).exchangeToMono(ClientResponse::releaseBody);
public Mono<Void> requestToVoid(HttpRequestValues requestValues) {
return toBodySpec(requestValues).exchangeToMono(ClientResponse::releaseBody);
}
@Override
public Mono<HttpHeaders> requestToHeaders(HttpRequestSpec requestSpec) {
return toBodySpec(requestSpec).retrieve().toBodilessEntity().map(ResponseEntity::getHeaders);
public Mono<HttpHeaders> requestToHeaders(HttpRequestValues requestValues) {
return toBodySpec(requestValues).retrieve().toBodilessEntity().map(ResponseEntity::getHeaders);
}
@Override
public <T> Mono<T> requestToBody(HttpRequestSpec reqrequestSpecest, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(reqrequestSpecest).retrieve().bodyToMono(bodyType);
public <T> Mono<T> requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(requestValues).retrieve().bodyToMono(bodyType);
}
@Override
public <T> Flux<T> requestToBodyFlux(HttpRequestSpec requestSpec, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(requestSpec).retrieve().bodyToFlux(bodyType);
public <T> Flux<T> requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(requestValues).retrieve().bodyToFlux(bodyType);
}
@Override
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestSpec requestSpec) {
return toBodySpec(requestSpec).retrieve().toBodilessEntity();
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestValues requestValues) {
return toBodySpec(requestValues).retrieve().toBodilessEntity();
}
@Override
public <T> Mono<ResponseEntity<T>> requestToEntity(HttpRequestSpec spec, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(spec).retrieve().toEntity(bodyType);
public <T> Mono<ResponseEntity<T>> requestToEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(requestValues).retrieve().toEntity(bodyType);
}
@Override
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestSpec spec, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(spec).retrieve().toEntityFlux(bodyType);
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
return toBodySpec(requestValues).retrieve().toEntityFlux(bodyType);
}
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
private WebClient.RequestBodySpec toBodySpec(HttpRequestSpec requestSpec) {
private WebClient.RequestBodySpec toBodySpec(HttpRequestValues requestValues) {
HttpMethod httpMethod = requestValues.getHttpMethod();
Assert.notNull(httpMethod, "No HttpMethod");
HttpMethod httpMethod = requestSpec.getHttpMethodRequired();
WebClient.RequestBodyUriSpec uriSpec = this.webClient.method(httpMethod);
WebClient.RequestBodySpec bodySpec;
if (requestSpec.getUri() != null) {
bodySpec = uriSpec.uri(requestSpec.getUri());
if (requestValues.getUri() != null) {
bodySpec = uriSpec.uri(requestValues.getUri());
}
else if (requestSpec.getUriTemplate() != null) {
bodySpec = (!requestSpec.getUriVariables().isEmpty() ?
uriSpec.uri(requestSpec.getUriTemplate(), requestSpec.getUriVariables()) :
uriSpec.uri(requestSpec.getUriTemplate(), requestSpec.getUriVariableValues()));
else if (requestValues.getUriTemplate() != null) {
bodySpec = uriSpec.uri(requestValues.getUriTemplate(), requestValues.getUriVariables());
}
else {
bodySpec = uriSpec.uri("");
throw new IllegalStateException("Neither full URL nor URI template");
}
bodySpec.headers(headers -> headers.putAll(requestSpec.getHeaders()));
bodySpec.cookies(cookies -> cookies.putAll(requestSpec.getCookies()));
bodySpec.headers(headers -> headers.putAll(requestValues.getHeaders()));
bodySpec.cookies(cookies -> cookies.putAll(requestValues.getCookies()));
if (requestSpec.getBodyValue() != null) {
bodySpec.bodyValue(requestSpec.getBodyValue());
if (requestValues.getBodyValue() != null) {
bodySpec.bodyValue(requestValues.getBodyValue());
}
else if (requestSpec.getBodyPublisher() != null) {
bodySpec.body(requestSpec.getBodyPublisher(), requestSpec.getBodyPublisherElementType());
else if (requestValues.getBody() != null) {
Assert.notNull(requestValues.getBodyElementType(), "Publisher body element type is required");
bodySpec.body(requestValues.getBody(), requestValues.getBodyElementType());
}
return bodySpec;