Salta al contenuto principale

Richieste HTTP con Rest Client nel framework Spring 6

Spring Restclient HTTP Rest Openai
Autore
Harpal Singh
Software Engineer
Tradotto da
Namastecode
Indice dei contenuti

1. Introduzione

In questo tutorial, impareremo come effettuare richieste HTTP utilizzando il client HTTP RestClient introdotto in Spring 6.1 (M2).

Prima di tutto, comprenderemo l’API RestClient. Poi, vedremo come effettuare richieste GET, POST, PUT e DELETE utilizzando l’API RestClient. Infine, esamineremo un esempio pratico di come fare richieste HTTP all’API di OpenAI e chiamare ChatGPT per generare del testo per noi.

2. Comprendere RestClient

Un po’ di tempo fa, il framework Spring ha introdotto il WebClient per effettuare richieste HTTP non bloccanti. Il WebClient utilizza un paradigma reattivo ed è adatto nelle applicazioni che funzionano bene in modalità asincrona e in scenari di streaming. Pertanto, utilizzarlo in un’applicazione non reattiva aggiunge complessità non necessaria e richiede che comprendiamo molto bene i tipi Mono e Flux. Inoltre, il WebClient non rende il programma più performante, dato che dobbiamo comunque attendere la risposta.

Quindi, il framework Spring ha introdotto l’API RestClient per effettuare richieste HTTP sincrone. Un approccio più moderno al classico RestTemplate. Questo non significa che RestTemplate sia deprecato. Infatti, dietro le quinte, entrambi utilizzano la stessa infrastruttura, come convertitori di messaggi, fabbriche di richieste, ecc. Tuttavia, è una buona idea utilizzare l’API RestClient poiché sarà il focus delle future iterazioni del framework Spring e riceverà più funzionalità.

RestClient utilizza le librerie HTTP come JDK HttpClient, Apache HttpComponents e altre. Fa uso della libreria disponibile nel classpath. Quindi, RestClient astrae tutti i dettagli del client HTTP e utilizza quella più appropriata, a seconda delle dipendenze che abbiamo nel nostro progetto.

3. Creare RestClient

Creiamo RestClient usando il metodo create(), che restituisce una nuova istanza con impostazioni predefinite:

RestClient simpleRestClient = RestClient.create();

Se vogliamo personalizzare il RestClient, possiamo utilizzare il metodo builder() e sfruttare l’API fluente per impostare le configurazioni di cui abbiamo bisogno:

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();

Il nostro RestClient personalizzato utilizza il client Apache per effettuare richieste HTTP. Poi, aggiungiamo un convertitore di messaggi personalizzato per gestire la serializzazione e deserializzazione JSON utilizzando Jackson. Impostiamo l’URL di base, gli header predefiniti e il tipo di contenuto per tutte le richieste effettuate con questo client.

Tieni presente che la maggior parte di queste configurazioni sono già impostate di default. Ora, abbiamo un RestClient personalizzato che possiamo utilizzare per effettuare richieste HTTP. Possiamo riutilizzare questa istanza di RestClient in tutta la nostra applicazione senza problemi, poiché è thread-safe.

Possiamo anche migrare a RestClient da RestTemplate in modo incrementale e riutilizzare le configurazioni esistenti per creare un RestClient:

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

4. Effettuare Richieste HTTP

In questa sezione, spiegheremo brevemente come effettuare richieste GET, POST, PUT e DELETE utilizzando l’API RestClient. Utilizzeremo una finta API chiamata awesomeapi.com per dimostrare le richieste. Per semplicità, useremo il metodo retrieve() per effettuare le richieste e ottenere la risposta come una String.

4.1. Effettuare una richiesta GET

Il tipo più comune di richiesta HTTP è la richiesta GET, che utilizziamo per ottenere dati da un server. Il RestClient offre un modo semplice per effettuare richieste GET con varie opzioni per gestire parametri e tipi di risposta:

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);

In questo frammento, stiamo effettuando due richieste GET. La prima ottiene semplicemente una risposta String grezza. La seconda riceve una risposta JSON di un oggetto Item con i campi name e price. Il metodo body() deserializza automaticamente la risposta in un’istanza valida di Item utilizzando Jackson sotto il cofano. Quindi, se la risposta differisce dalla struttura di Item, viene lanciata un’eccezione.

4.2. Effettuare una richiesta POST

Per inviare dati al server, utilizziamo la richiesta POST. Più formalmente, le richieste POST creano nuove risorse sul server. Come abbiamo visto con le richieste GET, il RestClient rende facile inviare oggetti codificati in JSON come corpo della richiesta:

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);

In questo esempio, creiamo un nuovo oggetto ItemRequest e lo inviamo al server. Il metodo contentType() specifica che stiamo inviando dati in formato JSON, e il metodo body() serializza automaticamente l’oggetto Item in un JSON valido. Solitamente, il server risponde con la risorsa che ha creato, che possiamo deserializzare in un oggetto Item. La differenza tra Item e ItemRequest è che Item ha un campo id generato dal server.

4.3. Effettuare una richiesta PUT

Supponiamo di voler aggiornare una risorsa esistente sul server. Possiamo utilizzare la richiesta PUT a questo scopo. Simile alle richieste POST, le richieste PUT includono un corpo della richiesta della risorsa che vogliamo aggiornare:

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);

L’esempio di richiesta PUT mostra come aggiornare un prodotto esistente. Specifichiamo l’id del prodotto nel metodo uri() e forniamo i dati aggiornati del prodotto nel corpo della richiesta. La risposta contiene le informazioni aggiornate del prodotto come confermato dal server.

Nota che il metodo uri() utilizza la sintassi delle variabili di percorso, un modo conveniente per specificare parti dinamiche dell’URI. Alcune API utilizzano parametri di query invece delle variabili di percorso. Per aggiungere parametri di query, possiamo utilizzare il UriBuilder e impostare il percorso, i parametri utilizzando il metodo queryParam(), e infine costruire l’Uri da passare al metodo 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);

I parametri di query vengono aggiunti all’URI dopo il carattere ?. Quindi l’URI risultante sarà https://awesomeapi.com/item/?id=123.

4.4. Effettuare una Richiesta DELETE

Infine, vediamo come eliminare una risorsa dal server utilizzando la richiesta DELETE:

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

Nel caso dell’esempio DELETE, utilizziamo toBodilessEntity() poiché le richieste DELETE generalmente non restituiscono un corpo di risposta. La sintassi della variabile di percorso specifica quale risorsa eliminare.

Se l’eliminazione ha successo, non viene generata alcuna eccezione. Questo è valido per tutte le richieste; se il server risponde con un codice di stato di errore, verrà generata un’eccezione. Possiamo utilizzare i blocchi try-catch per gestire queste eccezioni, ma ci sono modi più sensati per gestire questi errori. Lo vedremo nella prossima sezione.

5. Gestione degli Errori con onStatus()

Quando lavoriamo con le REST API, dobbiamo gestire gli errori in modo elegante. Non possiamo usare blocchi try-catch per ogni richiesta, poiché ciò risulterebbe in codice ripetitivo e poco leggibile. Il RestClient offre un modo molto pulito per gestire gli errori basati sul codice di stato della risposta. Usiamo il metodo onStatus() per gestire diversi codici di stato e fornire un modo più leggibile per gestire i fallimenti:

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);

In questo esempio, effettuiamo una richiesta GET per recuperare un oggetto Item con un id specifico. Se il server risponde con un errore 4xx del client, lanciamo un’eccezione con un messaggio che dice che l’elemento non è stato trovato sul server. Allo stesso modo, se riceviamo un errore 5xx del server, lanciamo un’altra RuntimeException indicando che il servizio non è disponibile. Questo mostra come differenziare tra diversi tipi di errori; in scenari reali, dobbiamo implementare la logica di gestione degli errori in base ai requisiti. Ad esempio, potremmo avere errori temporanei che richiedono una logica di retry.

6. Comprendere retrieve() vs exchange()

Il metodo retrieve() semplifica le richieste HTTP coprendo la maggior parte dei casi d’uso. Tuttavia, a volte abbiamo bisogno di un maggiore controllo sulla richiesta e sulla risposta HTTP. Ad esempio, potremmo dover leggere gli header della risposta che non rientrano nel metodo onStatus(). In questi casi, possiamo utilizzare il metodo exchange(). Attenzione: l’uso di exchange() non impiegherà i gestori di stato, poiché restituisce già la risposta completa, permettendoci di eseguire qualsiasi gestione degli errori necessaria.

Ad esempio, possiamo implementare lo stesso gestore di errori 404 fatto nella sezione precedente utilizzando 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);
 });

Abbiamo creato manualmente la logica di gestione degli errori utilizzando istruzioni if-else in questo esempio. L’uso dei gestori riduce la leggibilità del codice ma ci permette di accedere agli header dalla risposta, come l’ETag, cosa altrimenti impossibile con il metodo retrieve().

Proviamo un esempio più pratico di come chiamare una vera API.

7. Effettuare Chiamate HTTP all’API di OpenAI

L’API Rest è un ottimo modo per interagire con un servizio di terze parti. In questa sezione, useremo l’API di OpenAI per generare del testo utilizzando il modello ChatGPT. Utilizzeremo il RestClient per effettuare una richiesta POST all’API di OpenAI e ottenere la risposta.

Prima di tutto, dobbiamo ottenere una chiave API dal sito web di OpenAI. È una buona pratica non includere la chiave API nel codice sorgente e utilizzare variabili d’ambiente o un metodo sicuro per conservarla. Per questo esempio, assumeremo di avere una variabile d’ambiente chiamata OPENAI_API_KEY che contiene la chiave API. Per leggerla, utilizziamo semplicemente il metodo getenv() della classe System.

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

Successivamente, creiamo alcune classi di supporto record per rappresentare le strutture di richiesta e risposta richieste dall’API di OpenAI:


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

Ora, creiamo un’istanza di RestClient con gli header necessari per l’autenticazione e il tipo di contenuto:

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

Infine, facciamo una richiesta POST all’API di OpenAI per generare del testo utilizzando il modello 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());

Se tutto va bene, vedremo la risposta da 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."

Questo esempio mostra come utilizzare il RestClient per interagire con un’API reale, come quella fornita da OpenAI, per gestire strutture JSON complesse e requisiti di autenticazione in modo pulito e con sicurezza tipizzata.

8. Conclusione

In questo articolo, abbiamo appreso del nuovo RestClient introdotto nella nuova API RestClient di Spring Framework 6.1 per effettuare richieste HTTP. Abbiamo imparato come creare e configurare un RestClient, effettuare richieste HTTP (GET, POST, PUT, DELETE), gestire gli errori e lavorare con API reali come OpenAI.

Possiamo cercare il codice completo su Github.

Related

Comprendere la sintassi Cron di @Scheduled di Spring
Spring Cron Scheduling
Testcontainers nei Test di Integrazione di Spring Boot
Spring Testcontainers Testing
Ottieni Valori Definiti nel File di Proprietà in Spring
Spring Properties Basics