Chamando APIs externas com RestClient (Spring Boot 3.2+)
1. Visão geral
A partir do Spring Framework 6.1 (incluído no Spring Boot 3.2), o Spring ganhou um novo RestClient. É um cliente HTTP síncrono (bloqueante), com uma API fluente semelhante ao WebClient, mas sem precisar de Reactor. Este guia ensina você a criar um RestClient, fazer chamadas GET/POST, tratar erros, configurar timeouts, adicionar interceptadores e escrever testes. Depois de ler, você poderá substituir RestTemplate por RestClient em novos projetos.
2. Instalação
RestClient está no módulo spring-web. O starter web já inclui este módulo e Jackson para conversão JSON:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Ao longo deste artigo, vamos chamar uma API de pedidos simulada em https://api.shop.example. Declaramos os dados usando records:
public record Order(Long id, String customerName, BigDecimal total, String status) {}
public record CreateOrderRequest(String customerName, BigDecimal total) {}
A URL base é definida em application.properties:
shop.api.base-url=https://api.shop.example
3. Criando RestClient
3.1. O caminho mais rápido
RestClient restClient = RestClient.create("https://api.shop.example");
Este caminho é adequado ao fazer experimentos ou executar scripts. Em uma aplicação Spring Boot, você deve usar o builder fornecido pelo Boot.
3.2. Usando RestClient.Builder do Spring Boot
O Spring Boot configura automaticamente um bean RestClient.Builder. Este builder já vem com os HttpMessageConverter da aplicação, como o ObjectMapper que você personalizou. Ele também suporta observabilidade via Micrometer.
@Component
public class OrderClient {
private final RestClient restClient;
public OrderClient(RestClient.Builder builder,
@Value("${shop.api.base-url}") String baseUrl) {
this.restClient = builder
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
}
}
Explicação:
baseUrl(...): as chamadas subsequentes precisam apenas passar um caminho relativo como/orders/42.defaultHeader(...): este header é adicionado a cada requisição.build(): cria uma instância deRestClient. Esta instância é thread-safe, então você cria uma vez e reutiliza.
Nota: O bean
RestClient.Buildertem escopoprototype. Cada vez que você injeta, recebe um novo builder. Portanto, a configuração emOrderClientnão afeta outros clientes. Para aplicar uma configuração compartilhada a todos os builders, declare um beanRestClientCustomizer.
4. Enviando requisições GET
4.1. Obtendo o corpo como objeto
public Order getOrder(Long id) {
return restClient.get()
.uri("/orders/{id}", id)
.retrieve()
.body(Order.class);
}
get(): seleciona o método HTTP.uri("/orders/{id}", id): a variável{id}é substituída e codificada automaticamente. O resultado éGET https://api.shop.example/orders/42.retrieve(): envia a requisição e se prepara para ler a resposta.body(Order.class): o Jackson converte JSON para o recordOrder.
4.2. Obtendo status e headers
Quando você precisa do código de status ou dos headers, use toEntity:
ResponseEntity<Order> response = restClient.get()
.uri("/orders/{id}", 42)
.retrieve()
.toEntity(Order.class);
System.out.println(response.getStatusCode());
System.out.println(response.getHeaders().getContentType());
System.out.println(response.getBody());
200 OK
application/json
Order[id=42, customerName=Lan, total=250000, status=PAID]
4.3. Obtendo listas e parâmetros de consulta
Tipos genéricos como List<Order> são apagados em tempo de execução. Portanto, você precisa de ParameterizedTypeReference:
public List<Order> findOrders(String status, int page) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/orders")
.queryParam("status", status)
.queryParam("page", page)
.build())
.retrieve()
.body(new ParameterizedTypeReference<List<Order>>() {});
}
A chamada findOrders("PAID", 1) cria uma requisição GET /orders?status=PAID&page=1. A URL base é mantida quando você usa uriBuilder.
5. Enviando requisições POST, PUT, DELETE
public Order createOrder(CreateOrderRequest request) {
return restClient.post()
.uri("/orders")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(Order.class);
}
public void cancelOrder(Long id) {
restClient.delete()
.uri("/orders/{id}", id)
.retrieve()
.toBodilessEntity();
}
body(request): o objeto é serializado para JSON graças aocontentTypedeclarado.toBodilessEntity(): use quando você não precisa ler o corpo. Este método ainda ativa a requisição e trata erros.
put() e patch() funcionam igual a post().
Nota: Se você chamar
retrieve()sem chamar depoisbody(...),toEntity(...)outoBodilessEntity(), a requisição não será enviada. Este é um erro comum com chamadas DELETE.
6. Tratamento de erros
6.1. Comportamento padrão
Por padrão, retrieve() lança uma exceção quando a resposta tem um status de erro:
- 4xx: lança
HttpClientErrorException, por exemploHttpClientErrorException.NotFound. - 5xx: lança
HttpServerErrorException.
Erros de conexão ou timeout lançam ResourceAccessException.
6.2. Personalizando com onStatus
Você geralmente deseja converter erros HTTP em exceções de negócio:
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(Long id) {
super("Pedido " + id + " não encontrado");
}
}
public Order getOrder(Long id) {
return restClient.get()
.uri("/orders/{id}", id)
.retrieve()
.onStatus(status -> status.value() == 404, (request, response) -> {
throw new OrderNotFoundException(id);
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new IllegalStateException(
"API de loja com erro: " + response.getStatusCode());
})
.body(Order.class);
}
Os handlers são verificados na ordem declarada. O primeiro handler que corresponde ao status será executado. Se nenhum handler corresponder, o comportamento padrão da seção 6.1 será aplicado.
6.3. Controle total com exchange
Quando você precisa ler o corpo do erro ou ramificar por status, use exchange:
public Optional<Order> findOrder(Long id) {
return restClient.get()
.uri("/orders/{id}", id)
.exchange((request, response) -> {
if (response.getStatusCode().value() == 404) {
return Optional.empty();
}
if (response.getStatusCode().isError()) {
throw new IllegalStateException("Erro " + response.getStatusCode());
}
return Optional.ofNullable(
response.bodyTo(Order.class));
});
}
Nota: Em
exchange, os handlers de status padrão não são aplicados. Você deve verificar o status por conta própria. Se esquecer, o corpo do erro pode ser analisado em umOrdercom camposnull.
7. Timeout e cliente HTTP subjacente
Por padrão, RestClient escolhe a biblioteca HTTP disponível no classpath nesta ordem: Apache HttpClient 5, Jetty, Reactor Netty, java.net.http.HttpClient do JDK, depois SimpleClientHttpRequestFactory. Você sempre deve definir um timeout. Caso contrário, uma API lenta pode manter sua thread por muito tempo.
@Configuration
public class ShopClientConfig {
@Bean
public RestClientCustomizer timeoutCustomizer() {
return builder -> {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(2));
factory.setReadTimeout(Duration.ofSeconds(5));
builder.requestFactory(factory);
};
}
}
RestClientCustomizer é aplicado a todos os RestClient.Builder criados pelo Spring Boot. Se você quiser definir um timeout apenas para um cliente, chame builder.requestFactory(factory) diretamente no construtor desse cliente.
Com uma API lenta, a requisição falhará após 5 segundos:
org.springframework.web.client.ResourceAccessException: I/O error on GET request
for "https://api.shop.example/orders/42": Read timed out
A partir do Spring Boot 3.4, você também pode declarar timeouts usando as propriedades spring.http.client.connect-timeout e spring.http.client.read-timeout.
8. Interceptadores: anexando tokens, registrando logs
ClientHttpRequestInterceptor permite que você intercepte cada requisição. Por exemplo, o código a seguir anexa uma chave de API e registra o tempo de resposta:
@Slf4j
public class ApiKeyInterceptor implements ClientHttpRequestInterceptor {
private final String apiKey;
public ApiKeyInterceptor(String apiKey) {
this.apiKey = apiKey;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().set("X-Api-Key", apiKey);
long start = System.currentTimeMillis();
ClientHttpResponse response = execution.execute(request, body);
log.info("{} {} -> {} ({} ms)", request.getMethod(), request.getURI(),
response.getStatusCode(), System.currentTimeMillis() - start);
return response;
}
}
Você registra o interceptador ao construir o cliente:
this.restClient = builder
.baseUrl(baseUrl)
.requestInterceptor(new ApiKeyInterceptor(apiKey))
.build();
INFO ApiKeyInterceptor : GET https://api.shop.example/orders/42 -> 200 OK (87 ms)
Nota: Não leia
response.getBody()dentro do interceptador para registrar logs. O stream pode ser lido apenas uma vez. Se precisar obrigatoriamente ler, envolva a factory de requisição comBufferingClientHttpRequestFactory.
9. HTTP interface: declarando o cliente via interface
Em vez de escrever manualmente cada chamada, você pode descrever a API com uma interface. O Spring gerará a implementação baseada em RestClient:
public interface OrderApi {
@GetExchange("/orders/{id}")
Order getOrder(@PathVariable Long id);
@PostExchange("/orders")
Order createOrder(@RequestBody CreateOrderRequest request);
}
@Configuration
public class OrderApiConfig {
@Bean
public OrderApi orderApi(RestClient.Builder builder,
@Value("${shop.api.base-url}") String baseUrl) {
RestClient restClient = builder.baseUrl(baseUrl).build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(OrderApi.class);
}
}
Agora você apenas injeta OrderApi e chama orderApi.getOrder(42L). Este é um jeito conciso quando a API tem muitos endpoints. Timeouts, interceptadores e handlers de status ainda são configurados no RestClient subjacente.
10. Escrevendo testes com @RestClientTest
A partir do Spring Boot 3.2, @RestClientTest suporta RestClient. Esta anotação inicializa apenas beans relacionados ao cliente HTTP. Ela também conecta MockRestServiceServer ao RestClient.Builder, então nenhuma requisição real é enviada para fora.
Testamos OrderClient com a versão getOrder que tem onStatus na seção 6.2:
@RestClientTest(components = OrderClient.class,
properties = "shop.api.base-url=https://api.shop.example")
class OrderClientTest {
@Autowired
private OrderClient orderClient;
@Autowired
private MockRestServiceServer server;
@Test
void givenExistingOrder_whenGetOrder_thenReturnsOrder() {
server.expect(requestTo("https://api.shop.example/orders/42"))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("""
{"id":42,"customerName":"Lan","total":250000,"status":"PAID"}
""", MediaType.APPLICATION_JSON));
Order order = orderClient.getOrder(42L);
assertThat(order.id()).isEqualTo(42L);
assertThat(order.customerName()).isEqualTo("Lan");
assertThat(order.total()).isEqualByComparingTo("250000");
server.verify();
}
@Test
void givenMissingOrder_whenGetOrder_thenThrowsOrderNotFound() {
server.expect(requestTo("https://api.shop.example/orders/99"))
.andRespond(withStatus(HttpStatus.NOT_FOUND));
assertThatThrownBy(() -> orderClient.getOrder(99L))
.isInstanceOf(OrderNotFoundException.class)
.hasMessage("Pedido 99 não encontrado");
}
}
Os imports estáticos necessários estão em MockRestRequestMatchers e MockRestResponseCreators nos pacotes org.springframework.test.web.client.match e org.springframework.test.web.client.response.
server.expect(...): declara a requisição que o teste espera.andRespond(...): retorna uma resposta simulada para essa requisição.server.verify(): verifica que todas as requisições declaradas foram realmente enviadas.
Nota:
MockRestServiceServerfunciona apenas quando o cliente é criado a partir deRestClient.Builderque o Spring injeta. Se você chamarRestClient.create(...)dentro da classe, o teste enviará requisições reais pela rede.
11. Quando não usar RestClient
- Aplicações WebFlux (reativas):
RestClientbloqueia a thread. Se você chamá-lo dentro de um pipeline reativo, o event loop será interrompido. UseWebClient. - Precisa fazer streaming de dados grandes ou Server-Sent Events não bloqueantes:
WebClienté mais adequado. - Código legado rodando bem com
RestTemplate: você não é obrigado a reescrever.RestTemplateainda é suportado, apenas não recebe novos recursos. Você pode migrar gradualmente usandoRestClient.create(restTemplate)para manter a configuração existente.
Se seu aplicativo for executado em Java 21 com virtual threads (spring.threads.virtual.enabled=true), o modelo bloqueante de RestClient praticamente não tem mais desvantagens em termos de escalabilidade. O código continua simples e fácil de depurar.
12. Conclusão
Neste artigo, você usou RestClient para chamar APIs externas com uma API fluente e legível. Passamos por: criar o cliente a partir do builder do Spring Boot, enviar GET/POST, converter erros HTTP em exceções de negócio usando onStatus ou exchange, definir timeouts, adicionar interceptadores e declarar o cliente via HTTP interface. Por fim, você escreveu testes com @RestClientTest e MockRestServiceServer sem fazer chamadas de rede reais.
Para projetos Spring Boot 3.2+ seguindo o modelo servlet, RestClient deve ser a escolha padrão em vez de RestTemplate. Lembre-se de duas coisas: sempre defina um timeout, e sempre termine a cadeia retrieve() com um método de leitura de resposta.