The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:
Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
1. Overview
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.
2. REST API Endpoints
To start, letβs define a sample REST API with the following GET endpoints:
- /products β get all products
- /products/{id} β get product by ID
- /products/{id}/attributes/{attributeId} β get product attribute by id
- /products/?name={name}&deliveryDate={deliveryDate}&color={color} β find products
- /products/?tag[]={tag1}&tag[]={tag2} β get products by tags
- /products/?category={category1}&category={category2} β get products by categories
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.
3. WebClient Setup
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.
4. URI Path Component
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.
5. URI Query Parameters
Usually, a query parameter is a simple key-value pair like title=Baeldung. Letβs see how to build such URIs.
5.1. Single Value Parameters
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.
5.2. Array Parameters
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.
6. Encoding Mode
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:
- TEMPLATE_AND_VALUES: Pre-encode the URI template and strictly encode URI variables when expanded
- VALUES_ONLY: Do not encode the URI template, but strictly encode URI variables after expanding them into the template
- URI_COMPONENTS: Encode URI component value after expending URI variables
- NONE: No encoding will be applied
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.
7. Conclusion
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.
