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
This commit is contained in:
Arjen Poutsma
2017-03-29 11:51:51 -04:00
committed by Rossen Stoyanchev
parent cbd98d5247
commit 118f33aeda
7 changed files with 293 additions and 207 deletions

View File

@@ -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<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 {
}
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) {

View File

@@ -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 {
/**
* 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 {
* 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<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 {
* @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<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 {
* @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<RequestBodySpec> {
/**
* Perform the exchange with the body for the request populated using
* a {@link BodyInserter}.
* 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);
/**
* 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);
}
/**