1. Introduction
If you’ve ever been looking for a client to perform HTTP requests in your Spring Boot application you’ve probably run into the RestTemplate or reactive WebClient.
According to the official documentation RestTemplate class is in the maintenance mode since Spring 5 and we should consider using WebClient which is a part of the Spring WebFlux module. But what if we would like to use it in a standard Spring MVC application without migrating to Spring WebFlux?
In this article, we’ll walk you through the process of setup and consuming external APIs using WebClient with Spring MVC and Kotlin.
2. What is WebClient?
As we’ve mentioned in the introduction- WebClient is a non-blocking, reactive client, with which making the calls becomes really easy. It is a part of the spring-webflux module, which we need to add to our project to use it.
3. Imports
Let’s start with adding the required dependencies to our existing Spring Boot project:
implementation("org.springframework.boot:spring-boot-starter-webflux:2.3.1.RELEASE")
4. Create a Configuration Class
Secondly, let’s implement our configuration class with WebClient’s bean, which we’ll be able to inject to our components later:
@Configuration class WebClientConfiguration { @Bean fun webClient(builder: WebClient.Builder): WebClient = builder .baseUrl("localhost:8080") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build() }
As you can see, we’re using WebClient.Builder which allows us to configure our client’s default values (which will be common for all requests- like base url or default header) in a clean way. If you’d like to see the whole list of things we can configure here, please visit the documentation.
Make a real progress thanks to practical examples, exercises, and quizzes.
- 64 written lessons
- 62 quizzes with a total of 269 questions
- Dedicated Facebook Support Group
- 30-DAYS Refund Guarantee
5. Make Requests and Map Responses to Objects
Let start with an example API endpoint under http://localhost:8080/api/hello address returning such a JSON object:
{ "message": "Hello world", "timestamp": "2020-07-23T10:10:10.273174" }
Secondly, let’s prepare a data class corresponding to our JSON, and let’s implement example service:
data class ExampleResponse( val message: String, val timestamp: LocalDateTime ) @Component class ExampleClient( private val webClient: WebClient ) { // our functions will go here }
And that’s all we need to start working with our API endpoints.
5.1. Fetch the Data Using Retrieve() Function
Let’s start simply by building a function performing HTTP GET request and retrieving the response body:
fun retrieveExampleResponse(): ExampleResponse? = webClient.get() .uri { it.pathSegment("api", "hello").build() } .retrieve() .bodyToMono(ExampleResponse::class.java) .block()
As you might have noticed, we are using bodyToMono() function which extracts the body into a Mono and then we are subscribing to this Mono using block(). Please remember that block() function returns null if the Mono completes empty so the returning type of the function will be nullable ExampleResponse.
5.2. Fetching the Data Using Exchange() Function
We can also use exchange() method which returns ClientResponse with the response status and headers instead:
fun exchangeExampleResponse(): ResponseEntity<ExampleResponse>? = webClient.get() .uri { it.pathSegment("api", "hello").build() } .exchange() .flatMap { println("Raw status code: ${it.rawStatusCode()}") it.toEntity(ExampleResponse::class.java) } .block()
5.3. What Is the Difference Between the Exchange() and Retrieve() Functions?
According to the documentation, retrieve() is a shortcut to using exchange() and decoding the response body through ClientResponse. Generally speaking, we should prefer using retrieve() unless we have a good reason to use exchange() which returns a Mono of ClientResponse allowing us to check the status codes or headers.
5.4. Fetch the Array Of Objects
If the endpoint we are working with returns the array of objects instead of a single object, we can use another variant of bodyToMono() function which takes a type reference describing the expected response body element type as a parameter:
inline fun <reified T> typeReference() = object : ParameterizedTypeReference<T>() {} fun retrieveExampleResponseList(): List<ExampleResponse>? = webClient.get() .uri { it.pathSegment("api", "hello-list").build() } .retrieve() .bodyToMono(typeReference<List<ExampleResponse>>()) .block()
Please notice, that reified type parameters allow us to simplify our code. Thanks to them we will not have to write object expression object : ParameterizedTypeReference<T>() {} each time.
5.5. Call Secured Endpoints
In case we would like to perform a request on a secured endpoint we can use one of the HttpHeaders functions to set the value of the Authorization header, for example, to Basic Authentication based on the given username and password:
webClient.get() .uri { it.pathSegment("api", "secured", "bearer", "hello").build() } .headers { it.setBasicAuth(username, password) } .retrieve() .bodyToMono(ExampleResponse::class.java) .block()
or to the given Bearer token:
webClient.get() .uri { it.pathSegment("api", "secured", "hello").build() } .headers { it.setBearerAuth(token) } .retrieve() .bodyToMono(ExampleResponse::class.java) .block()
For more information about HttpHeaders class please refer to the documentation.
6. Send the Request Body
Sometimes we will need to set the request body in order to perform the action on a specific endpoint. RequestBodySpec interface contains several variants of the body() method. In this article, we will use BodyInserter to send the JSON or x-www-form-urlencoded request body.
6.1. Send JSON Request Body
Let’s start with HTTP POST request. To set the body we will use fromValue() function which allows us to pass the object of any type as a parameter:
webClient.post() .uri { it.pathSegment("api", "hello").build() } .body(BodyInserters.fromValue(requestBody)) .retrieve() .bodyToMono(ExampleResponse::class.java) .block()
6.2. Send x-www-form-urlencoded Request Body
Similarly, we will do for x-www-form-urlencoded request, but this time we will use fromFormData() which return a FormInserter to write the given key-value pair as URL-encoded form-data:
webClient.post() .uri { it.pathSegment("api", "hello").build() } .body(BodyInserters.fromFormData(name, value)) .retrieve() .bodyToMono(ExampleResponse::class.java) .block()
7. Create filters
Sometimes we would like to intercept our outgoing requests or incoming responses to perform some action or validation on each request/response. Let’s get back to our WebClient builder and add two functions invocations (we will implement them in the next chapters):
builder .filter(requestLoggerFilter()) .filter(responseLoggerFilter())
7.1. Create Request Logger
Let’s implement our filter which will log the HTTP method and target URL for each request:
fun requestLoggerFilter() = ExchangeFilterFunction.ofRequestProcessor { logger.info("Logging request: ${it.method()} ${it.url()}") Mono.just(it) }
As you can see we are using ofRequestProcessor() static method of ExchangeFilterFunction which allows us to pass the request processor to it. Processor is a Function with ClientRequest class as an input and Mono<ClientRequest> as an output.
7.2. Create Response logger
Finally, let’s implement a filter which will log the status code of each incoming response:
fun responseLoggerFilter() = ExchangeFilterFunction.ofResponseProcessor { logger.info("Response status code: ${it.statusCode()}") Mono.just(it) }
This time the processor is a Function with ClientResponse class as an input and Mono<ClientResponse> as an output.
8. Conclusion
In this post, we’ve learned how easy it is to use WebClient with Spring MVC and Kotlin. We’ve covered the most common use cases, like fetching the responses and sending the request body. For the full source code please head to this project on GitHub.
Moreover, if you may want to check out my article about the WebClient alternative- Retrofit.
And finally, if you enjoyed the article or have any comments, suggestions, or questions, please let us know in the comments below, by our Facebook page or group, or by using our contact form.