Codersee

Spring 5 WebClient with Spring Boot

A featured image for category: Spring

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.

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.

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.

Leave a Reply

Your email address will not be published.

Categories

Author

Piotr Wolak

Piotr Wolak

Founder Of Codersee

Join Newsletter And Get 2 FREE EBOOKS

Image shows the covers of free ebooks accessible for newsletter subscribers.

Join the FREE weekly newsletter and get two free eBooks:

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy.

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.