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
WebClientopera 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
WebClientgerencia 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.