Chega de RestTemplate, Conheça o WebClient

Chega de RestTemplate, Conheça o WebClient

A nova abordagem recomendada pelo Spring para fazer chamadas de API de forma reativa ou bloqueante

Introdução

Estava conversando com um desenvolvedor java sobre o seu projeto, que tinha a necessidade chamar uma API e persistir o retorno dela mas não precisava do seu retorno para seguir com a jornada... existem diversas formas de codificar isso, como isolando a responsabilidade para outro serviço, abrir uma Thread(essa foi a opção que ele tinha escolhido), utilizar um Event interno, entre muitas outras opções....

Então perguntei se ele conhecia o WebClient, pois ele atende exatamente essa necessidade, e obtive como resposta o seguinte:

"Sim, realmente ele ia suprir muito bem isso, mas ele é só para aplicações reativas."

Aí que foi onde ele cometeu o erro... o WebClient funciona para ambas aplicações e tipos de requisições, seja ela reativa e não bloqueante como o tradicional bloqueante. Tanto que o próprio Spring que até a sua versão 4 recomendava o uso do RestTemplate, agora com a chegada da versão 5 recomenda o uso do WebClient.

Bom, agora que já sabem que o WebClient é o cara, vamos ver como usamos ele? 😎

WebClient

⚠Não vou explorar muito o lado Flux neste artigo e sim o bloqueante e reativo, mas depois pretendo sim criar um apenas para o lado Flux. ⚠

Dependência

Para usar a WebClient, devemos ter o módulo spring-boot-starter-webflux importado para o projeto.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Configurando o WebClient

Para criar um WebClient podemos seguir qualquer uma das abordagens a seguir.

  • Builder do próprio Spring em nosso Bean
@Bean
public WebClient consultaCep(WebClient.Builder builder) {
        return builder.baseUrl("https://viacep.com.br/ws/").build();
}
  • Método create sem url base ou com.
var c1 = WebClient.create();

var c2 = WebClient.create("https://viacep.com.br/ws/");
  • Builder próprio
    (neste aqui estou customizando suas configurações de timeout)
var http = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
        .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(10)).addHandlerLast(new WriteTimeoutHandler(10)));

var connector = new ReactorClientHttpConnector(http.wiretap(true));

var wclient = WebClient
                .builder()
                .baseUrl("http://localhost:8080")
                .clientConnector(connector)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();

Métodos HTTP e seu Payload de saída

Para enviar uma solicitação, podemos usar seus métodos fluentes pré-criados, method(HttpMethod) como get(), put(), post(), delete() e até patch().

webClient.method(HttpMethod.GET);

webClient.post();

se a requisição tiver um corpo a ser enviado basta chamar .body e utilizar o Mono.just(Object) ou o Mono.justOrEmpty(Object) / Mono.justOrEmpty(Optional) para converter seu objeto de entrada.

Recomendo fortemente que utilize o Mono.justOrEmpty que ele ira evitar alguma possível quebra durante a conversão por estar Null e olha só que legal né... ele suporta nosso querido Optional !

webClient
.post()
.body(Mono.justOrEmpty(Optional.of("17015311")), String.class)

Definindo a rota da requisição e seus cabeçalhos

A gente já viu que desde o momento da criação podemos já definir a rota que será consumida ou um trecho dela para podermos complementar depois.

Aqui eu já tenho um webClient configurado com a baseUrl dele como "localhost:8080" e preciso chamar "localhost:8080/cep/{cep}"

Podemos juntar o trecho que falta "/cep/{cep}" utilizando o String formatted para atribuir o valor da minha variável a String e se tornar nossa URI e para adicionar um cabeçalho na requisições é só chamar .header("Key","Value")

var cep = "17015311"
webClient
.method(HttpMethod.GET)
.uri("/cep/%s".formatted(cep))
.header("Content-Type", "application/json")

Outra forma de fazermos isso é usando o uriBuilder (bem melhor 😜)

var cep = "17015311"
webClient
.get()
.uri(uriBuilder -> uriBuilder.path("/cep/{cep}")
      .queryParam("callback", "content")
      .build(cep))
.header("Content-Type", "application/json")

Definindo o que iremos consumir do servidor

Existem dois tipos de consumo no WebClient o retrieve() e o exchange().

O retrieve() executa diretamente a solicitação HTTP e recupera o corpo da resposta.

O exchange() método retorna ClientResponse com o status da resposta, os cabeçalhos e o corpo.
(também tem o exchangeToMono e exchangeToFlux como sub implementações dele para facilitar o uso)

no próximo tópico vou mostrar como são usados.

Definindo o formato da resposta retornada do servidor.

Depois de escolhermos qual vai ser a forma da nossa reposta precisamos definir qual objeto será a nossa resposta e para esta função temos o bodyToMono como o nome já diz a resposta esperada é única, e o bodyToFlux tem como resposta um fluxo de partes de resposta.

Mono<CepDTO> flux = consultaCep
                .get()
                .uri(uriBuilder -> uriBuilder.path("/{cep}/json/").build(cep))
                .retrieve()
                .bodyToMono(CepDTO.class)
                .block();
Flux<CepDTO> flux = consultaCep
                .get()
                .uri(uriBuilder -> uriBuilder.path("/{cep}/json/").build(cep))
                .exchangeToFlux(response -> {
                    if (response.statusCode().equals(HttpStatus.OK)) {
                        return response.bodyToFlux(CepDTO.class);
                    } else {
                        return response.createException().flatMapMany(Mono::error);
                    }
                });

Mono<List<CepDTO>> fluxToMonoCollection = consultaCep
                .get()
                .uri(uriBuilder -> uriBuilder.path("/{cep}/json/").build(cep))
                .retrieve()
                .bodyToFlux(CepDTO.class)
                .collectList();

Não esta esperando nenhum conteúdo ? utilize Void.class

.bodyToMono(Void.class);

Esta esperando uma List de objeto como resposta? utilize .collectList();

List<CepDTO> monoList = consultaCep
                .get()
                .uri(uriBuilder -> uriBuilder.path("/{cep}/json/").build(cep))
                .retrieve()
                .bodyToMono(CepDTO.class)
                .collectList();
                .block();

Observe que os métodos bodyToMono() e bodyToFlux() sempre esperam um corpo de resposta de um determinado tipo de classe.
Se o código de status da resposta for 4xx (erro do cliente) ou 5xx (erro do servidor), ou seja, não houver corpo de resposta, esses métodos lançarão WebClientException.

Realizando uma requisição de forma reativa e não bloqueante

Para tornar nossa request reativa precisamos chamar o "subscribe" e com isso conseguimos implementar a nossa logica para manipular a resposta obtida mesmo depois depois do fim da execução do método onde a chamada foi implementada.

//logica...

webClient
.get()
.uri(uriBuilder -> uriBuilder.path("/{cep}/json/").build(cep))
.retrieve()
.bodyToMono(String.class)
.subscribe(i -> {
    System.out.println("A request retornou agora ;) ");
});

//logica...
return "ok";

Bloqueando a requisição (esperando ela responder)

Para tornar a nossa requisição bloqueante como é no padrão do RestTemplate, feign e jax-rs ... precisamos implementar o .block 🙃

vamos pegar de exemplo a chamada reativa que fizemos ali em cima, remover o subscribe e adicionar o trecho .block

//logica...

return webClient
.get()
.uri(uriBuilder -> uriBuilder.path("/{cep}/json/").build(cep))
.retrieve()
.bodyToMono(String.class)
.block();

Desta forma dizemos para nossa aplicação que precisamos aguardar a conclusão desta requisição para seguirmos com o resto da logica implementada ou até mesmo retornar ela em nosso método. ✨

Tratando Exceções

Enfim a parte mais importante da nossa implementação, não podíamos deixar de fora a forma de como tratamos as exceções conforme cada status code né.

Pra isso temos 2 formas, o onStatus e o filter

  • O onStatus utilizamos direto em nossa chamada, informamos qual vai ser o status code de nossa validação e em seguida declaramos um response function, convertemos o seu payload para String e usamos "map" assim com é no Java Stream para transformar o valor em uma exceção, aqui no caso utilizei o RuntimeException como nossa exceção.
consultaCep
.get()
.uri(uriBuilder -> uriBuilder.path("/{cep}/json/").build(cep))
.retrieve()
.onStatus(
 HttpStatus.BAD_REQUEST::equals,
 response -> response.bodyToMono(String.class).map(RuntimeException::new)
)
.bodyToMono(CepDTO.class)
.block();


// Alternative 

.onStatus(status -> status.value() == HttpStatus.METHOD_NOT_ALLOWED.value(), 
 response -> Mono.error(new ServiceException("Metodo não autoziado", response.statusCode().value())))
  • Já o filter utilizamos ele como handler, no momento da criação do nosso WebClient podemos chamar ele e passarmos um ExchangeFilterFunction pra dentro, mas para criarmos um ExchangeFilterFunction precisamos criar um Mono .

Mono é um método que vai receber a interface do ClientResponse que será a resposta da nossa requisição e vai passar por nossas validações e já fazer a devidas ações... neste exemplo vou retorna um RuntimeException e sua mensagem será o Body da requisição.

private Mono<ClientResponse> exchangeFilterResponseProcessor(ClientResponse response) {
        HttpStatus status = response.statusCode();
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            return response
            .bodyToMono(String.class)
            .flatMap(body -> Mono.error(new RuntimeException(body)));
        }
        return Mono.just(response);
    }

Agora que temos o nosso método criado, vamos passar para o filter, chamamos o ExchangeFilterFunction.ofResponseProcessor e passamos o nosso método para dentro e setamos no filter.

WebClient
.builder()
.baseUrl("https://blog.sassine.dev")
.filter(ExchangeFilterFunction.ofResponseProcessor(this::exchangeFilterResponseProcessor))
.build();

Bonus: se estiver utilizando as ultimas versões java, você consegue deixar o seu método de exchangeFilterResponseProcessor bem mais elegante e performático, até por que depois de 5 validações o swtich se torna mais performático que o if else 🚀

private Mono<ClientResponse> exchangeFilterResponseProcessor(ClientResponse response) {
        return switch(response.statusCode()) {
        case INTERNAL_SERVER_ERROR, NOT_FOUND -> response.bodyToMono(String.class).flatMap(body -> Mono.error(new RuntimeException(body)));
        default -> Mono.just(response);
        };
    }

Rastremaneto/Log

Assim como o error handler, podemos utilizar o filter para criarmos um filtro de Log em nossas requisições....

Mesma coisa, precisamos criar um ExchangeFilterFunction.ofRequestProcessor porem desta vez conseguimos simplificar a criação dos nossos métodos de request e response, aí depois é só implementarmos nossa logica de logging conforme a necessidade 🤗

private ExchangeFilterFunction logRequest() {
    return ExchangeFilterFunction.ofRequestProcessor(cRequest -> {
        log.info("Request {} {}", cRequest.method(), cRequest.url());
        return Mono.just(cRequest);
    });
}

private ExchangeFilterFunction logResponse() {
    return ExchangeFilterFunction.ofResponseProcessor(cResponse -> {
        log.info("Response status code {} ", cResponse.statusCode());
        return Mono.just(cResponse);
    });
}

Métodos criados, agora só implementar eles no filter do nosso WebClient.

WebClient
.builder()
.baseUrl("https://viacep.com.br/ws/")
.filter(ExchangeFilterFunction.ofResponseProcessor(this::exchangeFilterResponseProcessor))
.filter(logRequest())
.filter(logResponse())
.build();

Conclusão

Acho que agora que conhecemos o WebClient chegamos todos a mesma conclusão, que de fato é bem mais fácil implementar o WebClient, manipular nossas respostas e tratarmos as exceções, rastrear e ainda podemos utilizar ele de forma não bloqueante e reativa se precisarmos.

Use e abuse desse conhecimento 😎

Did you find this article valuable?

Support Sassine EL-Asmar by becoming a sponsor. Any amount is appreciated!