diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java index 58033379d0..9d57856042 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpClientAdapter.java @@ -33,18 +33,18 @@ import org.springframework.http.ResponseEntity; */ public interface HttpClientAdapter { - Mono requestToVoid(HttpRequestSpec spec); + Mono requestToVoid(HttpRequestValues requestValues); - Mono requestToHeaders(HttpRequestSpec spec); + Mono requestToHeaders(HttpRequestValues requestValues); - Mono requestToBody(HttpRequestSpec spec, ParameterizedTypeReference bodyType); + Mono requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference bodyType); - Flux requestToBodyFlux(HttpRequestSpec spec, ParameterizedTypeReference bodyType); + Flux requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference bodyType); - Mono> requestToBodilessEntity(HttpRequestSpec spec); + Mono> requestToBodilessEntity(HttpRequestValues requestValues); - Mono> requestToEntity(HttpRequestSpec spec, ParameterizedTypeReference bodyType); + Mono> requestToEntity(HttpRequestValues requestValues, ParameterizedTypeReference bodyType); - Mono>> requestToEntityFlux(HttpRequestSpec spec, ParameterizedTypeReference bodyType); + Mono>> requestToEntityFlux(HttpRequestValues requestValues, ParameterizedTypeReference bodyType); } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java index 9a4fe47b3d..4be874d87b 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpMethodArgumentResolver.java @@ -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); } } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestSpec.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestSpec.java deleted file mode 100644 index 1801e9afee..0000000000 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestSpec.java +++ /dev/null @@ -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 EMPTY_COOKIES_MAP = - CollectionUtils.toMultiValueMap(Collections.emptyMap()); - - - @Nullable - private URI uri; - - @Nullable - private String uriTemplate; - - @Nullable - private Map uriVariables; - - @Nullable - private List uriVariablesList; - - @Nullable - private HttpMethod httpMethod; - - @Nullable - private HttpHeaders headers; - - @Nullable - private MultiValueMap 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 getUriVariables() { - this.uriVariables = (this.uriVariables != null ? this.uriVariables : new LinkedHashMap<>()); - return this.uriVariables; - } - - public List 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 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 > void setBodyPublisher(Publisher

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); - } - -} diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java new file mode 100644 index 0000000000..64f8f52534 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -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 EMPTY_COOKIES_MAP = + CollectionUtils.toMultiValueMap(Collections.emptyMap()); + + + private final HttpMethod httpMethod; + + @Nullable + private final URI uri; + + @Nullable + private final String uriTemplate; + + private final Map uriVariables; + + private final HttpHeaders headers; + + private final MultiValueMap 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 uriVariables, + @Nullable HttpHeaders headers, @Nullable MultiValueMap 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. + *

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. + *

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 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 getCookies() { + return this.cookies; + } + + /** + * Return the request body as a value to be serialized, if set. + *

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. + *

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 uriVariables; + + @Nullable + private HttpHeaders headers; + + @Nullable + private MultiValueMap 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. + *

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. + *

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. + *

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 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. + *

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. + *

This is mutually exclusive with, and resets any previously set + * {@link #setBodyValue(Object) body value}. + */ + public > void setBody(Publisher

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); + } + + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceArgumentResolver.java index c041e78d7e..4c68475e02 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceArgumentResolver.java @@ -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); } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index 1a1793bc05..cba2b08a24 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -62,7 +62,7 @@ final class HttpServiceMethod { private final List 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 acceptMediaTypes) { - private HttpRequestSpecFactory( - @Nullable HttpMethod httpMethod, @Nullable String url, + private HttpRequestValuesInitializer( + HttpMethod httpMethod, @Nullable String url, @Nullable MediaType contentType, @Nullable List 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 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> responseFunction, + Function> responseFunction, @Nullable ReactiveAdapter returnTypeAdapter, boolean blockForOptional, Duration blockTimeout) { private ResponseFunction( - Function> responseFunction, + Function> 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> responseFunction; + Function> 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> initResponseEntityFunction( + private static Function> initResponseEntityFunction( HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) { if (reactiveAdapter == null) { @@ -349,7 +344,7 @@ final class HttpServiceMethod { }); } - private static Function> initBodyFunction( + private static Function> initBodyFunction( HttpClientAdapter client, MethodParameter methodParam, @Nullable ReactiveAdapter reactiveAdapter) { ParameterizedTypeReference bodyType = diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java index 4ab0e17ce3..5be129459e 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/PathVariableArgumentResolver.java @@ -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) 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); } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java index f7042f1a8a..a40db1d3ce 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java @@ -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); } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java index 5bdcf6109d..6ebc67ad06 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java @@ -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 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 { } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java index f920345cf0..a050b7606a 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java @@ -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 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 id); - @HttpExchange + @GetExchange void executeOptionalNotRequired(@PathVariable(required = false) Optional 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 map); - @HttpExchange + @GetExchange void executeOptionalValueMap(@PathVariable Map> map); } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java index 591503d734..06bff5e3c8 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java @@ -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 requestToVoid(HttpRequestSpec requestSpec) { - saveInput("requestToVoid", requestSpec, null); + public Mono requestToVoid(HttpRequestValues requestValues) { + saveInput("requestToVoid", requestValues, null); return Mono.empty(); } @Override - public Mono requestToHeaders(HttpRequestSpec requestSpec) { - saveInput("requestToHeaders", requestSpec, null); + public Mono requestToHeaders(HttpRequestValues requestValues) { + saveInput("requestToHeaders", requestValues, null); return Mono.just(new HttpHeaders()); } @Override - public Mono requestToBody(HttpRequestSpec requestSpec, ParameterizedTypeReference bodyType) { - saveInput("requestToBody", requestSpec, bodyType); + public Mono requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) { + saveInput("requestToBody", requestValues, bodyType); return (Mono) Mono.just(getInvokedMethodName()); } @Override - public Flux requestToBodyFlux(HttpRequestSpec requestSpec, ParameterizedTypeReference bodyType) { - saveInput("requestToBodyFlux", requestSpec, bodyType); + public Flux requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) { + saveInput("requestToBodyFlux", requestValues, bodyType); return (Flux) Flux.just("request", "To", "Body", "Flux"); } @Override - public Mono> requestToBodilessEntity(HttpRequestSpec requestSpec) { - saveInput("requestToBodilessEntity", requestSpec, null); + public Mono> requestToBodilessEntity(HttpRequestValues requestValues) { + saveInput("requestToBodilessEntity", requestValues, null); return Mono.just(ResponseEntity.ok().build()); } @Override public Mono> requestToEntity( - HttpRequestSpec requestSpec, ParameterizedTypeReference type) { + HttpRequestValues requestValues, ParameterizedTypeReference type) { - saveInput("requestToEntity", requestSpec, type); + saveInput("requestToEntity", requestValues, type); return Mono.just((ResponseEntity) ResponseEntity.ok("requestToEntity")); } @Override public Mono>> requestToEntityFlux( - HttpRequestSpec requestSpec, ParameterizedTypeReference bodyType) { + HttpRequestValues requestValues, ParameterizedTypeReference bodyType) { - saveInput("requestToEntityFlux", requestSpec, bodyType); + saveInput("requestToEntityFlux", requestValues, bodyType); return Mono.just(ResponseEntity.ok((Flux) Flux.just("request", "To", "Entity", "Flux"))); } private void saveInput( - String methodName, HttpRequestSpec requestSpec, @Nullable ParameterizedTypeReference bodyType) { + String methodName, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference bodyType) { this.invokedMethodName = methodName; - this.requestSpec = requestSpec; + this.requestValues = requestValues; this.bodyType = bodyType; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java index 59c5b29041..e4685b6731 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java @@ -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 requestToVoid(HttpRequestSpec requestSpec) { - return toBodySpec(requestSpec).exchangeToMono(ClientResponse::releaseBody); + public Mono requestToVoid(HttpRequestValues requestValues) { + return toBodySpec(requestValues).exchangeToMono(ClientResponse::releaseBody); } @Override - public Mono requestToHeaders(HttpRequestSpec requestSpec) { - return toBodySpec(requestSpec).retrieve().toBodilessEntity().map(ResponseEntity::getHeaders); + public Mono requestToHeaders(HttpRequestValues requestValues) { + return toBodySpec(requestValues).retrieve().toBodilessEntity().map(ResponseEntity::getHeaders); } @Override - public Mono requestToBody(HttpRequestSpec reqrequestSpecest, ParameterizedTypeReference bodyType) { - return toBodySpec(reqrequestSpecest).retrieve().bodyToMono(bodyType); + public Mono requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) { + return toBodySpec(requestValues).retrieve().bodyToMono(bodyType); } @Override - public Flux requestToBodyFlux(HttpRequestSpec requestSpec, ParameterizedTypeReference bodyType) { - return toBodySpec(requestSpec).retrieve().bodyToFlux(bodyType); + public Flux requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) { + return toBodySpec(requestValues).retrieve().bodyToFlux(bodyType); } @Override - public Mono> requestToBodilessEntity(HttpRequestSpec requestSpec) { - return toBodySpec(requestSpec).retrieve().toBodilessEntity(); + public Mono> requestToBodilessEntity(HttpRequestValues requestValues) { + return toBodySpec(requestValues).retrieve().toBodilessEntity(); } @Override - public Mono> requestToEntity(HttpRequestSpec spec, ParameterizedTypeReference bodyType) { - return toBodySpec(spec).retrieve().toEntity(bodyType); + public Mono> requestToEntity(HttpRequestValues requestValues, ParameterizedTypeReference bodyType) { + return toBodySpec(requestValues).retrieve().toEntity(bodyType); } @Override - public Mono>> requestToEntityFlux(HttpRequestSpec spec, ParameterizedTypeReference bodyType) { - return toBodySpec(spec).retrieve().toEntityFlux(bodyType); + public Mono>> requestToEntityFlux(HttpRequestValues requestValues, ParameterizedTypeReference 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;