Zademy

RestClient en Spring Boot 4: La Modernización de la Comunicación Síncrona

spring-boot; spring-framework; restclient; http-client
words palabras

Spring Boot 4.0, construido sobre Spring Framework 7, cambia cómo las aplicaciones Java hacen HTTP. El objetivo es claro: matar la deuda técnica de RestTemplate, adoptar Virtual Threads de Java 21+ y simplificar la comunicación entre servicios. RestClient es la pieza central de esa estrategia.

Qué trae nuevo

RestClient apareció en Spring Framework 6.1 (Spring Boot 3.2) como reemplazo de RestTemplate. En Spring Boot 4 se consolida como el cliente síncrono por defecto.

La nueva API

RestClient es síncrono pero con una API fluida basada en builders, similar a WebClient. En vez de las 40 sobrecargas de RestTemplate, encadenas métodos:

String res = restClient.get().uri(url).retrieve().body(String.class);

El manejo de errores es otra mejora notable. Con onStatus() defines qué hacer para cada código HTTP sin atrapar excepciones a lo bruto. Y los interceptores te dan un punto limpio para logging o autenticación.

Clientes declarativos (HTTP Service Clients)

Si has usado Feign, esto te va a gustar. Spring Framework 7 introdujo @ImportHttpServices, que junto con la auto-configuración de Spring Boot 4 te permite definir clientes HTTP como interfaces Java puras anotadas. Cero código manual de HttpServiceProxyFactory. Escribes una interfaz, Spring genera el proxy, lo inyectas y listo.

A este patrón le llaman el "Feign Killer" en la comunidad, y hasta donde lo he usado, no es marketing.

Rendimiento y Virtual Threads

RestClient es bloqueante. En el mundo antiguo eso significaba problemas de escalabilidad: cada hilo bloqueado esperando I/O era un hilo de plataforma caro (entre 1 y 8 MB) desperdiciado.

Con Virtual Threads de Java 21+ eso deja de ser un problema. Cuando un VT hace I/O, se "estaciona" y libera el carrier thread para que ejecute otra tarea. Los VTs pesan alrededor de 1 KB. El resultado: tu código Spring MVC bloqueante escala casi como WebClient reactivo, pero sin la complejidad mental de Mono, Flux y operadores.

Para activarlos, basta con poner spring.threads.virtual.enabled=true en tu configuración. Spring Boot 4 también centraliza los timeouts con spring.http.clients.connect-timeout y spring.http.clients.read-timeout.

Cómo se usa: RestClient vs RestTemplate

La diferencia se nota enseguida. RestTemplate usa un patrón de plantilla con decenas de sobrecargas (getForObject, getForEntity, exchange, ...). RestClient usa encadenamiento:

RestTemplate: restTemplate.getForObject(url, String.class) RestClient: restClient.get().uri(url).retrieve().body(String.class)

Para un POST con RestTemplate escribes postForEntity(url, request, T.class). Con RestClient: restClient.post().uri(url).body(request).retrieve().toEntity(T.class). Más verboso en caracteres, pero mucho más legible cuando encadenas headers, query params y manejo de errores.

RestTemplate está deprecado desde Spring Framework 6.1 y tiene los días contados: se elimina en Framework 8.0.

Ejemplos

Crear un bean configurado

La práctica recomendada es inyectar el RestClient.Builder y configurar URL base y headers una sola vez:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfiguration {

    @Bean
    public RestClient githubRestClient(RestClient.Builder builder) {
        // Configura una URL base y un encabezado por defecto para todas las solicitudes
        return builder
            .baseUrl("https://api.github.com")
            .defaultHeader("Accept", "application/json")
            .build();
    }
}

GET con variables de ruta y query params

// Suponiendo que 'githubRestClient' ya está inyectado

public String getRepositoryInfo(String owner, String repo, boolean verbose) {
    String uriTemplate = "/repos/{owner}/{repo}";

    return githubRestClient.get()
        .uri(uriTemplate, owner, repo) // Mapea {owner} y {repo}
        .queryParam("verbose", verbose) // Añade ?verbose=true o false
        .retrieve()
        .body(String.class);
}

POST (crear un recurso)

El cliente serializa el objeto a JSON automáticamente y deserializa la respuesta:

import org.springframework.http.MediaType;

public User createNewUser(NewUserDTO userData) {
    return githubRestClient.post()
        .uri("/users")
        .contentType(MediaType.APPLICATION_JSON) // Establece Content-Type
        .body(userData) // Objeto que se serializará a JSON
        .retrieve()
        .body(User.class); // Deserializa la respuesta a un objeto User
    }
}

Manejo de errores por código de estado

onStatus() te da control fino sobre cada respuesta antes de que se lance una excepción genérica:

import org.springframework.http.HttpStatus;

public User safeGetUser(long id) {
    return githubRestClient.get()
        .uri("/users/{id}", id)
        .retrieve()
        .onStatus(HttpStatus.NOT_FOUND, (request, response) -> {
            // Manejo específico para 404
            System.out.println("Usuario no encontrado, devolviendo default");
            // Se puede lanzar una excepción o, en este caso, devolver un valor predeterminado
            throw new CustomResourceNotFoundException("User ID " + id + " not found");
        })
        .onStatus(HttpStatus::is5xxServerError, (request, response) -> {
            // Manejo para 5xx (errores de servidor)
            throw new RuntimeException("Error interno del servicio remoto.");
        })
        .body(User.class);
}

Cliente declarativo (Spring Boot 4)

Defines el contrato en una interfaz. Spring genera el proxy:

// 1. Interfaz de Cliente (Definición del Contrato)
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;

@HttpExchange(url = "https://api.external.com/api/v1") // Base URL
public interface ExternalApi {
    @GetExchange("/items/{itemId}") // Mapea a GET https://api.external.com/api/v1/items/{itemId}
    Item getItem(@PathVariable String itemId);
}

// 2. Uso en un Servicio (Spring Boot 4.0 inyecta el proxy automáticamente tras configurar @ImportHttpServices)
@Service
public class ItemService {
    private final ExternalApi externalApi;

    // Se inyecta la implementación proxy generada
    public ItemService(ExternalApi externalApi) {
        this.externalApi = externalApi;
    }

    public Item findItem(String id) {
        // Se siente como llamar a un método local
        return externalApi.getItem(id);
    }
}

Si estás empezando un proyecto nuevo en Spring Boot 4, usa RestClient con @HttpExchange para todo lo que sea service-to-service. El código queda más limpio, más testeable y preparado para los Virtual Threads sin tocar nada extra. RestTemplate va a desaparecer en Framework 8.0; cuanto antes migres, menos dolor.