Spring Boot WebClient with request logging

RestTemplate is dead and everybody should use WebClient now. That’s what you find everywhere so I had a look how to configure an instance of WebClient properly:

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.http.codec.ClientCodecConfigurer
import org.springframework.http.codec.json.Jackson2JsonDecoder
import org.springframework.http.codec.json.Jackson2JsonEncoder
import org.springframework.web.reactive.function.client.ExchangeStrategies
import org.springframework.web.reactive.function.client.WebClient

@Configuration
class WebClientConfig(
    private val jsonMapper: ObjectMapper,
) {

    @Bean
    @ConditionalOnMissingBean
    fun genericWebClient(): WebClient {
        return WebClient.builder()
            .exchangeStrategies(ExchangeStrategies
                .builder()
                .codecs { clientDefaultCodecsConfigurer: ClientCodecConfigurer ->
                    clientDefaultCodecsConfigurer.defaultCodecs()
                        .jackson2JsonEncoder(Jackson2JsonEncoder(jsonMapper, MediaType.APPLICATION_JSON))
                    clientDefaultCodecsConfigurer.defaultCodecs()
                        .jackson2JsonDecoder(Jackson2JsonDecoder(jsonMapper, MediaType.APPLICATION_JSON))
                }.build()
            )
            .filter(WebClientLoggerBuilder.logRequest())
            .build()
    }
}

This defines a bean genericWebClient with Spring Boot configured Jackson ObjectMapper instance.

Here is also the code for the WebClientLoggerBuilder:

import org.springframework.web.reactive.function.client.ClientRequest
import org.springframework.web.reactive.function.client.ExchangeFilterFunction
import reactor.core.publisher.Mono
import java.util.function.Consumer
import org.slf4j.Logger
import org.slf4j.LoggerFactory

object WebClientLoggerBuilder {

    private val log: Logger = LoggerFactory.getLogger(javaClass)

    fun logRequest(): ExchangeFilterFunction {
        return ExchangeFilterFunction.ofRequestProcessor { clientRequest: ClientRequest ->
            if (log.isDebugEnabled) {
                val sb = StringBuilder("Request: \n")
                    .append(clientRequest.method())
                    .append(" ")
                    .append(clientRequest.url())
                    .append("\n")
                clientRequest.headers()
                    .forEach { name: String?, values: List<String?> ->
                        values.forEach(
                            Consumer { value: String? -> sb.append(name).append(": ").append(value).append("\n") })
                    }
                log.debug(sb.toString())
            }
            Mono.just(clientRequest)
        }
    }
}

This simply logs method, path and headers of a request if debug log is enabled and added to the WebClient. Executing a blocking call that parses JSON to a list of objects is then quite easy:

fun <T> queryListWithoutBody(
    path: String,
    listClass: Class<T>,
    params: MultiValueMap<String, String>
): List<T> {
    return webClient.get()
        .uri { it.path(path).queryParams(params).build() }
        .retrieve()
        .bodyToFlux(listClass)
        .collectList()
        .block() ?: emptyList()
}

With this code you’re able to replace RestTemplate without going reactive. Adding spring-boot-starter-webflux also won’t enable netty if you also have good old spring-boot-starter-web in the classpath.