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

@@ -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<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 {
}
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 {
}
@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 {
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 {
}
@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<String, String> cookies) {
public DefaultRequestBodySpec cookies(MultiValueMap<String, String> 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 <T> RequestHeadersSpec<?> body(BodyInserter<T, ? super ClientHttpRequest> inserter) {
this.inserter = inserter;
return this;
}
@Override
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 Mono<ClientResponse> exchange() {
ClientRequest request = this.initRequestBuilder().build();
return exchangeFunction.exchange(request);
}
@Override
public <T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
ClientRequest request = this.initRequestBuilder().body(inserter).build();
return exchangeFunction.exchange(request);
}
ClientRequest request = this.inserter != null ?
initRequestBuilder().body(this.inserter).build() :
initRequestBuilder().build();
@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();
return exchangeFunction.exchange(request);
}

View File

@@ -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<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 {
/**
* 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 {
* @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<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 {
* @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<ClientResponse> exchange();
/**
* Set the body of the request to the given {@code BodyInserter} and
* perform the request.
* @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
*/
<T> Mono<ClientResponse> exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
}
interface RequestBodySpec extends RequestHeadersSpec<RequestBodySpec> {
/**
* Set the body of the request to the given {@code Publisher} 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 this builder
*/
<T> RequestHeadersSpec<?> body(BodyInserter<T, ? super ClientHttpRequest> inserter);
/**
* 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);
}
}

View File

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

View File

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