Ir al contenido

Solicitudes HTTP con Rest Client en Spring Framework 6

Spring Restclient HTTP Rest Openai
Autor
Harpal Singh
Software Engineer
Traducido por
Namastecode
Tabla de contenido

1. Introducción

En este tutorial, aprenderemos cómo realizar solicitudes HTTP utilizando el cliente HTTP RestClient introducido en Spring 6.1 (M2).

Primero, comprenderemos la API de RestClient. Luego, veremos cómo realizar solicitudes GET, POST, PUT y DELETE utilizando la API de RestClient. Finalmente, examinaremos un ejemplo práctico de cómo hacer solicitudes HTTP a la API de OpenAI y llamar a ChatGPT para que genere algo de texto para nosotros.

2. Entendiendo RestClient

Hace un tiempo, el framework de Spring introdujo el WebClient para realizar solicitudes HTTP no bloqueantes. WebClient utiliza un paradigma reactivo y es adecuado para aplicaciones que funcionan bien en modo asincrónico y escenarios de transmisión. Por lo tanto, usarlo en una aplicación no reactiva agrega complejidad innecesaria y requiere que entendamos muy bien los tipos Mono y Flux. Además, WebClient no hace que el programa sea más eficiente, ya que de todas formas necesitamos esperar la respuesta.

Entonces, el framework de Spring introdujo la API RestClient para realizar solicitudes HTTP síncronas. Un enfoque más moderno al clásico RestTemplate. Eso no significa que RestTemplate esté en desuso. De hecho, bajo el capó, ambos utilizan la misma infraestructura, como convertidores de mensajes, fábricas de solicitudes, etc. Sin embargo, es una buena idea seguir con la API RestClient, ya que será el foco de futuras iteraciones del framework de Spring y recibirá más características.

RestClient utiliza bibliotecas HTTP como JDK HttpClient, Apache HttpComponents, y otras. Hace uso de la biblioteca que está disponible en el classpath. Así que, RestClient abstrae todos los detalles del cliente HTTP y utiliza el más apropiado, dependiendo de las dependencias que tengamos en nuestro proyecto.

3. Crear RestClient

Creamos RestClient usando el método create(), el cual devuelve una nueva instancia con configuraciones por defecto:

RestClient simpleRestClient = RestClient.create();

Si queremos personalizar el RestClient, podemos usar el método builder() y emplear la API fluida para establecer las configuraciones que necesitemos:

RestClient customRestClient = RestClient.builder()
 .requestFactory(new HttpComponentsClientHttpRequestFactory())
 .messageConverters(converters -> converters.add(new MappingJackson2HttpMessageConverter()))
 .baseUrl("https://example.com")
 .defaultHeader("Authorization", "Bearer your-token")
 .defaultHeader("Content-Type", "application/json")
 .build();

Nuestro RestClient personalizado utiliza el cliente Apache para realizar solicitudes HTTP. Luego, añadimos un convertidor de mensajes personalizado para manejar la serialización y deserialización JSON utilizando Jackson. Establecemos la URL base, los encabezados predeterminados y el tipo de contenido para todas las solicitudes realizadas con este cliente.

Ten en cuenta que la mayoría de estas configuraciones ya están establecidas por defecto. Ahora, tenemos un RestClient personalizado que podemos usar para realizar solicitudes HTTP. Podemos reutilizar esta instancia de RestClient en toda nuestra aplicación sin problemas, ya que es segura para subprocesos.

También podemos migrar a RestClient desde RestTemplate de manera incremental y reutilizar las configuraciones existentes para crear un RestClient:

RestTemplate restTemplate = new RestTemplate();
RestClient restClientFromRestTemplate = RestClient.create(restTemplate);

4. Realizando Solicitudes HTTP

En esta sección, explicaremos brevemente cómo realizar solicitudes GET, POST, PUT y DELETE utilizando la API RestClient. Usaremos una API ficticia llamada awesomeapi.com para demostrar las solicitudes. Para simplificar, utilizaremos el método retrieve() para realizar las solicitudes y obtener la respuesta como un String.

4.1. Realizando una solicitud GET

El tipo más común de solicitud HTTP es la solicitud GET, que utilizamos para obtener datos de un servidor. El RestClient proporciona una forma sencilla de realizar solicitudes GET con varias opciones para manejar parámetros y tipos de respuesta:

String response = simpleRestClient.get()
 .uri("https://awesomeapi.com/string/")
 .retrieve()
 .body(String.class);

record Item(String id, String name, double price) {    
} // JSON: {"id": 123, "name": "Product", "price": 99.99}

Item item = simpleRestClient.get()
 .uri("https://awesomeapi.com/item/{id}", 123)
 .retrieve()
 .body(Item.class);

En este fragmento, estamos realizando dos solicitudes GET. La primera simplemente obtiene una respuesta en formato String sin procesar. La segunda recibe una respuesta JSON de un objeto Item con los campos name y price. El método body() deserializa automáticamente la respuesta en una instancia válida de Item utilizando Jackson internamente. Por lo tanto, si la respuesta difiere de la estructura de Item, lanza una excepción.

4.2. Realizando una solicitud POST

Para enviar datos al servidor, utilizamos la solicitud POST. Más formalmente, las solicitudes POST crean nuevos recursos en el servidor. Como vimos con las solicitudes GET, RestClient facilita el envío de objetos codificados en JSON como el cuerpo de la solicitud:

record ItemRequest(String name, double price) {
} // JSON: {"name": "Product", "price": 99.99}

ItemRequest newItem = new ItemRequest("Product", 99.99);

Item createdItem = simpleRestClient.post()
 .uri("https://awesomeapi.com/item/")
 .contentType(MediaType.APPLICATION_JSON)
 .body(newItem)
 .retrieve()
 .body(Item.class);

En este ejemplo, creamos un nuevo objeto ItemRequest y lo enviamos al servidor. El método contentType() especifica que estamos enviando datos JSON, y el método body() serializa automáticamente el objeto Item en un JSON válido. Normalmente, el servidor responde con el recurso que creó, el cual podemos deserializar en un objeto Item. La diferencia entre Item y ItemRequest es que Item tiene un campo id generado por el servidor.

4.3. Realizando una solicitud PUT

Supongamos que queremos actualizar un recurso existente en el servidor. Podemos usar la solicitud PUT para este propósito. Similar a las solicitudes POST, las solicitudes PUT incluyen un cuerpo de solicitud de un recurso que queremos actualizar:

ItemRequest updatedItem = new ItemRequest("Updated Product", 129.99);

Item updated = simpleRestClient.put()
 .uri("https://awesomeapi.com/item/{id}", 123)
 .contentType(MediaType.APPLICATION_JSON)
 .body(updatedItem)
 .retrieve()
 .body(Item.class);

El ejemplo de la solicitud PUT muestra cómo actualizar un producto existente. Especificamos el id del producto en el método uri() y proporcionamos los datos actualizados del producto en el cuerpo de la solicitud. La respuesta contiene la información del producto actualizado según lo confirmado por el servidor.

Observemos que el método uri() utiliza la sintaxis de variables de ruta, una forma conveniente de especificar partes dinámicas del URI. Algunas APIs utilizan parámetros de consulta en lugar de variables de ruta. Para agregar parámetros de consulta, podemos usar el UriBuilder y establecer la ruta, los parámetros utilizando el método queryParam(), y finalmente construir el Uri para pasar al método uri():    

Item updatedWithQuery = simpleRestClient.put()
 .uri(uriBuilder ->
            uriBuilder.path("https://awesomeapi.com/item/")
 .queryParam("id", 123)
 .build())
 .contentType(MediaType.APPLICATION_JSON)
 .body(updatedItem)
 .retrieve()
 .body(Item.class);

Los parámetros de consulta se añaden al URI después del carácter ?. Por lo tanto, el URI resultante será https://awesomeapi.com/item/?id=123.

4.4. Realizando una solicitud DELETE

Finalmente, veamos cómo eliminar un recurso del servidor utilizando la solicitud DELETE:

simpleRestClient.delete()
 .uri("https://awesomeapi.com/item/{id}", 123)
 .retrieve()
 .toBodilessEntity();

En el ejemplo de DELETE, utilizamos toBodilessEntity() ya que las solicitudes DELETE normalmente no devuelven un cuerpo de respuesta. La sintaxis de la variable de ruta especifica qué recurso eliminar.

Si la eliminación es exitosa, no se lanza ninguna excepción. Esto es válido para todas las solicitudes; si el servidor responde con un código de estado de error, se lanzará una excepción. Podemos usar bloques try-catch para manejar estas excepciones, pero hay formas más sensatas de gestionar estos errores. Veremos eso en la siguiente sección.

5. Manejo de Errores usando onStatus()

Cuando trabajamos con REST APIs, necesitamos manejar los errores de manera elegante. No podemos usar bloques try-catch para cada solicitud, ya que resultaría en un código repetitivo y difícil de leer. El RestClient ofrece una forma muy limpia de manejar errores basados en el código de estado de la respuesta. Usamos el método onStatus() para manejar diferentes códigos de estado y proporcionar una manera más legible de manejar fallos:

String id = "123";
item = simpleRestClient.get()
 .uri("https://awesomeapi.com/item/{id}", id)
 .retrieve()
 .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
        throw new RuntimeException("Item not found with id: " + id);
 })
 .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
        throw new RuntimeException("Service is currently unavailable");
 })
 .body(Item.class);

En este ejemplo, hacemos una solicitud GET para obtener un objeto Item con un id específico. Si el servidor responde con un error de cliente 4xx, lanzamos una excepción con un mensaje diciendo que el ítem no se encontró en el servidor. De manera similar, si obtenemos un error de servidor 5xx, lanzamos otra RuntimeException indicando que el servicio no está disponible. Esto muestra cómo diferenciar entre diferentes tipos de errores; en escenarios del mundo real, debemos implementar la lógica de manejo de errores según los requisitos. Por ejemplo, podríamos tener errores temporales que requieren lógica de reintento.

6. Entendiendo retrieve() vs exchange()

El retrieve() simplifica las solicitudes HTTP mientras cubre la mayoría de los casos de uso. Sin embargo, a veces necesitamos más control sobre la solicitud y respuesta HTTP. Por ejemplo, podríamos necesitar leer los encabezados de respuesta que no encajan en el método onStatus(). Podemos utilizar el método exchange() en estos casos. Tengamos en cuenta que al usar exchange() no se utilizarán los manejadores de estado, ya que este método devuelve la respuesta completa, permitiéndonos realizar cualquier manejo de errores que necesitemos.

Por ejemplo, podemos implementar el mismo manejo de error 404 realizado en la sección anterior usando exchange():

Item itemResponse = simpleRestClient.get()
 .uri("https://awesomeapi.com/item/{id}", id)
 .exchange((request, response) -> {

        HttpHeaders headers = response.getHeaders();
        String etag = headers.getETag();

        if (response.getStatusCode().is4xxClientError()) {
            throw new RuntimeException("Item not found with id: " + id);
 } else if(response.getStatusCode().is5xxServerError()){
            throw new RuntimeException("Service is currently unavailable");
 }

        log.info("Got request with ETAG: " + etag);
        return response.bodyTo(Item.class);
 });

Creamos manualmente la lógica de manejo de errores utilizando declaraciones if-else en este ejemplo. El uso de handlers reduce la legibilidad del código, pero nos permite acceder a los headers de la respuesta, como el ETag, lo cual es imposible de otro modo con el método retrieve().

Probemos un ejemplo más práctico de cómo llamar a una API real.

7. Realizando Llamadas HTTP a la API de OpenAI

La Rest API es una excelente manera de interactuar con un servicio de terceros. En esta sección, usaremos la API de OpenAI para generar texto utilizando el modelo ChatGPT. Utilizaremos el RestClient para realizar una solicitud POST a la API de OpenAI y obtener la respuesta.

Primero, necesitamos obtener una clave de API del sitio web de OpenAI. Es una buena práctica no incluir la clave de API en la base de código y utilizar variables de entorno o una forma segura de almacenarla. Para este ejemplo, asumiremos que tenemos una variable de entorno llamada OPENAI_API_KEY que contiene la clave de API. Para leerla, simplemente usamos el método getenv() de la clase System.

String apiKey = System.getenv("OPENAI_API_KEY");

A continuación, creamos algunas clases auxiliares record para representar las estructuras de solicitud y respuesta requeridas por la API de OpenAI:


record ChatRequest(String model, List<Message> messages) {} 
record Message(String role, String content) {}
record ChatResponse(List<Choice> choices) {}
record Choice(Message message) {}

Ahora, creamos una instancia de RestClient con los encabezados necesarios para la autenticación y el tipo de contenido:

RestClient openAiClient = RestClient.builder()
 .baseUrl("https://api.openai.com/v1")
 .defaultHeader("Authorization", "Bearer " + apiKey)
 .defaultHeader("Content-Type", "application/json")
 .build();

Finalmente, hacemos una solicitud POST a la API de OpenAI para generar texto utilizando el modelo ChatGPT:

ChatRequest request = new ChatRequest(
    "gpt-4o-mini",
    List.of(new Message("user", "What is the brightest star in our galaxy?"))
);

ChatResponse response = openAiClient.post()
 .uri("/chat/completions")
 .body(request)
 .retrieve()
 .body(ChatResponse.class);

log.info("ChatGPT: " + response.choices().get(0).message().content());

Si todo va bien, veremos la respuesta de ChatGPT:

INFO: ChatGPT: The brightest star in our galaxy, the Milky Way, is typically considered to be **Sagittarius A***. However, strictly speaking, Sagittarius A* is actually the supermassive black hole at the center of the Milky Way. The title of the brightest visible star from Earth is generally given to **Sirius**, which is part of the constellation Canis Major. Sirius is approximately 8.6 light-years away and is about twice as massive as the Sun. It is often referred to as the "Dog Star."

Este ejemplo muestra cómo usar el RestClient para interactuar con una API del mundo real, como la proporcionada por OpenAI, para manejar estructuras JSON complejas y requisitos de autenticación de manera limpia y segura en cuanto al tipo.

8. Conclusión

En este artículo, aprendimos sobre el nuevo RestClient introducido en el nuevo API de RestClient de Spring Framework 6.1 para realizar solicitudes HTTP. Aprendimos cómo crear y configurar un RestClient, realizar solicitudes HTTP (GET, POST, PUT, DELETE), manejar errores y trabajar con APIs del mundo real como OpenAI.

Podemos buscar el código completo en Github.

Relacionados

Comprendiendo la Sintaxis Cron de @Scheduled en Spring
Spring Cron Scheduling
Testcontainers en Pruebas de Integración de Spring Boot
Spring Testcontainers Testing
Obtener valores definidos en el archivo de propiedades en Spring
Spring Properties Basics