This commit is contained in:
Rossen Stoyanchev 2018-05-26 09:24:23 -04:00
parent 3ede3a4b34
commit 2acf91a438
8 changed files with 183 additions and 127 deletions

View File

@ -443,8 +443,8 @@ public interface WebTestClient {
Builder responseTimeout(Duration timeout);
/**
* Shortcut for pre-packaged customizations to WebTestClient builder.
* @param configurer the configurer to apply
* Apply the given {@code Consumer} to this builder instance.
* <p>This can be useful for applying pre-packaged customizations.
*/
Builder apply(WebTestClientConfigurer configurer);

View File

@ -23,6 +23,33 @@ import org.springframework.core.codec.Encoder;
* Extension of {@link CodecConfigurer} for HTTP message reader and writer
* options relevant on the client side.
*
* <p>HTTP message readers for the following are registered by default:
* <ul>{@code byte[]}
* <li>{@link java.nio.ByteBuffer}
* <li>{@link org.springframework.core.io.buffer.DataBuffer DataBuffer}
* <li>{@link org.springframework.core.io.Resource Resource}
* <li>{@link String}
* <li>{@link org.springframework.util.MultiValueMap
* MultiValueMap&lt;String,String&gt;} for form data
* <li>JSON and Smile, if Jackson is present
* <li>XML, if JAXB2 is present
* <li>Server-Sent Events
* </ul>
*
* <p>HTTP message writers registered by default:
* <ul>{@code byte[]}
* <li>{@link java.nio.ByteBuffer}
* <li>{@link org.springframework.core.io.buffer.DataBuffer DataBuffer}
* <li>{@link org.springframework.core.io.Resource Resource}
* <li>{@link String}
* <li>{@link org.springframework.util.MultiValueMap
* MultiValueMap&lt;String,String&gt;} for form data
* <li>{@link org.springframework.util.MultiValueMap
* MultiValueMap&lt;String,Object&gt;} for multipart data
* <li>JSON and Smile, if Jackson is present
* <li>XML, if JAXB2 is present
* </ul>
*
* @author Rossen Stoyanchev
* @since 5.0
*/

View File

@ -22,6 +22,33 @@ import org.springframework.core.codec.Encoder;
* Extension of {@link CodecConfigurer} for HTTP message reader and writer
* options relevant on the server side.
*
* <p>HTTP message readers for the following are registered by default:
* <ul>{@code byte[]}
* <li>{@link java.nio.ByteBuffer}
* <li>{@link org.springframework.core.io.buffer.DataBuffer DataBuffer}
* <li>{@link org.springframework.core.io.Resource Resource}
* <li>{@link String}
* <li>{@link org.springframework.util.MultiValueMap
* MultiValueMap&lt;String,String&gt;} for form data
* <li>{@link org.springframework.util.MultiValueMap
* MultiValueMap&lt;String,Object&gt;} for multipart data
* <li>JSON and Smile, if Jackson is present
* <li>XML, if JAXB2 is present
* </ul>
*
* <p>HTTP message writers registered by default:
* <ul>{@code byte[]}
* <li>{@link java.nio.ByteBuffer}
* <li>{@link org.springframework.core.io.buffer.DataBuffer DataBuffer}
* <li>{@link org.springframework.core.io.Resource Resource}
* <li>{@link String}
* <li>{@link org.springframework.util.MultiValueMap
* MultiValueMap&lt;String,String&gt;} for form data
* <li>JSON and Smile, if Jackson is present
* <li>XML, if JAXB2 is present
* <li>Server-Sent Events
* </ul>
*
* @author Rossen Stoyanchev
* @since 5.0
*/

View File

@ -40,8 +40,7 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Implementations of {@link BodyInserter} that write various bodies, such a reactive streams,
* server-sent events, resources, etc.
* Static factory methods for {@link BodyInserter} implementations.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev

View File

@ -33,6 +33,15 @@ import org.springframework.http.codec.HttpMessageWriter;
*/
final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Builder {
final static ExchangeStrategies DEFAULT_EXCHANGE_STRATEGIES;
static {
DefaultExchangeStrategiesBuilder builder = new DefaultExchangeStrategiesBuilder();
builder.defaultConfiguration();
DEFAULT_EXCHANGE_STRATEGIES = builder.build();
}
private final ClientCodecConfigurer codecConfigurer = ClientCodecConfigurer.create();
@ -53,36 +62,36 @@ final class DefaultExchangeStrategiesBuilder implements ExchangeStrategies.Build
@Override
public ExchangeStrategies build() {
return new DefaultExchangeStrategies(this.codecConfigurer.getReaders(),
this.codecConfigurer.getWriters());
return new DefaultExchangeStrategies(
this.codecConfigurer.getReaders(), this.codecConfigurer.getWriters());
}
private static class DefaultExchangeStrategies implements ExchangeStrategies {
private final List<HttpMessageReader<?>> messageReaders;
private final List<HttpMessageReader<?>> readers;
private final List<HttpMessageWriter<?>> messageWriters;
private final List<HttpMessageWriter<?>> writers;
public DefaultExchangeStrategies(
List<HttpMessageReader<?>> messageReaders, List<HttpMessageWriter<?>> messageWriters) {
this.messageReaders = unmodifiableCopy(messageReaders);
this.messageWriters = unmodifiableCopy(messageWriters);
public DefaultExchangeStrategies(List<HttpMessageReader<?>> readers, List<HttpMessageWriter<?>> writers) {
this.readers = unmodifiableCopy(readers);
this.writers = unmodifiableCopy(writers);
}
private static <T> List<T> unmodifiableCopy(List<? extends T> list) {
return Collections.unmodifiableList(new ArrayList<>(list));
}
@Override
public List<HttpMessageReader<?>> messageReaders() {
return this.messageReaders;
return this.readers;
}
@Override
public List<HttpMessageWriter<?>> messageWriters() {
return this.messageWriters;
return this.writers;
}
}

View File

@ -16,18 +16,19 @@
package org.springframework.web.reactive.function.client;
import java.net.URI;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.util.Assert;
/**
* Exposes request-response exchange functionality, such as to
* {@linkplain #create(ClientHttpConnector) create} an {@code ExchangeFunction} given a
* {@code ClientHttpConnector}.
* Static factory methods to create an {@link ExchangeFunction}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
@ -39,24 +40,25 @@ public abstract class ExchangeFunctions {
/**
* Create a new {@link ExchangeFunction} with the given connector. This method uses
* {@linkplain ExchangeStrategies#withDefaults() default strategies}.
* @param connector the connector to create connections
* @return the created function
* Create an {@code ExchangeFunction} with the given {@code ClientHttpConnector}.
* This is the same as calling
* {@link #create(ClientHttpConnector, ExchangeStrategies)} and passing
* {@link ExchangeStrategies#withDefaults()}.
* @param connector the connector to use for connecting to servers
* @return the created {@code ExchangeFunction}
*/
public static ExchangeFunction create(ClientHttpConnector connector) {
return create(connector, ExchangeStrategies.withDefaults());
}
/**
* Create a new {@link ExchangeFunction} with the given connector and strategies.
* @param connector the connector to create connections
* @param strategies the strategies to use
* @return the created function
* Create an {@code ExchangeFunction} with the given
* {@code ClientHttpConnector} and {@code ExchangeStrategies}.
* @param connector the connector to use for connecting to servers
* @param strategies the {@code ExchangeStrategies} to use
* @return the created {@code ExchangeFunction}
*/
public static ExchangeFunction create(ClientHttpConnector connector, ExchangeStrategies strategies) {
Assert.notNull(connector, "ClientHttpConnector must not be null");
Assert.notNull(strategies, "ExchangeStrategies must not be null");
return new DefaultExchangeFunction(connector, strategies);
}
@ -67,26 +69,33 @@ public abstract class ExchangeFunctions {
private final ExchangeStrategies strategies;
public DefaultExchangeFunction(ClientHttpConnector connector, ExchangeStrategies strategies) {
Assert.notNull(connector, "ClientHttpConnector must not be null");
Assert.notNull(strategies, "ExchangeStrategies must not be null");
this.connector = connector;
this.strategies = strategies;
}
@Override
public Mono<ClientResponse> exchange(ClientRequest request) {
Assert.notNull(request, "ClientRequest must not be null");
HttpMethod httpMethod = request.method();
URI url = request.url();
return this.connector
.connect(request.method(), request.url(),
clientHttpRequest -> request.writeTo(clientHttpRequest, this.strategies))
.connect(httpMethod, url, httpRequest -> request.writeTo(httpRequest, this.strategies))
.doOnSubscribe(subscription -> logger.debug("Subscriber present"))
.doOnRequest(n -> logger.debug("Demand signaled"))
.doOnCancel(() -> logger.debug("Cancelling request"))
.map(response -> {
if (logger.isDebugEnabled()) {
int status = response.getRawStatusCode();
HttpStatus resolvedStatus = HttpStatus.resolve(status);
logger.debug("Response received, status: " + status +
(resolvedStatus != null ? " " + resolvedStatus.getReasonPhrase() : ""));
int code = response.getRawStatusCode();
HttpStatus status = HttpStatus.resolve(code);
String reason = status != null ? " " + status.getReasonPhrase() : "";
logger.debug("Response received, status: " + code + reason);
}
return new DefaultClientResponse(response, this.strategies);
});

View File

@ -24,10 +24,10 @@ import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
/**
* Defines the strategies for invoking {@link ExchangeFunction}s. An instance of
* this class is immutable; instances are typically created through the mutable {@link Builder}:
* either through {@link #builder()} to set up default strategies, or {@link #empty()} to start
* from scratch.
* Provides strategies for use in an {@link ExchangeFunction}.
*
* <p>To create an instance, see the static methods {@link #withDefaults()},
* {@link #builder()}, and {@link #empty()}.
*
* @author Brian Clozel
* @author Arjen Poutsma
@ -36,13 +36,13 @@ import org.springframework.http.codec.HttpMessageWriter;
public interface ExchangeStrategies {
/**
* Return the {@link HttpMessageReader}s to be used for request body conversion.
* Return {@link HttpMessageReader}s to read and decode the response body with.
* @return the stream of message readers
*/
List<HttpMessageReader<?>> messageReaders();
/**
* Return the {@link HttpMessageWriter}s to be used for response body conversion.
* Return {@link HttpMessageWriter}s to write and encode the request body with.
* @return the stream of message writers
*/
List<HttpMessageWriter<?>> messageWriters();
@ -51,16 +51,17 @@ public interface ExchangeStrategies {
// Static methods
/**
* Return a new {@code ExchangeStrategies} with default initialization.
* @return the new {@code ExchangeStrategies}
* Return a new {@code ExchangeStrategies} with default configuration
* provided by {@link ClientCodecConfigurer}.
*/
static ExchangeStrategies withDefaults() {
return builder().build();
return DefaultExchangeStrategiesBuilder.DEFAULT_EXCHANGE_STRATEGIES;
}
/**
* Return a mutable builder for a {@code ExchangeStrategies} with default initialization.
* @return the builder
* Return a builder pre-configured with default configuration to start.
* This is the same as {@link #withDefaults()} but returns a mutable builder
* for further customizations.
*/
static Builder builder() {
DefaultExchangeStrategiesBuilder builder = new DefaultExchangeStrategiesBuilder();
@ -69,8 +70,7 @@ public interface ExchangeStrategies {
}
/**
* Return a mutable, empty builder for a {@code ExchangeStrategies}.
* @return the builder
* Return a builder with empty configuration to start.
*/
static Builder empty() {
return new DefaultExchangeStrategiesBuilder();

View File

@ -52,13 +52,17 @@ import org.springframework.web.util.UriBuilderFactory;
* {@link #create(String)} or obtain a {@link WebClient#builder()} to create an
* instance.
*
* <p>For examples with a response body see
* {@link RequestHeadersSpec#retrieve() retrieve()} and
* {@link RequestHeadersSpec#exchange() exchange()}.
* For examples with a request body see
* {@link RequestBodySpec#body(Publisher, Class) body(Publisher,Class)},
* {@link RequestBodySpec#syncBody(Object) syncBody(Object)}, and
* {@link RequestBodySpec#body(BodyInserter) body(BodyInserter)}.
* <p>For examples with a response body, see the Javadoc for:
* <ul>
* <li>{@link RequestHeadersSpec#retrieve() retrieve()}
* <li>{@link RequestHeadersSpec#exchange() exchange()}
* </ul>
* For examples with a request body see:
* <ul>
* <li>{@link RequestBodySpec#body(Publisher, Class) body(Publisher,Class)}
* <li>{@link RequestBodySpec#syncBody(Object) syncBody(Object)}
* <li>{@link RequestBodySpec#body(BodyInserter) body(BodyInserter)}
* </ul>
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
@ -67,57 +71,57 @@ import org.springframework.web.util.UriBuilderFactory;
public interface WebClient {
/**
* Prepare an HTTP GET request.
* Start building an HTTP GET request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> get();
/**
* Prepare an HTTP HEAD request.
* Start building an HTTP HEAD request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> head();
/**
* Prepare an HTTP POST request.
* Start building an HTTP POST request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec post();
/**
* Prepare an HTTP PUT request.
* Start building an HTTP PUT request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec put();
/**
* Prepare an HTTP PATCH request.
* Start building an HTTP PATCH request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec patch();
/**
* Prepare an HTTP DELETE request.
* Start building an HTTP DELETE request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> delete();
/**
* Prepare an HTTP OPTIONS request.
* Start building an HTTP OPTIONS request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec<?> options();
/**
* Prepare a request for the specified {@code HttpMethod}.
* Start building a request for the given {@code HttpMethod}.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec method(HttpMethod method);
/**
* Return a builder for a new {@code WebClient} with properties replicated
* from the current {@code WebClient} instance, but without affecting it.
* Return a builder to create a new {@code WebClient} whose settings are
* replicated from the current {@code WebClient}.
*/
Builder mutate();
@ -125,7 +129,7 @@ public interface WebClient {
// Static, factory methods
/**
* Create a new {@code WebClient} with a Reactor Netty connector.
* Create a new {@code WebClient} with Reactor Netty by default.
* @see #create(String)
* @see #builder()
*/
@ -134,7 +138,7 @@ public interface WebClient {
}
/**
* A variant of {@link #create()} that accepts a default base URL. For more
* Variant of {@link #create()} that accepts a default base URL. For more
* details see {@link Builder#baseUrl(String) Builder.baseUrl(String)}.
* @param baseUrl the base URI for all requests
* @see #builder()
@ -161,27 +165,25 @@ public interface WebClient {
*
* <p>For example given base URL "http://abc.com/v1":
* <p><pre class="code">
* Mono&#060;Account&#062; result = client.get()
* .uri("/accounts/{id}", 43)
* .exchange()
* .then(response -> response.bodyToMono(Account.class));
* Mono&#060;Account&#062; result = client.get().uri("/accounts/{id}", 43)
* .retrieve()
* .bodyToMono(Account.class);
*
* // Result: http://abc.com/v1/accounts/43
*
* Flux&#060;Account&#062; result = client.get()
* .uri(builder -> builder.path("/accounts").queryParam("q", "12").build())
* .exchange()
* .then(response -> response.bodyToFlux(Account.class));
* .retrieve()
* .bodyToFlux(Account.class);
*
* // Result: http://abc.com/v1/accounts?q=12
* </pre>
*
* <p>The base URL can be overridden with an absolute URI:
* <pre class="code">
* Mono&#060;Account&#062; result = client.get()
* .uri("http://xyz.com/path")
* .exchange()
* .then(response -> response.bodyToMono(Account.class));
* Mono&#060;Account&#062; result = client.get().uri("http://xyz.com/path")
* .retrieve()
* .bodyToMono(Account.class);
*
* // Result: http://xyz.com/path
* </pre>
@ -190,8 +192,8 @@ public interface WebClient {
* <pre class="code">
* Flux&#060;Account&#062; result = client.get()
* .uri(builder -> builder.replacePath("/v2/accounts").queryParam("q", "12").build())
* .exchange()
* .then(response -> response.bodyToFlux(Account.class));
* .retrieve()
* .bodyToFlux(Account.class);
*
* // Result: http://abc.com/v2/accounts?q=12
* </pre>
@ -232,12 +234,9 @@ public interface WebClient {
/**
* Manipulate the default headers with the given consumer. The
* headers provided to the consumer are "live", so that the consumer can be used to
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
* {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other
* {@link HttpHeaders} methods.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return this builder
* headers provided to the consumer are "live", so that the consumer
* can be used to overwrite or remove existing values.
* @param headersConsumer the headers consumer
*/
Builder defaultHeaders(Consumer<HttpHeaders> headersConsumer);
@ -250,28 +249,12 @@ public interface WebClient {
/**
* Manipulate the default cookies with the given consumer. The
* map provided to the consumer is "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
* {@link MultiValueMap} methods.
* cookies provided to the consumer are "live", so that the consumer
* can be used to overwrite or remove existing values.
* @param cookiesConsumer a function that consumes the cookies map
* @return this builder
*/
Builder defaultCookies(Consumer<MultiValueMap<String, String>> cookiesConsumer);
/**
* Configure the {@link ClientHttpConnector} to use.
* <p>By default an instance of
* {@link org.springframework.http.client.reactive.ReactorClientHttpConnector
* ReactorClientHttpConnector} is created if this is not set. However a
* shared instance may be passed instead, e.g. for use with multiple
* {@code WebClient}'s targeting different base URIs.
* @param connector the connector to use
* @see #exchangeStrategies(ExchangeStrategies)
* @see #exchangeFunction(ExchangeFunction)
*/
Builder clientConnector(ClientHttpConnector connector);
/**
* Add the given filter to the filter chain.
* @param filter the filter to be added to the chain
@ -279,8 +262,8 @@ public interface WebClient {
Builder filter(ExchangeFilterFunction filter);
/**
* Manipulate the filters with the given consumer. The
* list provided to the consumer is "live", so that the consumer can be used to remove
* Manipulate the filters with the given consumer. The list provided to
* the consumer is "live", so that the consumer can be used to remove
* filters, change ordering, etc.
* @param filtersConsumer a function that consumes the filter list
* @return this builder
@ -288,34 +271,40 @@ public interface WebClient {
Builder filters(Consumer<List<ExchangeFilterFunction>> filtersConsumer);
/**
* Provide a pre-configured {@link ExchangeFunction} instance. This is
* an alternative to and effectively overrides the following:
* <ul>
* <li>{@link #clientConnector(ClientHttpConnector)}
* <li>{@link #exchangeStrategies(ExchangeStrategies)}.
* </ul>
* @param exchangeFunction the exchange function to use
* @see #clientConnector(ClientHttpConnector)
* @see #exchangeStrategies(ExchangeStrategies)
* Configure the {@link ClientHttpConnector} to use. This is useful for
* plugging in and/or customizing options of the underlying HTTP client
* library (e.g. SSL).
* <p>By default this is set to
* {@link org.springframework.http.client.reactive.ReactorClientHttpConnector
* ReactorClientHttpConnector}.
* @param connector the connector to use
*/
Builder exchangeFunction(ExchangeFunction exchangeFunction);
Builder clientConnector(ClientHttpConnector connector);
/**
* Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
* <p>By default this is obtained from {@link ExchangeStrategies#withDefaults()}.
* @param strategies the strategies to use
* @see #clientConnector(ClientHttpConnector)
* @see #exchangeFunction(ExchangeFunction)
*/
Builder exchangeStrategies(ExchangeStrategies strategies);
/**
* Provide an {@link ExchangeFunction} pre-configured with
* {@link ClientHttpConnector} and {@link ExchangeStrategies}.
* <p>This is an alternative to, and effectively overrides
* {@link #clientConnector}, and {@link #exchangeStrategies}.
* @param exchangeFunction the exchange function to use
*/
Builder exchangeFunction(ExchangeFunction exchangeFunction);
/**
* Clone this {@code WebClient.Builder}
*/
Builder clone();
/**
* Shortcut for pre-packaged customizations to WebTest builder.
* Apply the given {@code Consumer} to this builder instance.
* <p>This can be useful for applying pre-packaged customizations.
* @param builderConsumer the consumer to apply
*/
Builder apply(Consumer<Builder> builderConsumer);
@ -390,11 +379,9 @@ public interface WebClient {
S cookie(String name, String value);
/**
* Manipulate the request's cookies with the given consumer. The
* map provided to the consumer is "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
* {@link MultiValueMap} methods.
* Manipulate the default cookies with the given consumer. The
* cookies provided to the consumer are "live", so that the consumer
* can be used to overwrite or remove existing values.
* @param cookiesConsumer a function that consumes the cookies map
* @return this builder
*/
@ -425,11 +412,9 @@ public interface WebClient {
S header(String headerName, String... headerValues);
/**
* Manipulate the request's headers with the given consumer. The
* headers provided to the consumer are "live", so that the consumer can be used to
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
* {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other
* {@link HttpHeaders} methods.
* Manipulate the default headers with the given consumer. The
* headers provided to the consumer are "live", so that the consumer
* can be used to overwrite or remove existing values.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return this builder
*/