Browse Source

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
pull/1358/head
Arjen Poutsma 8 years ago committed by Rossen Stoyanchev
parent
commit
118f33aeda
  1. 127
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
  2. 101
      spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
  3. 5
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java
  4. 104
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java
  5. 120
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java
  6. 22
      spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt
  7. 7
      spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java

127
spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

@ -34,6 +34,7 @@ import reactor.core.publisher.Mono; @@ -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 { @@ -97,42 +98,45 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public UriSpec get() {
return toUriSpec(WebClient::get);
public UriSpec<RequestHeadersSpec<?>> get() {
return toUriSpec(wc -> wc.method(HttpMethod.GET));
}
@Override
public UriSpec head() {
return toUriSpec(WebClient::head);
public UriSpec<RequestHeadersSpec<?>> head() {
return toUriSpec(wc -> wc.method(HttpMethod.HEAD));
}
@Override
public UriSpec post() {
return toUriSpec(WebClient::post);
public UriSpec<RequestBodySpec> post() {
return toUriSpec(wc -> wc.method(HttpMethod.POST));
}
@Override
public UriSpec put() {
return toUriSpec(WebClient::put);
public UriSpec<RequestBodySpec> put() {
return toUriSpec(wc -> wc.method(HttpMethod.PUT));
}
@Override
public UriSpec patch() {
return toUriSpec(WebClient::patch);
public UriSpec<RequestBodySpec> patch() {
return toUriSpec(wc -> wc.method(HttpMethod.PATCH));
}
@Override
public UriSpec delete() {
return toUriSpec(WebClient::delete);
public UriSpec<RequestHeadersSpec<?>> delete() {
return toUriSpec(wc -> wc.method(HttpMethod.DELETE));
}
@Override
public UriSpec options() {
return toUriSpec(WebClient::options);
public UriSpec<RequestHeadersSpec<?>> options() {
return toUriSpec(wc -> wc.method(HttpMethod.OPTIONS));
}
private UriSpec toUriSpec(Function<WebClient, WebClient.UriSpec> function) {
return new DefaultUriSpec(function.apply(this.webClient));
@SuppressWarnings("unchecked")
private <S extends RequestHeadersSpec<?>> UriSpec<S> toUriSpec(
Function<WebClient, WebClient.UriSpec<WebClient.RequestBodySpec>> function) {
return new DefaultUriSpec<>(function.apply(this.webClient));
}
@ -156,123 +160,132 @@ class DefaultWebTestClient implements WebTestClient { @@ -156,123 +160,132 @@ class DefaultWebTestClient implements WebTestClient {
}
private class DefaultUriSpec implements UriSpec {
@SuppressWarnings("unchecked")
private class DefaultUriSpec<S extends RequestHeadersSpec<?>> implements UriSpec<S> {
private final WebClient.UriSpec uriSpec;
private final WebClient.UriSpec<WebClient.RequestBodySpec> uriSpec;
DefaultUriSpec(WebClient.UriSpec spec) {
DefaultUriSpec(WebClient.UriSpec<WebClient.RequestBodySpec> 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<String, ?> uriVariables) {
return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables));
public S uri(String uriTemplate, Map<String, ?> uriVariables) {
return (S) new DefaultRequestBodySpec(this.uriSpec.uri(uriTemplate, uriVariables));
}
@Override
public HeaderSpec uri(Function<UriBuilder, URI> uriBuilder) {
return new DefaultHeaderSpec(this.uriSpec.uri(uriBuilder));
public S uri(Function<UriBuilder, URI> 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<String, String> cookies) {
this.headerSpec.cookies(cookies);
public RequestBodySpec cookies(MultiValueMap<String, String> 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 <T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
return toResponseSpec(this.headerSpec.exchange(inserter));
public <T> RequestHeadersSpec<?> body(BodyInserter<T, ? super ClientHttpRequest> inserter) {
this.bodySpec.body(inserter);
return this;
}
@Override
public <T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass) {
return toResponseSpec(this.headerSpec.exchange(publisher, elementClass));
public <T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass) {
this.bodySpec.body(publisher, elementClass);
return this;
}
@Override
public <T> RequestHeadersSpec<?> body(T body) {
this.bodySpec.body(body);
return this;
}
private DefaultResponseSpec toResponseSpec(Mono<ClientResponse> mono) {

101
spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java

@ -78,43 +78,43 @@ public interface WebTestClient { @@ -78,43 +78,43 @@ public interface WebTestClient {
* Prepare an HTTP GET request.
* @return a spec for specifying the target URL
*/
UriSpec get();
UriSpec<RequestHeadersSpec<?>> get();
/**
* Prepare an HTTP HEAD request.
* @return a spec for specifying the target URL
*/
UriSpec head();
UriSpec<RequestHeadersSpec<?>> head();
/**
* Prepare an HTTP POST request.
* @return a spec for specifying the target URL
*/
UriSpec post();
UriSpec<RequestBodySpec> post();
/**
* Prepare an HTTP PUT request.
* @return a spec for specifying the target URL
*/
UriSpec put();
UriSpec<RequestBodySpec> put();
/**
* Prepare an HTTP PATCH request.
* @return a spec for specifying the target URL
*/
UriSpec patch();
UriSpec<RequestBodySpec> patch();
/**
* Prepare an HTTP DELETE request.
* @return a spec for specifying the target URL
*/
UriSpec delete();
UriSpec<RequestHeadersSpec<?>> delete();
/**
* Prepare an HTTP OPTIONS request.
* @return a spec for specifying the target URL
*/
UriSpec options();
UriSpec<RequestHeadersSpec<?>> options();
/**
@ -327,13 +327,13 @@ public interface WebTestClient { @@ -327,13 +327,13 @@ public interface WebTestClient {
/**
* Specification for providing the URI of a request.
*/
interface UriSpec {
interface UriSpec<S extends RequestHeadersSpec<?>> {
/**
* 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 { @@ -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 { @@ -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<String, ?> uriVariables);
S uri(String uri, Map<String, ?> 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<UriBuilder, URI> uriFunction);
S uri(Function<UriBuilder, URI> uriFunction);
}
/**
* Specification for adding request headers and performing an exchange.
*/
interface HeaderSpec {
interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
/**
* Set the list of acceptable {@linkplain MediaType media types}, as
@ -371,7 +371,7 @@ public interface WebTestClient { @@ -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 { @@ -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 { @@ -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 { @@ -413,7 +395,7 @@ public interface WebTestClient {
* @param cookies the existing cookies to copy from
* @return the same instance
*/
HeaderSpec cookies(MultiValueMap<String, String> cookies);
S cookies(MultiValueMap<String, String> cookies);
/**
* Set the value of the {@code If-Modified-Since} header.
@ -422,14 +404,14 @@ public interface WebTestClient { @@ -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 { @@ -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 { @@ -452,26 +434,55 @@ public interface WebTestClient {
*/
ResponseSpec exchange();
}
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
/**
* 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 <T> the body type, or the the element type (for a stream)
* @return spec for decoding the response
* @see org.springframework.web.reactive.function.BodyInserters
*/
<T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
<T> RequestHeadersSpec<?> body(BodyInserter<T, ? super ClientHttpRequest> 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 <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return spec for decoding the response
*/
<T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass);
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> 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 <T> the type contained in the body
* @return a {@code Mono} with the response
*/
<T> RequestHeadersSpec<?> body(T body);
}
/**

5
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; @@ -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 { @@ -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();

104
spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

@ -36,6 +36,7 @@ import org.springframework.util.CollectionUtils; @@ -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 { @@ -70,42 +71,48 @@ class DefaultWebClient implements WebClient {
@Override
public UriSpec get() {
return method(HttpMethod.GET);
public UriSpec<RequestHeadersSpec<?>> get() {
return methodInternal(HttpMethod.GET);
}
@Override
public UriSpec head() {
return method(HttpMethod.HEAD);
public UriSpec<RequestHeadersSpec<?>> head() {
return methodInternal(HttpMethod.HEAD);
}
@Override
public UriSpec post() {
return method(HttpMethod.POST);
public UriSpec<RequestBodySpec> post() {
return methodInternal(HttpMethod.POST);
}
@Override
public UriSpec put() {
return method(HttpMethod.PUT);
public UriSpec<RequestBodySpec> put() {
return methodInternal(HttpMethod.PUT);
}
@Override
public UriSpec patch() {
return method(HttpMethod.PATCH);
public UriSpec<RequestBodySpec> patch() {
return methodInternal(HttpMethod.PATCH);
}
@Override
public UriSpec delete() {
return method(HttpMethod.DELETE);
public UriSpec<RequestHeadersSpec<?>> delete() {
return methodInternal(HttpMethod.DELETE);
}
@Override
public UriSpec options() {
return method(HttpMethod.OPTIONS);
public UriSpec<RequestHeadersSpec<?>> options() {
return methodInternal(HttpMethod.OPTIONS);
}
private UriSpec method(HttpMethod httpMethod) {
return new DefaultUriSpec(httpMethod);
@Override
public UriSpec<RequestBodySpec> method(HttpMethod httpMethod) {
return methodInternal(httpMethod);
}
@SuppressWarnings("unchecked")
private <S extends RequestHeadersSpec<?>> UriSpec<S> methodInternal(HttpMethod httpMethod) {
return new DefaultUriSpec<>(httpMethod);
}
@Override
@ -116,7 +123,7 @@ class DefaultWebClient implements WebClient { @@ -116,7 +123,7 @@ class DefaultWebClient implements WebClient {
}
private class DefaultUriSpec implements UriSpec {
private class DefaultUriSpec<S extends RequestHeadersSpec<?>> implements UriSpec<S> {
private final HttpMethod httpMethod;
@ -126,28 +133,29 @@ class DefaultWebClient implements WebClient { @@ -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<String, ?> uriVariables) {
public S uri(String uriTemplate, Map<String, ?> uriVariables) {
return uri(uriBuilderFactory.expand(uriTemplate, uriVariables));
}
@Override
public HeaderSpec uri(Function<UriBuilder, URI> uriFunction) {
public S uri(Function<UriBuilder, URI> 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 { @@ -157,7 +165,9 @@ class DefaultWebClient implements WebClient {
private MultiValueMap<String, String> cookies;
DefaultHeaderSpec(HttpMethod httpMethod, URI uri) {
private BodyInserter<?, ? super ClientHttpRequest> inserter;
DefaultRequestBodySpec(HttpMethod httpMethod, URI uri) {
this.httpMethod = httpMethod;
this.uri = uri;
}
@ -177,7 +187,7 @@ class DefaultWebClient implements WebClient { @@ -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 { @@ -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 { @@ -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<String, String> cookies) {
public DefaultRequestBodySpec cookies(MultiValueMap<String, String> cookies) {
if (cookies != null) {
getCookies().putAll(cookies);
}
@ -231,7 +241,7 @@ class DefaultWebClient implements WebClient { @@ -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 { @@ -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<ClientResponse> exchange() {
ClientRequest request = this.initRequestBuilder().build();
return exchangeFunction.exchange(request);
public <T> RequestHeadersSpec<?> body(BodyInserter<T, ? super ClientHttpRequest> inserter) {
this.inserter = inserter;
return this;
}
@Override
public <T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
ClientRequest request = this.initRequestBuilder().body(inserter).build();
return exchangeFunction.exchange(request);
public <T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass) {
this.inserter = BodyInserters.fromPublisher(publisher, elementClass);
return this;
}
@Override
public <T> RequestHeadersSpec<?> body(T body) {
this.inserter = BodyInserters.fromObject(body);
return this;
}
@Override
public <T, S extends Publisher<T>> Mono<ClientResponse> exchange(S publisher, Class<T> elementClass) {
ClientRequest request = initRequestBuilder().headers(this.headers).body(publisher, elementClass).build();
public Mono<ClientResponse> exchange() {
ClientRequest request = this.inserter != null ?
initRequestBuilder().body(this.inserter).build() :
initRequestBuilder().build();
return exchangeFunction.exchange(request);
}

120
spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java

@ -26,6 +26,7 @@ import org.reactivestreams.Publisher; @@ -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 { @@ -58,43 +59,49 @@ public interface WebClient {
* Prepare an HTTP GET request.
* @return a spec for specifying the target URL
*/
UriSpec get();
UriSpec<RequestHeadersSpec<?>> get();
/**
* Prepare an HTTP HEAD request.
* @return a spec for specifying the target URL
*/
UriSpec head();
UriSpec<RequestHeadersSpec<?>> head();
/**
* Prepare an HTTP POST request.
* @return a spec for specifying the target URL
*/
UriSpec post();
UriSpec<RequestBodySpec> post();
/**
* Prepare an HTTP PUT request.
* @return a spec for specifying the target URL
*/
UriSpec put();
UriSpec<RequestBodySpec> put();
/**
* Prepare an HTTP PATCH request.
* @return a spec for specifying the target URL
*/
UriSpec patch();
UriSpec<RequestBodySpec> patch();
/**
* Prepare an HTTP DELETE request.
* @return a spec for specifying the target URL
*/
UriSpec delete();
UriSpec<RequestHeadersSpec<?>> delete();
/**
* Prepare an HTTP OPTIONS request.
* @return a spec for specifying the target URL
*/
UriSpec options();
UriSpec<RequestHeadersSpec<?>> options();
/**
* Prepare a request for the specified {@code HttpMethod}.
* @return a spec for specifying the target URL
*/
UriSpec<RequestBodySpec> method(HttpMethod method);
/**
@ -266,38 +273,38 @@ public interface WebClient { @@ -266,38 +273,38 @@ public interface WebClient {
/**
* Contract for specifying the URI for a request.
*/
interface UriSpec {
interface UriSpec<S extends RequestHeadersSpec<?>> {
/**
* 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<String, ?> uriVariables);
S uri(String uri, Map<String, ?> uriVariables);
/**
* Build the URI for the request using the {@link UriBuilderFactory}
* configured for this client.
*/
HeaderSpec uri(Function<UriBuilder, URI> uriFunction);
S uri(Function<UriBuilder, URI> uriFunction);
}
/**
* Contract for specifying request headers leading up to the exchange.
*/
interface HeaderSpec {
interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
/**
* Set the list of acceptable {@linkplain MediaType media types}, as
@ -305,7 +312,7 @@ public interface WebClient { @@ -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 { @@ -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 { @@ -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<String, String> cookies);
S cookies(MultiValueMap<String, String> cookies);
/**
* Set the value of the {@code If-Modified-Since} header.
@ -355,14 +344,14 @@ public interface WebClient { @@ -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 { @@ -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<ClientResponse> exchange();
}
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
/**
* 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 <T> the type contained in the body
* @return a {@code Mono} with the response
* @return this builder
*/
<T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
<T> RequestHeadersSpec<?> body(BodyInserter<T, ? super ClientHttpRequest> 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}.
* <p>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 <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return a {@code Mono} with the response
* @return this builder
*/
<T, S extends Publisher<T>> Mono<ClientResponse> exchange(S publisher, Class<T> elementClass);
<T, S extends Publisher<T>> RequestHeadersSpec<?> body(S publisher, Class<T> elementClass);
/**
* Set the body of the request to the given {@code Object}.
* <p>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 <T> the type contained in the body
* @return this builder
*/
<T> RequestHeadersSpec<?> body(T body);
}
}

22
spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt

@ -1,13 +1,29 @@ @@ -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 <reified T : Any, S : Publisher<T>> WebClient.HeaderSpec.exchange(publisher: S) =
exchange(publisher, T::class.java)
inline fun <reified T : Any, S : Publisher<T>> WebClient.RequestBodySpec.body(publisher: S) =
body(publisher, T::class.java)

7
spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java

@ -35,8 +35,8 @@ import org.springframework.http.HttpStatus; @@ -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 { @@ -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)

Loading…
Cancel
Save