From 118f33aedad84e9f8a01e4db983545d18fdd1fc3 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 29 Mar 2017 11:51:51 -0400 Subject: [PATCH] Request body improvements in WebClient, WebTestClient This commit makes changes to WebClient and WebTestClient in oder to limit setting the body according to HTTP method and also to facilitate providing the request body as Object. Specifically, this commit: - Moves methods that operate on the request body to a RequestBodySpec in both WebClient and WebTestClient, and rename them to `body`. These methods now just *set* the body, without performing an exchange (which now requires an explicit exchange call). - Parameterizes UriSpec in both WebClient and WebTestClient, so that it returns either a RequestHeadersSpec or a RequestBodySpec. Issue: SPR-15394 --- .../reactive/server/DefaultWebTestClient.java | 127 ++++++++++-------- .../web/reactive/server/WebTestClient.java | 101 +++++++------- .../server/samples/ResponseEntityTests.java | 5 +- .../function/client/DefaultWebClient.java | 104 ++++++++------ .../reactive/function/client/WebClient.java | 120 ++++++++++------- .../function/client/WebClientExtensions.kt | 22 ++- .../client/WebClientIntegrationTests.java | 7 +- 7 files changed, 286 insertions(+), 200 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 0eefdceeb8..92f0f8ba93 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -34,6 +34,7 @@ import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; @@ -97,42 +98,45 @@ class DefaultWebTestClient implements WebTestClient { @Override - public UriSpec get() { - return toUriSpec(WebClient::get); + public UriSpec> get() { + return toUriSpec(wc -> wc.method(HttpMethod.GET)); } @Override - public UriSpec head() { - return toUriSpec(WebClient::head); + public UriSpec> head() { + return toUriSpec(wc -> wc.method(HttpMethod.HEAD)); } @Override - public UriSpec post() { - return toUriSpec(WebClient::post); + public UriSpec post() { + return toUriSpec(wc -> wc.method(HttpMethod.POST)); } @Override - public UriSpec put() { - return toUriSpec(WebClient::put); + public UriSpec put() { + return toUriSpec(wc -> wc.method(HttpMethod.PUT)); } @Override - public UriSpec patch() { - return toUriSpec(WebClient::patch); + public UriSpec patch() { + return toUriSpec(wc -> wc.method(HttpMethod.PATCH)); } @Override - public UriSpec delete() { - return toUriSpec(WebClient::delete); + public UriSpec> delete() { + return toUriSpec(wc -> wc.method(HttpMethod.DELETE)); } @Override - public UriSpec options() { - return toUriSpec(WebClient::options); + public UriSpec> options() { + return toUriSpec(wc -> wc.method(HttpMethod.OPTIONS)); } - private UriSpec toUriSpec(Function function) { - return new DefaultUriSpec(function.apply(this.webClient)); + @SuppressWarnings("unchecked") + private > UriSpec toUriSpec( + Function> function) { + + return new DefaultUriSpec<>(function.apply(this.webClient)); } @@ -156,123 +160,132 @@ class DefaultWebTestClient implements WebTestClient { } - private class DefaultUriSpec implements UriSpec { + @SuppressWarnings("unchecked") + private class DefaultUriSpec> implements UriSpec { - private final WebClient.UriSpec uriSpec; + private final WebClient.UriSpec uriSpec; - DefaultUriSpec(WebClient.UriSpec spec) { + DefaultUriSpec(WebClient.UriSpec spec) { this.uriSpec = spec; } @Override - public HeaderSpec uri(URI uri) { - return new DefaultHeaderSpec(this.uriSpec.uri(uri)); + public S uri(URI uri) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uri)); } @Override - public HeaderSpec uri(String uriTemplate, Object... uriVariables) { - return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables)); + public S uri(String uriTemplate, Object... uriVariables) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(String uriTemplate, Map uriVariables) { - return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables)); + public S uri(String uriTemplate, Map uriVariables) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(Function uriBuilder) { - return new DefaultHeaderSpec(this.uriSpec.uri(uriBuilder)); + public S uri(Function uriBuilder) { + return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uriBuilder)); } } - private class DefaultHeaderSpec implements WebTestClient.HeaderSpec { + private class DefaultRequestBodySpec implements RequestBodySpec { - private final WebClient.HeaderSpec headerSpec; + private final WebClient.RequestBodySpec bodySpec; private final String requestId; - DefaultHeaderSpec(WebClient.HeaderSpec spec) { - this.headerSpec = spec; + DefaultRequestBodySpec(WebClient.RequestBodySpec spec) { + this.bodySpec = spec; this.requestId = String.valueOf(requestIndex.incrementAndGet()); - this.headerSpec.header(WiretapConnector.REQUEST_ID_HEADER_NAME, this.requestId); + this.bodySpec.header(WiretapConnector.REQUEST_ID_HEADER_NAME, this.requestId); } @Override - public DefaultHeaderSpec header(String headerName, String... headerValues) { - this.headerSpec.header(headerName, headerValues); + public RequestBodySpec header(String headerName, String... headerValues) { + this.bodySpec.header(headerName, headerValues); return this; } @Override - public DefaultHeaderSpec headers(HttpHeaders headers) { - this.headerSpec.headers(headers); + public RequestBodySpec headers(HttpHeaders headers) { + this.bodySpec.headers(headers); return this; } @Override - public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { - this.headerSpec.accept(acceptableMediaTypes); + public RequestBodySpec accept(MediaType... acceptableMediaTypes) { + this.bodySpec.accept(acceptableMediaTypes); return this; } @Override - public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { - this.headerSpec.acceptCharset(acceptableCharsets); + public RequestBodySpec acceptCharset(Charset... acceptableCharsets) { + this.bodySpec.acceptCharset(acceptableCharsets); return this; } @Override - public DefaultHeaderSpec contentType(MediaType contentType) { - this.headerSpec.contentType(contentType); + public RequestBodySpec contentType(MediaType contentType) { + this.bodySpec.contentType(contentType); return this; } @Override - public DefaultHeaderSpec contentLength(long contentLength) { - this.headerSpec.contentLength(contentLength); + public RequestBodySpec contentLength(long contentLength) { + this.bodySpec.contentLength(contentLength); return this; } @Override - public DefaultHeaderSpec cookie(String name, String value) { - this.headerSpec.cookie(name, value); + public RequestBodySpec cookie(String name, String value) { + this.bodySpec.cookie(name, value); return this; } @Override - public DefaultHeaderSpec cookies(MultiValueMap cookies) { - this.headerSpec.cookies(cookies); + public RequestBodySpec cookies(MultiValueMap cookies) { + this.bodySpec.cookies(cookies); return this; } @Override - public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { - this.headerSpec.ifModifiedSince(ifModifiedSince); + public RequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) { + this.bodySpec.ifModifiedSince(ifModifiedSince); return this; } @Override - public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { - this.headerSpec.ifNoneMatch(ifNoneMatches); + public RequestBodySpec ifNoneMatch(String... ifNoneMatches) { + this.bodySpec.ifNoneMatch(ifNoneMatches); return this; } @Override public ResponseSpec exchange() { - return toResponseSpec(this.headerSpec.exchange()); + return toResponseSpec(this.bodySpec.exchange()); } @Override - public ResponseSpec exchange(BodyInserter inserter) { - return toResponseSpec(this.headerSpec.exchange(inserter)); + public RequestHeadersSpec body(BodyInserter inserter) { + this.bodySpec.body(inserter); + return this; } @Override - public > ResponseSpec exchange(S publisher, Class elementClass) { - return toResponseSpec(this.headerSpec.exchange(publisher, elementClass)); + public > RequestHeadersSpec body(S publisher, Class elementClass) { + this.bodySpec.body(publisher, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(T body) { + this.bodySpec.body(body); + return this; } private DefaultResponseSpec toResponseSpec(Mono mono) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 8b1750ecc1..155107aa67 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -78,43 +78,43 @@ public interface WebTestClient { * Prepare an HTTP GET request. * @return a spec for specifying the target URL */ - UriSpec get(); + UriSpec> get(); /** * Prepare an HTTP HEAD request. * @return a spec for specifying the target URL */ - UriSpec head(); + UriSpec> head(); /** * Prepare an HTTP POST request. * @return a spec for specifying the target URL */ - UriSpec post(); + UriSpec post(); /** * Prepare an HTTP PUT request. * @return a spec for specifying the target URL */ - UriSpec put(); + UriSpec put(); /** * Prepare an HTTP PATCH request. * @return a spec for specifying the target URL */ - UriSpec patch(); + UriSpec patch(); /** * Prepare an HTTP DELETE request. * @return a spec for specifying the target URL */ - UriSpec delete(); + UriSpec> delete(); /** * Prepare an HTTP OPTIONS request. * @return a spec for specifying the target URL */ - UriSpec options(); + UriSpec> options(); /** @@ -327,13 +327,13 @@ public interface WebTestClient { /** * Specification for providing the URI of a request. */ - interface UriSpec { + interface UriSpec> { /** * Specify the URI using an absolute, fully constructed {@link URI}. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(URI uri); + S uri(URI uri); /** * Specify the URI for the request using a URI template and URI variables. @@ -341,7 +341,7 @@ public interface WebTestClient { * with a base URI) it will be used to expand the URI template. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(String uri, Object... uriVariables); + S uri(String uri, Object... uriVariables); /** * Specify the URI for the request using a URI template and URI variables. @@ -349,21 +349,21 @@ public interface WebTestClient { * with a base URI) it will be used to expand the URI template. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(String uri, Map uriVariables); + S uri(String uri, Map uriVariables); /** * Build the URI for the request with a {@link UriBuilder} obtained * through the {@link UriBuilderFactory} configured for this client. * @return spec to add headers or perform the exchange */ - HeaderSpec uri(Function uriFunction); + S uri(Function uriFunction); } /** * Specification for adding request headers and performing an exchange. */ - interface HeaderSpec { + interface RequestHeadersSpec> { /** * Set the list of acceptable {@linkplain MediaType media types}, as @@ -371,7 +371,7 @@ public interface WebTestClient { * @param acceptableMediaTypes the acceptable media types * @return the same instance */ - HeaderSpec accept(MediaType... acceptableMediaTypes); + S accept(MediaType... acceptableMediaTypes); /** * Set the list of acceptable {@linkplain Charset charsets}, as specified @@ -379,25 +379,7 @@ public interface WebTestClient { * @param acceptableCharsets the acceptable charsets * @return the same instance */ - HeaderSpec acceptCharset(Charset... acceptableCharsets); - - /** - * Set the length of the body in bytes, as specified by the - * {@code Content-Length} header. - * @param contentLength the content length - * @return the same instance - * @see HttpHeaders#setContentLength(long) - */ - HeaderSpec contentLength(long contentLength); - - /** - * Set the {@linkplain MediaType media type} of the body, as specified - * by the {@code Content-Type} header. - * @param contentType the content type - * @return the same instance - * @see HttpHeaders#setContentType(MediaType) - */ - HeaderSpec contentType(MediaType contentType); + S acceptCharset(Charset... acceptableCharsets); /** * Add a cookie with the given name and value. @@ -405,7 +387,7 @@ public interface WebTestClient { * @param value the cookie value * @return the same instance */ - HeaderSpec cookie(String name, String value); + S cookie(String name, String value); /** * Copy the given cookies into the entity's cookies map. @@ -413,7 +395,7 @@ public interface WebTestClient { * @param cookies the existing cookies to copy from * @return the same instance */ - HeaderSpec cookies(MultiValueMap cookies); + S cookies(MultiValueMap cookies); /** * Set the value of the {@code If-Modified-Since} header. @@ -422,14 +404,14 @@ public interface WebTestClient { * @param ifModifiedSince the new value of the header * @return the same instance */ - HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince); + S ifModifiedSince(ZonedDateTime ifModifiedSince); /** * Set the values of the {@code If-None-Match} header. * @param ifNoneMatches the new value of the header * @return the same instance */ - HeaderSpec ifNoneMatch(String... ifNoneMatches); + S ifNoneMatch(String... ifNoneMatches); /** * Add the given, single header value under the given name. @@ -437,14 +419,14 @@ public interface WebTestClient { * @param headerValues the header value(s) * @return the same instance */ - HeaderSpec header(String headerName, String... headerValues); + S header(String headerName, String... headerValues); /** * Copy the given headers into the entity's headers map. * @param headers the existing headers to copy from * @return the same instance */ - HeaderSpec headers(HttpHeaders headers); + S headers(HttpHeaders headers); /** * Perform the exchange without a request body. @@ -452,26 +434,55 @@ public interface WebTestClient { */ ResponseSpec exchange(); + } + + interface RequestBodySpec extends RequestHeadersSpec { + /** + * Set the length of the body in bytes, as specified by the + * {@code Content-Length} header. + * @param contentLength the content length + * @return the same instance + * @see HttpHeaders#setContentLength(long) + */ + RequestBodySpec contentLength(long contentLength); + /** - * Perform the exchange with the body for the request populated using - * a {@link BodyInserter}. + * Set the {@linkplain MediaType media type} of the body, as specified + * by the {@code Content-Type} header. + * @param contentType the content type + * @return the same instance + * @see HttpHeaders#setContentType(MediaType) + */ + RequestBodySpec contentType(MediaType contentType); + + /** + * Set the body of the request to the given {@code BodyInserter}. * @param inserter the inserter * @param the body type, or the the element type (for a stream) * @return spec for decoding the response * @see org.springframework.web.reactive.function.BodyInserters */ - ResponseSpec exchange(BodyInserter inserter); + RequestHeadersSpec body(BodyInserter inserter); /** - * Perform the exchange and use the given {@code Publisher} for the - * request body. + * Set the body of the request to the given {@code Publisher}. * @param publisher the request body data * @param elementClass the class of elements contained in the publisher * @param the type of the elements contained in the publisher * @param the type of the {@code Publisher} * @return spec for decoding the response */ - > ResponseSpec exchange(S publisher, Class elementClass); + > RequestHeadersSpec body(S publisher, Class elementClass); + + /** + * Set the body of the request to the given {@code Object} and + * perform the request. + * @param body the {@code Object} to write to the request + * @param the type contained in the body + * @return a {@code Mono} with the response + */ + RequestHeadersSpec body(T body); + } /** diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java index bc7c71bbb3..e0255eb9dc 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java @@ -41,7 +41,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import static org.hamcrest.CoreMatchers.endsWith; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; /** @@ -115,7 +115,8 @@ public class ResponseEntityTests { @Test public void postEntity() throws Exception { this.client.post().uri("/persons") - .exchange(Mono.just(new Person("John")), Person.class) + .body(Mono.just(new Person("John")), Person.class) + .exchange() .expectStatus().isCreated() .expectHeader().valueEquals("location", "/persons/John") .expectBody().isEmpty(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 00922bc7cd..44de4ed753 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -36,6 +36,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilderFactory; @@ -70,42 +71,48 @@ class DefaultWebClient implements WebClient { @Override - public UriSpec get() { - return method(HttpMethod.GET); + public UriSpec> get() { + return methodInternal(HttpMethod.GET); } @Override - public UriSpec head() { - return method(HttpMethod.HEAD); + public UriSpec> head() { + return methodInternal(HttpMethod.HEAD); } @Override - public UriSpec post() { - return method(HttpMethod.POST); + public UriSpec post() { + return methodInternal(HttpMethod.POST); } @Override - public UriSpec put() { - return method(HttpMethod.PUT); + public UriSpec put() { + return methodInternal(HttpMethod.PUT); } @Override - public UriSpec patch() { - return method(HttpMethod.PATCH); + public UriSpec patch() { + return methodInternal(HttpMethod.PATCH); } @Override - public UriSpec delete() { - return method(HttpMethod.DELETE); + public UriSpec> delete() { + return methodInternal(HttpMethod.DELETE); } @Override - public UriSpec options() { - return method(HttpMethod.OPTIONS); + public UriSpec> options() { + return methodInternal(HttpMethod.OPTIONS); } - private UriSpec method(HttpMethod httpMethod) { - return new DefaultUriSpec(httpMethod); + @Override + public UriSpec method(HttpMethod httpMethod) { + return methodInternal(httpMethod); + } + + @SuppressWarnings("unchecked") + private > UriSpec methodInternal(HttpMethod httpMethod) { + return new DefaultUriSpec<>(httpMethod); } @Override @@ -116,7 +123,7 @@ class DefaultWebClient implements WebClient { } - private class DefaultUriSpec implements UriSpec { + private class DefaultUriSpec> implements UriSpec { private final HttpMethod httpMethod; @@ -126,28 +133,29 @@ class DefaultWebClient implements WebClient { } @Override - public HeaderSpec uri(String uriTemplate, Object... uriVariables) { + public S uri(String uriTemplate, Object... uriVariables) { return uri(uriBuilderFactory.expand(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(String uriTemplate, Map uriVariables) { + public S uri(String uriTemplate, Map uriVariables) { return uri(uriBuilderFactory.expand(uriTemplate, uriVariables)); } @Override - public HeaderSpec uri(Function uriFunction) { + public S uri(Function uriFunction) { return uri(uriFunction.apply(uriBuilderFactory.builder())); } @Override - public HeaderSpec uri(URI uri) { - return new DefaultHeaderSpec(this.httpMethod, uri); + @SuppressWarnings("unchecked") + public S uri(URI uri) { + return (S) new DefaultRequestBodySpec(this.httpMethod, uri); } } - private class DefaultHeaderSpec implements HeaderSpec { + private class DefaultRequestBodySpec implements RequestBodySpec { private final HttpMethod httpMethod; @@ -157,7 +165,9 @@ class DefaultWebClient implements WebClient { private MultiValueMap cookies; - DefaultHeaderSpec(HttpMethod httpMethod, URI uri) { + private BodyInserter inserter; + + DefaultRequestBodySpec(HttpMethod httpMethod, URI uri) { this.httpMethod = httpMethod; this.uri = uri; } @@ -177,7 +187,7 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec header(String headerName, String... headerValues) { + public DefaultRequestBodySpec header(String headerName, String... headerValues) { for (String headerValue : headerValues) { getHeaders().add(headerName, headerValue); } @@ -185,7 +195,7 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec headers(HttpHeaders headers) { + public DefaultRequestBodySpec headers(HttpHeaders headers) { if (headers != null) { getHeaders().putAll(headers); } @@ -193,37 +203,37 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) { + public DefaultRequestBodySpec accept(MediaType... acceptableMediaTypes) { getHeaders().setAccept(Arrays.asList(acceptableMediaTypes)); return this; } @Override - public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) { + public DefaultRequestBodySpec acceptCharset(Charset... acceptableCharsets) { getHeaders().setAcceptCharset(Arrays.asList(acceptableCharsets)); return this; } @Override - public DefaultHeaderSpec contentType(MediaType contentType) { + public DefaultRequestBodySpec contentType(MediaType contentType) { getHeaders().setContentType(contentType); return this; } @Override - public DefaultHeaderSpec contentLength(long contentLength) { + public DefaultRequestBodySpec contentLength(long contentLength) { getHeaders().setContentLength(contentLength); return this; } @Override - public DefaultHeaderSpec cookie(String name, String value) { + public DefaultRequestBodySpec cookie(String name, String value) { getCookies().add(name, value); return this; } @Override - public DefaultHeaderSpec cookies(MultiValueMap cookies) { + public DefaultRequestBodySpec cookies(MultiValueMap cookies) { if (cookies != null) { getCookies().putAll(cookies); } @@ -231,7 +241,7 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) { + public DefaultRequestBodySpec ifModifiedSince(ZonedDateTime ifModifiedSince) { ZonedDateTime gmt = ifModifiedSince.withZoneSameInstant(ZoneId.of("GMT")); String headerValue = DateTimeFormatter.RFC_1123_DATE_TIME.format(gmt); getHeaders().set(HttpHeaders.IF_MODIFIED_SINCE, headerValue); @@ -239,26 +249,36 @@ class DefaultWebClient implements WebClient { } @Override - public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) { + public DefaultRequestBodySpec ifNoneMatch(String... ifNoneMatches) { getHeaders().setIfNoneMatch(Arrays.asList(ifNoneMatches)); return this; } @Override - public Mono exchange() { - ClientRequest request = this.initRequestBuilder().build(); - return exchangeFunction.exchange(request); + public RequestHeadersSpec body(BodyInserter inserter) { + this.inserter = inserter; + return this; } @Override - public Mono exchange(BodyInserter inserter) { - ClientRequest request = this.initRequestBuilder().body(inserter).build(); - return exchangeFunction.exchange(request); + public > RequestHeadersSpec body(S publisher, Class elementClass) { + this.inserter = BodyInserters.fromPublisher(publisher, elementClass); + return this; + } + + @Override + public RequestHeadersSpec body(T body) { + this.inserter = BodyInserters.fromObject(body); + return this; } @Override - public > Mono exchange(S publisher, Class elementClass) { - ClientRequest request = initRequestBuilder().headers(this.headers).body(publisher, elementClass).build(); + public Mono exchange() { + + ClientRequest request = this.inserter != null ? + initRequestBuilder().body(this.inserter).build() : + initRequestBuilder().build(); + return exchangeFunction.exchange(request); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 2d1b2e733f..0f608835c2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -26,6 +26,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; @@ -58,43 +59,49 @@ public interface WebClient { * Prepare an HTTP GET request. * @return a spec for specifying the target URL */ - UriSpec get(); + UriSpec> get(); /** * Prepare an HTTP HEAD request. * @return a spec for specifying the target URL */ - UriSpec head(); + UriSpec> head(); /** * Prepare an HTTP POST request. * @return a spec for specifying the target URL */ - UriSpec post(); + UriSpec post(); /** * Prepare an HTTP PUT request. * @return a spec for specifying the target URL */ - UriSpec put(); + UriSpec put(); /** * Prepare an HTTP PATCH request. * @return a spec for specifying the target URL */ - UriSpec patch(); + UriSpec patch(); /** * Prepare an HTTP DELETE request. * @return a spec for specifying the target URL */ - UriSpec delete(); + UriSpec> delete(); /** * Prepare an HTTP OPTIONS request. * @return a spec for specifying the target URL */ - UriSpec options(); + UriSpec> options(); + + /** + * Prepare a request for the specified {@code HttpMethod}. + * @return a spec for specifying the target URL + */ + UriSpec method(HttpMethod method); /** @@ -266,38 +273,38 @@ public interface WebClient { /** * Contract for specifying the URI for a request. */ - interface UriSpec { + interface UriSpec> { /** * Specify the URI using an absolute, fully constructed {@link URI}. */ - HeaderSpec uri(URI uri); + S uri(URI uri); /** * Specify the URI for the request using a URI template and URI variables. * If a {@link UriBuilderFactory} was configured for the client (e.g. * with a base URI) it will be used to expand the URI template. */ - HeaderSpec uri(String uri, Object... uriVariables); + S uri(String uri, Object... uriVariables); /** * Specify the URI for the request using a URI template and URI variables. * If a {@link UriBuilderFactory} was configured for the client (e.g. * with a base URI) it will be used to expand the URI template. */ - HeaderSpec uri(String uri, Map uriVariables); + S uri(String uri, Map uriVariables); /** * Build the URI for the request using the {@link UriBuilderFactory} * configured for this client. */ - HeaderSpec uri(Function uriFunction); + S uri(Function uriFunction); } /** * Contract for specifying request headers leading up to the exchange. */ - interface HeaderSpec { + interface RequestHeadersSpec> { /** * Set the list of acceptable {@linkplain MediaType media types}, as @@ -305,7 +312,7 @@ public interface WebClient { * @param acceptableMediaTypes the acceptable media types * @return this builder */ - HeaderSpec accept(MediaType... acceptableMediaTypes); + S accept(MediaType... acceptableMediaTypes); /** * Set the list of acceptable {@linkplain Charset charsets}, as specified @@ -313,25 +320,7 @@ public interface WebClient { * @param acceptableCharsets the acceptable charsets * @return this builder */ - HeaderSpec acceptCharset(Charset... acceptableCharsets); - - /** - * Set the length of the body in bytes, as specified by the - * {@code Content-Length} header. - * @param contentLength the content length - * @return this builder - * @see HttpHeaders#setContentLength(long) - */ - HeaderSpec contentLength(long contentLength); - - /** - * Set the {@linkplain MediaType media type} of the body, as specified - * by the {@code Content-Type} header. - * @param contentType the content type - * @return this builder - * @see HttpHeaders#setContentType(MediaType) - */ - HeaderSpec contentType(MediaType contentType); + S acceptCharset(Charset... acceptableCharsets); /** * Add a cookie with the given name and value. @@ -339,14 +328,14 @@ public interface WebClient { * @param value the cookie value * @return this builder */ - HeaderSpec cookie(String name, String value); + S cookie(String name, String value); /** * Copy the given cookies into the entity's cookies map. * @param cookies the existing cookies to copy from * @return this builder */ - HeaderSpec cookies(MultiValueMap cookies); + S cookies(MultiValueMap cookies); /** * Set the value of the {@code If-Modified-Since} header. @@ -355,14 +344,14 @@ public interface WebClient { * @param ifModifiedSince the new value of the header * @return this builder */ - HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince); + S ifModifiedSince(ZonedDateTime ifModifiedSince); /** * Set the values of the {@code If-None-Match} header. * @param ifNoneMatches the new value of the header * @return this builder */ - HeaderSpec ifNoneMatch(String... ifNoneMatches); + S ifNoneMatch(String... ifNoneMatches); /** * Add the given, single header value under the given name. @@ -370,40 +359,75 @@ public interface WebClient { * @param headerValues the header value(s) * @return this builder */ - HeaderSpec header(String headerName, String... headerValues); + S header(String headerName, String... headerValues); /** * Copy the given headers into the entity's headers map. * @param headers the existing headers to copy from * @return this builder */ - HeaderSpec headers(HttpHeaders headers); + S headers(HttpHeaders headers); /** - * Perform the request without a request body. + * Exchange the built request for a delayed {@code ClientResponse}. * @return a {@code Mono} with the response */ Mono exchange(); + } + + interface RequestBodySpec extends RequestHeadersSpec { + /** - * Set the body of the request to the given {@code BodyInserter} and - * perform the request. + * Set the length of the body in bytes, as specified by the + * {@code Content-Length} header. + * @param contentLength the content length + * @return this builder + * @see HttpHeaders#setContentLength(long) + */ + RequestBodySpec contentLength(long contentLength); + + /** + * Set the {@linkplain MediaType media type} of the body, as specified + * by the {@code Content-Type} header. + * @param contentType the content type + * @return this builder + * @see HttpHeaders#setContentType(MediaType) + */ + RequestBodySpec contentType(MediaType contentType); + + /** + * Set the body of the request to the given {@code BodyInserter}. * @param inserter the {@code BodyInserter} that writes to the request * @param the type contained in the body - * @return a {@code Mono} with the response + * @return this builder */ - Mono exchange(BodyInserter inserter); + RequestHeadersSpec body(BodyInserter inserter); /** - * Set the body of the request to the given {@code Publisher} and - * perform the request. + * Set the body of the request to the given {@code Publisher}. + *

This method is a convenient shortcut for {@link #body(BodyInserter)} with a + * {@linkplain org.springframework.web.reactive.function.BodyInserters#fromPublisher} + * Publisher body inserter}. * @param publisher the {@code Publisher} to write to the request * @param elementClass the class of elements contained in the publisher * @param the type of the elements contained in the publisher * @param the type of the {@code Publisher} - * @return a {@code Mono} with the response + * @return this builder */ - > Mono exchange(S publisher, Class elementClass); + > RequestHeadersSpec body(S publisher, Class elementClass); + + /** + * Set the body of the request to the given {@code Object}. + *

This method is a convenient shortcut for {@link #body(BodyInserter)} with a + * {@linkplain org.springframework.web.reactive.function.BodyInserters#fromObject + * Object body inserter}. + * @param body the {@code Object} to write to the request + * @param the type contained in the body + * @return this builder + */ + RequestHeadersSpec body(T body); + } } diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt index 3018c605fa..1b41cfaf0b 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt @@ -1,13 +1,29 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.reactive.function.client import org.reactivestreams.Publisher /** - * Extension for [WebClient.HeaderSpec.exchange] providing a variant without explicit class + * Extension for [WebClient.RequestHeadersSpec.exchangePublisher] providing a variant without explicit class * parameter thanks to Kotlin reified type parameters. * * @author Sebastien Deleuze * @since 5.0 */ -inline fun > WebClient.HeaderSpec.exchange(publisher: S) = - exchange(publisher, T::class.java) +inline fun > WebClient.RequestBodySpec.body(publisher: S) = + body(publisher, T::class.java) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index c3945a2d72..8c6e34694c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -35,8 +35,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.Pojo; -import static org.junit.Assert.*; -import static org.springframework.web.reactive.function.BodyInserters.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; /** * Integration tests using a {@link ExchangeFunction} through {@link WebClient}. @@ -188,7 +188,8 @@ public class WebClientIntegrationTests { .uri("/pojo/capitalize") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .exchange(fromObject(new Pojo("foofoo", "barbar"))) + .body(new Pojo("foofoo", "barbar")) + .exchange() .then(response -> response.bodyToMono(Pojo.class)); StepVerifier.create(result)