
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: May 11, 2024
A lot of frameworks and projects are introducing reactive programming and asynchronous request handling. As such, Spring 5 introduced a reactive WebClient implementation as part of the WebFlux framework.
In this tutorial, we’ll learn how to reactively consume REST API endpoints with WebClient.
To start, let’s define a sample REST API with the following GET endpoints:
Here we defined a few different URIs. In just a moment, we’ll figure out how to build and send each type of URI with WebClient.
Please note that the URIs for gettings products by tags and categories contain arrays as query parameters; however, the syntax differs because there’s no strict definition of how arrays should be represented in URIs. This primarily depends on the server-side implementation. Accordingly, we’ll cover both cases.
First, we’ll need to create an instance of WebClient. For this article, we’ll be using a mocked object to verify that a valid URI is requested.
Let’s define the client and related mock objects:
exchangeFunction = mock(ExchangeFunction.class);
ClientResponse mockResponse = mock(ClientResponse.class);
when(mockResponse.bodyToMono(String.class))
.thenReturn(Mono.just("test"));
when(exchangeFunction.exchange(argumentCaptor.capture()))
.thenReturn(Mono.just(mockResponse));
webClient = WebClient
.builder()
.baseUrl("https://example.com/api")
.exchangeFunction(exchangeFunction)
.build();
We’ll also pass a base URL that will be prepended to all requests made by the client.
Finally, to verify that a particular URI has been passed to the underlying ExchangeFunction instance, we’ll use the following helper method:
private void verifyCalledUrl(String relativeUrl) {
ClientRequest request = argumentCaptor.getValue();
assertEquals(String.format("%s%s", BASE_URL, relativeUrl), request.url().toString());
verify(this.exchangeFunction).exchange(request);
verifyNoMoreInteractions(this.exchangeFunction);
}
The WebClientBuilder class has the uri() method that provides the UriBuilder instance as an argument. Generally, we make an API call in the following manner:
webClient.get()
.uri(uriBuilder -> uriBuilder
//... building a URI
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
We’ll use UriBuilder extensively in this guide to construct URIs. It’s worth noting that we can build a URI using other methods, and then just pass the generated URI as a String.
A path component consists of a sequence of path segments separated by a slash ( / ). First, we’ll start with a simple case where a URI doesn’t have any variable segments, /products:
webClient.get()
.uri("/products")
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products");
For this case, we can just pass a String as an argument.
Next, we’ll take the /products/{id} endpoint and build the corresponding URI:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/{id}")
.build(2))
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/2");
From the code above, we can see that the actual segment values are passed to the build() method.
In a similar way, we can create a URI with multiple path segments for the /products/{id}/attributes/{attributeId} endpoint:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/{id}/attributes/{attributeId}")
.build(2, 13))
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/2/attributes/13");
A URI can have as many path segments as required, though the final URI length must not exceed limitations. Finally, we need to remember to keep the right order of actual segment values passed to the build() method.
Usually, a query parameter is a simple key-value pair like title=Baeldung. Let’s see how to build such URIs.
We’ll start with single value parameters and take the /products/?name={name}&deliveryDate={deliveryDate}&color={color} endpoint. To set a query parameter, we’ll call the queryParam() method of the UriBuilder interface:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.queryParam("deliveryDate", "13/04/2019")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");
Here we added three query parameters and assigned actual values immediately. Conversely, it’s also possible to leave placeholders instead of exact values:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "{title}")
.queryParam("color", "{authorId}")
.queryParam("deliveryDate", "{date}")
.build("AndroidPhone", "black", "13/04/2019"))
.retrieve()
.bodyToMono(String.class)
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13%2F04%2F2019");
This might be especially helpful when passing a builder object further in a chain.
Note that there’s one important difference between the two code snippets above. With attention to the expected URIs, we can see that they’re encoded differently. Particularly, the slash character ( / ) was escaped in the last example.
Generally speaking, RFC3986 doesn’t require the encoding of slashes in the query; however, some server-side applications might require such conversion. Therefore, we’ll see how to change this behavior later in this guide.
We might need to pass an array of values, and there aren’t strict rules for passing arrays in a query string. Therefore, an array representation in a query string differs from project to project, and usually depends on underlying frameworks. We’ll cover the most widely used formats in this article.
Let’s start with the /products/?tag[]={tag1}&tag[]={tag2} endpoint:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("tag[]", "Snapdragon", "NFC")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?tag%5B%5D=Snapdragon&tag%5B%5D=NFC");
As we can see, the final URI contains multiple tag parameters, followed by encoded square brackets. The queryParam() method accepts variable arguments as values, so there’s no need to call the method several times.
Alternatively, we can omit square brackets and just pass multiple query parameters with the same key, but different values, /products/?category={category1}&category={category2}:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("category", "Phones", "Tablets")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?category=Phones&category=Tablets");
Finally, there’s one more extensively-used method to encode an array, which is to pass comma-separated values. Let’s transform our previous example into comma-separated values:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("category", String.join(",", "Phones", "Tablets"))
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?category=Phones,Tablets");
We’re just using the join() method of the String class to create a comma-separated string. We can also use any other delimiter that’s expected by the application.
Remember how we previously mentioned URL encoding?
If the default behavior doesn’t fit our requirements, we can change it. We need to provide a UriBuilderFactory implementation while building a WebClient instance. In this case, we’ll use the DefaultUriBuilderFactory class. To set encoding, we’ll call the setEncodingMode() method. The following modes are available:
The default value is TEMPLATE_AND_VALUES. Let’s set the mode to URI_COMPONENTS:
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT);
webClient = WebClient
.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.exchangeFunction(exchangeFunction)
.build();
As a result, the following assertion will succeed:
webClient.get()
.uri(uriBuilder - > uriBuilder
.path("/products/")
.queryParam("name", "AndroidPhone")
.queryParam("color", "black")
.queryParam("deliveryDate", "13/04/2019")
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.empty())
.block();
verifyCalledUrl("/products/?name=AndroidPhone&color=black&deliveryDate=13/04/2019");
And, of course, we can provide a completely custom UriBuilderFactory implementation to handle URI creation manually.
In this article, we learned how to build different types of URIs using WebClient and DefaultUriBuilder.
Along the way, we covered various types and formats of query parameters. Finally, we wrapped up by changing the default encoding mode of the URL builder.