Explorando o WebClient: A Alternativa Moderna ao RestTemplate no Spring

Com o avanço do Spring Framework para a versão 5.0 e subsequentes, o RestTemplate foi marcado como obsoleto. A recomendação oficial é a adoção do WebClient, uma interface mais moderna e reativa para comunicação HTTP. Embora o RestTemplate ainda funcione, a transição para o WebClient é incentivada para novos projetos e manutenções.

O WebClient oferece vantagens significativas em relação ao seu predecessor:

  • I/O Não Bloqueante: Baseado em Reactor, o WebClient opera de forma reativa e não bloqueante, otimizando o uso de recursos e melhorando a escalabilidade em cenários de alta demanda.
  • Estilo de Programação Funcional: Sua API fluente e baseada em um estilo funcional torna o código mais legível, conciso e fácil de cofnigurar.
  • Suporte a Streaming: O WebClient gerencia eficientemente o streaming de dados de requisição e resposta, ideal para lidar com arquivos grandes ou dados em tempo real.
  • Tratamento de Erros Aprimorado: Oferece mecanismos mais robustos para tratamento de erros e log, facilitando a depuração e a identificação de problemas.

Um ponto crucial é a limitação do RestTemplate em configurar timeouts de requisição, mesmo em versões mais recentes do Spring Web. Essa deficiência é um dos principais motivos para migrar para o WebClient.

1. Configurando o Cliente HTTP

Para iniciar, é possível customizar o cliente subjacente, como o Netty (Reactor Netty), para definir timeouts de conexão, resposta e leitura.


HttpClient httpClient = HttpClient.create()
   .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout) // Timeout de conexão
   .responseTimeout(Duration.ofMillis(requestTimeout)) // Timeout para a resposta completa
   .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(readTimeout))); // Timeout de leitura

WebClient client = WebClient.builder()
   .clientConnector(new ReactorClientHttpConnector(httpClient))
   .baseUrl("http://localhost:8080") // URL base opcional
   .build();
 

2. Realizando Requisições de Forma Síncrona

Para quem prefere o modelo de requisição e espera de resposta, o WebClient pode simular o comportamento síncrono do RestTemplate:


public String enviarRequisicaoSincrona(String url, String corpoRequisicao) {
   LOG.info("Enviando requisição para API - URL: {} Corpo: {}", url, corpoRequisicao);
   String resposta = "";
   try {
       resposta = client
           .method(HttpMethod.POST)
           .uri(url)
           .accept(MediaType.APPLICATION_JSON)
           .contentType(MediaType.APPLICATION_JSON)
           .bodyValue(corpoRequisicao)
           .retrieve() // Inicia a recuperação da resposta
           .bodyToMono(String.class) // Converte o corpo da resposta para um Mono de String
           .block(); // Bloqueia a execução até que a resposta seja recebida

   } catch (Exception ex) {
       LOG.error("Erro ao chamar a API", ex);
       throw new RuntimeException("Erro no serviço XYZ: " + ex.getMessage());
   } finally {
       LOG.info("Resposta da API: {}", resposta);
   }
   return resposta;
}
 

O método .block() é utilizado para aguardar a resposta. Em cenários que exigem alta concorrência, o uso de .subscribe() para processamento assíncrono é mais recomendado.

3. Executando Requisições Assincronamente

Para um processamento não bloqueante, onde a aplicação não espera pela resposta imediatamente, utilize .subscribe():


public static Mono<String> enviarRequisicaoAssincrona(String url, String dadosPost) {
   WebClient webClientPadrao = WebClient.builder().build(); // Cliente com configuração padrão
   return webClientPadrao.post()
       .uri(url)
       .contentType(MediaType.APPLICATION_FORM_URLENCODED)
       .body(BodyInserters.fromFormData("data", dadosPost)) // Envia dados como form-urlencoded
       .retrieve()
       .bodyToMono(String.class); // Retorna um Mono<string>
}

// Exemplo de uso:
enviarRequisicaoAssincrona("https://api.exemplo.com/resource", "parametro=valor")
   .subscribe(
       resposta -> {
           // Lógica para processar a resposta bem-sucedida
           System.out.println("Resposta recebida: " + resposta);
       },
       erro -> {
           // Lógica para tratar erros
           System.err.println("Ocorreu um erro: " + erro.getMessage());
       }
   );
 </string>

O método .subscribe() recebe dois lambdas: um para o sucesso e outro para o erro. Isso permite que você defina o comportamento da sua aplicação em ambos os cenários de forma assíncrona.

4. Tratando Erros de Status (4xx e 5xx)

O WebClient permite interceptar e tratar respostas com status de erro:


public static Mono<String> requisicaoComTratamentoDeErro(String url, String dadosPost) {
   WebClient webClient = WebClient.builder()
       .baseUrl(url) // Define a URL base para simplificar
       .build();

   return webClient.post()
       .uri("/") // Caminho relativo à URL base
       .contentType(MediaType.APPLICATION_FORM_URLENCODED)
       .body(BodyInserters.fromFormData("data", dadosPost))
       .retrieve()
       // Define o tratamento para erros do cliente (4xx)
       .onStatus(HttpStatus::is4xxClientError, clientResponse ->
           Mono.error(new RuntimeException("Erro do Cliente: " + clientResponse.statusCode()))
       )
       // Define o tratamento para erros do servidor (5xx)
       .onStatus(HttpStatus::is5xxServerError, clientResponse ->
           Mono.error(new RuntimeException("Erro do Servidor: " + clientResponse.statusCode()))
       )
       .bodyToMono(String.class);
}
 

O método .onStatus() é encadeado antes de .bodyToMono(). Ele recebe um predicado para identificar o status code e uma função que retorna um Mono de erro a ser propagado.

5. Agindo com Base no Status do Erro

Dentro do .subscribe(), é possível inspecionar o tipo de exceção para um tratamento mais granular:


requisicaoComTratamentoDeErro("https://api.exemplo.com/resource", "parametro=valor")
   .subscribe(
       resposta -> System.out.println("Sucesso: " + resposta),
       erro -> {
           System.err.println("Erro geral: " + erro.getMessage());
           // Verifica se o erro é específico do WebClient
           if (erro instanceof WebClientResponseException) {
               WebClientResponseException wcEx = (WebClientResponseException) erro;
               int statusCode = wcEx.getStatusCode().value();
               String statusText = wcEx.getStatusText();
               System.err.println("Código de Status: " + statusCode);
               System.err.println("Texto do Status: " + statusText);
               // É possível acessar o corpo da resposta de erro com wcEx.getResponseBodyAsString()
           }
       }
   );
 

Ao capturar uma WebClientResponseException, você obtém detalhes como o código de status e o texto associado, permitindo lógicas de negócio específicas, como retentativas ou fallback.

6. Tratamento Completo de Respostas e Erros

Um exemplo mais abrangente de como tratar diferentes tipos de erros:


responseMono.subscribe(
   resposta -> {
       LOG.info("SUCESSO - Resposta da API: {}", resposta);
   },
   erro -> {
       LOG.error("ERRO - Ocorreu um erro: {}", erro.getMessage());
       LOG.error("ERRO - Classe do erro: {}", erro.getClass());

       // Erros retornados pelo servidor
       if (erro instanceof WebClientResponseException) {
           WebClientResponseException wcEx = (WebClientResponseException) erro;
           int statusCode = wcEx.getStatusCode().value();
           String statusText = wcEx.getStatusText();
           LOG.info("ERRO - Código de Status: {}", statusCode);
           LOG.info("ERRO - Texto do Status: {}", statusText);

           // Tratamento específico para erros 4xx
           if (statusCode >= 400 && statusCode < 500) {
               LOG.info("ERRO - Corpo da Resposta de Erro: {}", wcEx.getResponseBodyAsString());
           }

           Throwable causa = wcEx.getCause();
           LOG.error("ERRO - Detalhe WebClientResponseException");
           if (causa != null) {
               LOG.info("ERRO - Causa: {}", causa.getClass());
               if (causa instanceof ReadTimeoutException) {
                   LOG.error("ERRO - Exceção de Timeout de Leitura");
               }
               if (causa instanceof TimeoutException) {
                   LOG.error("ERRO - Exceção de Timeout");
               }
           }
       }

       // Erros de requisição do cliente (ex: timeouts de conexão)
       if (erro instanceof WebClientRequestException) {
           WebClientRequestException wcrEx = (WebClientRequestException) erro;
           Throwable causa = wcrEx.getCause();
           LOG.error("ERRO - Detalhe WebClientRequestException");
           if (causa != null) {
               LOG.info("ERRO - Causa: {}", causa.getClass());
               if (causa instanceof ReadTimeoutException) {
                   LOG.error("ERRO - Exceção de Timeout de Leitura");
               }
               if (causa instanceof ConnectTimeoutException) {
                   LOG.error("ERRO - Exceção de Timeout de Conexão");
               }
           }
       }
   }
);
 

Timeouts

É possível definir o timeout de leitura para requisições individuais:


return webClient
   .method(this.httpMethod)
   .uri(this.uri)
   .headers(httpHeaders -> httpHeaders.addAll(additionalHeaders))
   .bodyValue(this.requestEntity)
   .retrieve()
   .bodyToMono(responseType)
   .timeout(Duration.ofMillis(readTimeout)) // Timeout para esta requisição específica
   .block();
 

No entanto, o timeout de conexão é configurado na instância do HttpClient e, se precisar ser alterado, requer a criação de uma nova instância do WebClient.

Diferença entre Timeouts:

  • Connection Timeout: Tempo máximo para estabelecer a conexão com o servidor.
  • Read Timeout: Tempo máximo para receber dados do servidor após a conexão ter sido estabelecida.
  • Request Timeout: Tempo total desde o envio da requisição até o recebimento completo da resposta. O .timeout() do Reactor lida com isso.

Tags: Spring webflux webclient resttemplate java

Publicado em 6-19 19:52