Skip to content

Latest commit

 

History

History
512 lines (384 loc) · 14.8 KB

File metadata and controls

512 lines (384 loc) · 14.8 KB

WebClient

Spring WebFlux includes a reactive, non-blocking WebClient for performing HTTP requests using a functional-style API that exposes Reactor Flux and Mono types, see web-reactive.adoc. The client relies on the same codecs that WebFlux server applications use to work with request and response content.

Internally WebClient delegates to an HTTP client library. By default it uses Reactor Netty, there is built-in support for the Jetty reactive HtpClient, and others can be plugged in through a ClientHttpConnector.

Configuration

The simplest way to create a WebClient is through one of the static factory methods:

  • WebClient.create()

  • WebClient.create(String baseUrl)

The above uses Reactor Netty HttpClient from "io.projectreactor.netty:reactor-netty" with default settings and participates in global resources such for event loop threads and a connection pool, see Reactor Netty configuration.

The WebClient.Builder can be used for access to further options:

  • uriBuilderFactory — customized UriBuilderFactory to use as a base URL.

  • defaultHeader — headers for every request.

  • defaultCookie) — cookies for every request.

  • defaultRequest — Consumer to customize every request.

  • filter — client filter for every request.

  • exchangeStrategies — HTTP message reader/writer customizations.

  • clientConnector — HTTP client library settings.

For example, to configure HTTP codecs:

	ExchangeStrategies strategies = ExchangeStrategies.builder()
			.codecs(configurer -> {
				// ...
			})
			.build();

	WebClient client = WebClient.builder()
			.exchangeStrategies(strategies)
			.build();

Once built a WebClient instance is immutable. However, you can clone it, and build a modified copy without affecting the original instance:

	WebClient client1 = WebClient.builder()
			.filter(filterA).filter(filterB).build();

	WebClient client2 = client1.mutate()
			.filter(filterC).filter(filterD).build();

	// client1 has filterA, filterB

	// client2 has filterA, filterB, filterC, filterD

Reactor Netty

To customize Reactor Netty settings:

	HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
	ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);

	WebClient webClient = WebClient.builder().clientConnector(connector).build();

By default HttpClient participates in the global Reactor Netty resources held in reactor.netty.http.HttpResources, including event loop threads and a connection pool. This is the recommended mode since fixed, shared resources are preferred for event loop concurrency. In this mode global resources remain active until the process exits.

If the server is timed with the process, there is typically no need for an explicit shutdown. However if the server can start or stop in-process, e.g. Spring MVC application deployed as a WAR, you can declare a Spring-managed bean of type ReactorResourceFactory with useGlobalResources=true (the default) to ensure the Reactor Netty global resources are shut down when the Spring ApplicationContext is closed:

	@Bean
	public ReactorResourceFactory reactorResourceFactory() {
		return new ReactorResourceFactory();
	}

You may also choose not to participate in the global Reactor Netty resources. However keep in mind in this mode the burden is on you to ensure all Reactor Netty client and server instances use shared resources:

	@Bean
	public ReactorResourceFactory resourceFactory() {
		ReactorResourceFactory factory = new ReactorResourceFactory();
		factory.setGlobalResources(false); // (1)
		return factory;
	}

	@Bean
	public WebClient webClient() {

		Function<HttpClient, HttpClient> mapper = client -> {
			// Further customizations...
		};

		ClientHttpConnector connector =
				new ReactorClientHttpConnector(resourceFactory(), mapper); // (2)

		return WebClient.builder().clientConnector(connector).build(); // (3)
	}
  1. Create resources independent of global ones.

  2. Use ReactorClientHttpConnector constructor with resource factory.

  3. Plug the connector into the WebClient.Builder.

Jetty

To customize Jetty HttpClient settings:

	HttpClient httpClient = new HttpClient();
	httpClient.setCookieStore(...);
	ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);

	WebClient webClient = WebClient.builder().clientConnector(connector).build();

By default HttpClient creates its own resources (Executor, ByteBufferPool, Scheduler) which remain active until the process exits or stop() is called.

You can share resources between multiple intances of Jetty client (and server) and ensure the resources are shut down when the Spring ApplicationContext is closed by declaring a Spring-managed bean of type JettyResourceFactory:

	@Bean
	public JettyResourceFactory resourceFactory() {
		return new JettyResourceFactory();
	}

	@Bean
	public WebClient webClient() {

		Consumer<HttpClient> customizer = client -> {
			// Further customizations...
		};

		ClientHttpConnector connector =
				new JettyClientHttpConnector(resourceFactory(), customizer); // (2)

		return WebClient.builder().clientConnector(connector).build(); // (3)
	}
  1. Create shared resources.

  2. Use JettyClientHttpConnector constructor with resource factory.

  3. Plug the connector into the WebClient.Builder.

Retrieve

The retrieve() method is the easiest way to get a response body and decode it:

	WebClient client = WebClient.create("http://example.org");

	Mono<Person> result = client.get()
			.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
			.retrieve()
			.bodyToMono(Person.class);

You can also get a stream of objects decoded from the response:

	Flux<Quote> result = client.get()
			.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
			.retrieve()
			.bodyToFlux(Quote.class);

By default, responses with 4xx or 5xx status codes result in an WebClientResponseException or one of its HTTP status specific sub-classes such as WebClientResponseException.BadRequest, WebClientResponseException.NotFound, and others. You can also use the onStatus method to customize the resulting exception:

	Mono<Person> result = client.get()
			.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
			.retrieve()
			.onStatus(HttpStatus::is4xxServerError, response -> ...)
			.onStatus(HttpStatus::is5xxServerError, response -> ...)
			.bodyToMono(Person.class);

Exchange

The exchange() method provides more control. The below example is equivalent to retrieve() but also provides access to the ClientResponse:

	Mono<Person> result = client.get()
			.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
			.exchange()
			.flatMap(response -> response.bodyToMono(Person.class));

At this level you can also create a full ResponseEntity:

	Mono<ResponseEntity<Person>> result = client.get()
			.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
			.exchange()
			.flatMap(response -> response.toEntity(Person.class));

Note that unlike retrieve(), with exchange() there are no automatic error signals for 4xx and 5xx responses. You have to check the status code and decide how to proceed.

Caution

When using exchange() you must always use any of the body or toEntity methods of ClientResponse to ensure resources are released and to avoid potential issues with HTTP connection pooling. You can use bodyToMono(Void.class) if no response content is expected. However keep in mind that if the response does have content, the connection will be closed and will not be placed back in the pool.

Request body

The request body can be encoded from an Object:

	Mono<Person> personMono = ... ;

	Mono<Void> result = client.post()
			.uri("/persons/{id}", id)
			.contentType(MediaType.APPLICATION_JSON)
			.body(personMono, Person.class)
			.retrieve()
			.bodyToMono(Void.class);

You can also have a stream of objects encoded:

	Flux<Person> personFlux = ... ;

	Mono<Void> result = client.post()
			.uri("/persons/{id}", id)
			.contentType(MediaType.APPLICATION_STREAM_JSON)
			.body(personFlux, Person.class)
			.retrieve()
			.bodyToMono(Void.class);

Or if you have the actual value, use the syncBody shortcut method:

	Person person = ... ;

	Mono<Void> result = client.post()
			.uri("/persons/{id}", id)
			.contentType(MediaType.APPLICATION_JSON)
			.syncBody(person)
			.retrieve()
			.bodyToMono(Void.class);

Form data

To send form data, provide a MultiValueMap<String, String> as the body. Note that the content is automatically set to "application/x-www-form-urlencoded" by the FormHttpMessageWriter:

	MultiValueMap<String, String> formData = ... ;

	Mono<Void> result = client.post()
			.uri("/path", id)
			.syncBody(formData)
			.retrieve()
			.bodyToMono(Void.class);

You can also supply form data in-line via BodyInserters:

	import static org.springframework.web.reactive.function.BodyInserters.*;

	Mono<Void> result = client.post()
			.uri("/path", id)
			.body(fromFormData("k1", "v1").with("k2", "v2"))
			.retrieve()
			.bodyToMono(Void.class);

Multipart data

To send multipart data, you need to provide a MultiValueMap<String, ?> whose values are either Objects representing part content, or HttpEntity representing the content and headers for a part. MultipartBodyBuilder provides a convenient API to prepare a multipart request:

	MultipartBodyBuilder builder = new MultipartBodyBuilder();
	builder.part("fieldPart", "fieldValue");
	builder.part("filePart", new FileSystemResource("...logo.png"));
	builder.part("jsonPart", new Person("Jason"));

	MultiValueMap<String, HttpEntity<?>> parts = builder.build();

In most cases you do not have to specify the Content-Type for each part. The content type is determined automatically based on the HttpMessageWriter chosen to serialize it, or in the case of a Resource based on the file extension. If necessary you can explicitly provide the MediaType to use for each part through one fo the overloaded builder part methods.

Once a MultiValueMap is prepared, the easiest way to pass it to the the WebClient is through the syncBody method:

	MultipartBodyBuilder builder = ...;

	Mono<Void> result = client.post()
			.uri("/path", id)
			.syncBody(builder.build())
			.retrieve()
			.bodyToMono(Void.class);

If the MultiValueMap contains at least one non-String value, which could also be represent regular form data (i.e. "application/x-www-form-urlencoded"), you don’t have to set the Content-Type to "multipart/form-data". This is always the case when using MultipartBodyBuilder which ensures an HttpEntity wrapper.

As an alternative to MultipartBodyBuilder, you can also provide multipart content, inline-style, through the built-in BodyInserters. For example:

	import static org.springframework.web.reactive.function.BodyInserters.*;

	Mono<Void> result = client.post()
			.uri("/path", id)
			.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
			.retrieve()
			.bodyToMono(Void.class);

Client Filters

You can register a client filter (ExchangeFilterFunction) through the WebClient.Builder in order to intercept and/or modify requests:

WebClient client = WebClient.builder()
		.filter((request, next) -> {

			ClientRequest filtered = ClientRequest.from(request)
					.header("foo", "bar")
					.build();

			return next.exchange(filtered);
		})
		.build();

This can be used for cross-cutting concerns such as authentication. The example below uses a filter for basic authentication through a static factory method:

// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = WebClient.builder()
		.filter(basicAuthentication("user", "password"))
		.build();

Filters apply globally to every request. To change how a filter’s behavior for a specific request, you can add request attributes to the ClientRequest that can then be accessed by all filters in the chain:

WebClient client = WebClient.builder()
		.filter((request, next) -> {
			Optional<Object> usr = request.attribute("myAttribute");
			// ...
		})
		.build();

client.get().uri("http://example.org/")
		.attribute("myAttribute", "...")
		.retrieve()
		.bodyToMono(Void.class);

	}

You can also replicate an existing WebClient, and insert new filters or remove already registered filters. In the example below, a basic authentication filter is inserted at index 0:

// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = webClient.mutate()
		.filters(filterList -> {
			filterList.add(0, basicAuthentication("user", "password"));
		})
		.build();

Testing

To test code that uses the WebClient, you can use a mock web server such as the OkHttp MockWebServer. To see example use, check WebClientIntegrationTests in the Spring Framework tests, or the static-server sample in the OkHttp repository.