Zum Hauptinhalt springen

Http-Anfragen mit Rest Client im Spring Framework 6

Spring Restclient HTTP Rest Openai
Autor
Harpal Singh
Software Engineer
Übersetzt von
Namastecode
Inhaltsverzeichnis

1. Einführung

In diesem Tutorial lernen wir, wie man HTTP-Anfragen mit dem RestClient HTTP-Client durchführt, der in Spring 6.1 (M2) eingeführt wurde.

Zuerst werden wir die RestClient API verstehen. Dann werden wir sehen, wie man GET-, POST-, PUT- und DELETE-Anfragen mit der RestClient API durchführt. Schließlich schauen wir uns ein praktisches Beispiel an, wie man HTTP-Anfragen an die OpenAI API stellt und ChatGPT aufruft, um für uns Text zu generieren.

2. Das Verständnis von RestClient

Vor einiger Zeit hat das Spring-Framework den WebClient eingeführt, um nicht-blockierende HTTP-Anfragen zu stellen. WebClient verwendet ein reaktives Paradigma und ist geeignet für Anwendungen, die gut im asynchronen Modus und in Streaming-Szenarien funktionieren. Daher fügt die Verwendung in einer nicht-reaktiven Anwendung unnötige Komplexität hinzu und erfordert von uns, die Mono- und Flux-Typen sehr gut zu verstehen. Außerdem macht WebClient das Programm nicht performanter, da wir ohnehin auf die Antwort warten müssen.

Also, das Spring-Framework hat die RestClient-API eingeführt, um synchrone HTTP-Anfragen zu stellen. Ein modernerer Ansatz im Vergleich zur klassischen RestTemplate. Das bedeutet nicht, dass RestTemplate veraltet ist. Tatsächlich verwenden beide im Hintergrund dieselbe Infrastruktur, wie z.B. Message Converters, Request Factories, etc. Dennoch ist es eine gute Idee, sich an die RestClient-API zu halten, da sie im Mittelpunkt zukünftiger Iterationen des Spring-Frameworks stehen wird und mehr Funktionen erhalten wird.

RestClient verwendet HTTP-Bibliotheken wie den JDK HttpClient, Apache HttpComponents und andere. Es nutzt die Bibliothek, die im Klassenpfad verfügbar ist. RestClient abstrahiert also alle Details des HTTP-Clients und verwendet den am besten geeigneten, abhängig von den Abhängigkeiten, die wir in unserem Projekt haben.

3. Erstellen Sie RestClient

Wir erstellen RestClient mit der Methode create(), die eine neue Instanz mit den Standardeinstellungen zurückgibt:

RestClient simpleRestClient = RestClient.create();

Wenn wir den RestClient anpassen möchten, können wir die builder()-Methode verwenden und die Fluent-API nutzen, um die Konfigurationen festzulegen, die wir benötigen:

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

Unser benutzerdefinierter RestClient verwendet den Apache-Client, um HTTP-Anfragen zu stellen. Anschließend fügen wir einen benutzerdefinierten Nachrichtenkonverter hinzu, um die JSON-Serialisierung und -Deserialisierung mit Jackson zu handhaben. Wir legen die Basis-URL, Standard-Header und den Inhaltstyp für alle Anfragen fest, die mit diesem Client gemacht werden.

Beachten wir, dass die meisten dieser Konfigurationen bereits standardmäßig eingerichtet sind. Nun haben wir einen angepassten RestClient, den wir verwenden können, um HTTP-Anfragen zu stellen. Wir können diese RestClient-Instanz problemlos in unserer gesamten Anwendung wiederverwenden, da sie threadsicher ist.

Wir können auch schrittweise von RestTemplate zu RestClient migrieren und die vorhandenen Konfigurationen wiederverwenden, um einen RestClient zu erstellen:

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

4. HTTP-Anfragen stellen

In diesem Abschnitt erklären wir kurz, wie man GET-, POST-, PUT- und DELETE-Anfragen mit der RestClient API durchführt. Wir verwenden eine fiktive API namens awesomeapi.com, um die Anfragen zu demonstrieren. Der Einfachheit halber verwenden wir die Methode retrieve(), um die Anfragen zu stellen und die Antwort als String zu erhalten.

4.1. Durchführung einer GET-Anfrage

Der am häufigsten verwendete HTTP-Request ist der GET-Request, den wir nutzen, um Daten von einem Server abzurufen. Der RestClient bietet eine einfache Möglichkeit, GET-Requests durchzuführen, mit verschiedenen Optionen zur Handhabung von Parametern und Antworttypen:

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 diesem Ausschnitt führen wir zwei GET-Anfragen durch. Die erste holt lediglich eine rohe String-Antwort. Die zweite erhält eine JSON-Antwort eines Item-Objekts mit den Feldern name und price. Die body()-Methode deserialisiert die Antwort automatisch in eine gültige Instanz von Item unter der Haube mit Jackson. Wenn die Antwort von der Item-Struktur abweicht, wird eine Ausnahme ausgelöst.

4.2. Einen POST-Request durchführen

Um Daten an den Server zu senden, verwenden wir die POST-Anfrage. Formaler ausgedrückt, erstellen POST-Anfragen neue Ressourcen auf dem Server. Wie wir bei GET-Anfragen gesehen haben, macht es der RestClient einfach, JSON-kodierte Objekte als Anfrageinhalt zu senden:

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 diesem Beispiel erstellen wir ein neues ItemRequest-Objekt und senden es an den Server. Die contentType()-Methode gibt an, dass wir JSON-Daten senden, und die body()-Methode serialisiert das Item-Objekt automatisch in ein gültiges JSON. Normalerweise antwortet der Server mit der Ressource, die er erstellt hat, welche wir in ein Item-Objekt deserialisieren können. Der Unterschied zwischen Item und ItemRequest besteht darin, dass Item ein id-Feld hat, das vom Server generiert wird.

4.3. Einen PUT-Request durchführen

Angenommen, wir möchten eine vorhandene Ressource auf dem Server aktualisieren. Dafür können wir die PUT-Anfrage verwenden. Ähnlich wie bei POST-Anfragen beinhalten PUT-Anfragen einen Anfragetext der Ressource, die wir aktualisieren möchten:

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

Das PUT-Request-Beispiel zeigt, wie ein bestehendes Produkt aktualisiert wird. Wir geben die ID des Produkts in der uri()-Methode an und stellen die aktualisierten Produktdaten im Anfrage-Body bereit. Die Antwort enthält die aktualisierten Produktinformationen, wie sie vom Server bestätigt wurden.

Beachten Sie, dass die Methode uri() die Syntax von Pfadvariablen verwendet, eine bequeme Möglichkeit, dynamische Teile der URI anzugeben. Einige APIs verwenden Abfrageparameter anstelle von Pfadvariablen. Um Abfrageparameter hinzuzufügen, können wir den UriBuilder verwenden und den Pfad sowie die Parameter mit der Methode queryParam() festlegen und schließlich die Uri erstellen, um sie an die Methode uri() zu übergeben:    

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

Abfrageparameter werden nach dem ?-Zeichen zur URI hinzugefügt. Die resultierende URI wird also https://awesomeapi.com/item/?id=123 sein.

4.4. Einen DELETE Request durchführen

Schließlich wollen wir uns ansehen, wie man eine Ressource vom Server mithilfe der DELETE-Anfrage löscht:

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

Im DELETE-Beispiel verwenden wir toBodilessEntity(), da DELETE-Anfragen typischerweise keinen Antwortkörper zurückgeben. Die Syntax der Pfadvariable gibt an, welche Ressource gelöscht werden soll.

Wenn das Löschen erfolgreich ist, wird keine Ausnahme ausgelöst. Dies gilt für alle Anfragen; wenn der Server mit einem Fehlerstatuscode antwortet, wird eine Ausnahme ausgelöst. Wir können try-catch-Blöcke verwenden, um diese Ausnahmen zu behandeln, aber es gibt vernünftigere Möglichkeiten, mit diesen Fehlern umzugehen. Das werden wir im nächsten Abschnitt sehen.

5. Fehlerbehandlung mit onStatus()

Beim Arbeiten mit REST-APIs müssen wir Fehler elegant handhaben. Wir können nicht für jede Anfrage try-catch-Blöcke verwenden, da dies zu Boilerplate-Code und Unlesbarkeit führt. Der RestClient bietet eine sehr saubere Möglichkeit, Fehler basierend auf dem Statuscode der Antwort zu handhaben. Wir verwenden die Methode onStatus(), um verschiedene Statuscodes zu behandeln und eine lesbarere Art und Weise bereitzustellen, mit Fehlern umzugehen:

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 diesem Beispiel führen wir eine GET-Anfrage aus, um ein Item-Objekt mit einer bestimmten ID abzurufen. Wenn der Server mit einem 4xx-Client-Fehler antwortet, werfen wir eine Ausnahme mit einer Nachricht, die besagt, dass das Item auf dem Server nicht gefunden wurde. Ebenso werfen wir bei einem 5xx-Server-Fehler eine weitere RuntimeException, die darauf hinweist, dass der Dienst nicht verfügbar ist. Dies zeigt, wie man zwischen verschiedenen Fehlertypen unterscheiden kann; in realen Szenarien müssen wir die Fehlerbehandlungslogik basierend auf den Anforderungen implementieren. Zum Beispiel könnten wir temporäre Fehler haben, die eine Retry-Logik erfordern.

6. Verständnis von retrieve() vs exchange()

Die Methode retrieve() vereinfacht HTTP-Anfragen und deckt die meisten Anwendungsfälle ab. Manchmal benötigen wir jedoch mehr Kontrolle über die HTTP-Anfrage und -Antwort. Zum Beispiel könnten wir die Antwort-Header lesen müssen, die nicht in die Methode onStatus() passen. In solchen Fällen können wir die Methode exchange() verwenden. Beachten Sie, dass bei der Verwendung von exchange() keine Status-Handler verwendet werden, da sie bereits die vollständige Antwort zurückgibt, was uns ermöglicht, jegliche Fehlerbehandlung durchzuführen, die wir benötigen.

Zum Beispiel können wir die gleiche 404-Fehlerbehandlung, die im vorherigen Abschnitt durchgeführt wurde, mit exchange() implementieren:

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

Wir haben die Fehlerbehandlungslogik in diesem Beispiel manuell mit if-else-Anweisungen erstellt. Der Einsatz von Handlern verringert zwar die Lesbarkeit des Codes, ermöglicht uns jedoch den Zugriff auf Header aus der Antwort, wie z.B. den ETag, was mit der retrieve()-Methode ansonsten nicht möglich ist.

Lassen Sie uns ein praktischeres Beispiel ausprobieren, bei dem wir eine echte API aufrufen.

7. HTTP-Anfragen an die OpenAI-API stellen

Rest-API ist eine großartige Möglichkeit, mit einem Drittanbieterdienst zu interagieren. In diesem Abschnitt werden wir die OpenAI-API verwenden, um mithilfe des ChatGPT-Modells Text zu generieren. Wir werden den RestClient nutzen, um eine POST-Anfrage an die OpenAI-API zu senden und die Antwort zu erhalten.

Zuerst müssen wir einen API-Schlüssel von der OpenAI-Website erhalten. Es ist eine gute Praxis, den API-Schlüssel nicht im Code zu hinterlegen und stattdessen Umgebungsvariablen oder eine sichere Methode zur Speicherung zu nutzen. Für dieses Beispiel nehmen wir an, dass wir eine Umgebungsvariable namens OPENAI_API_KEY haben, die den API-Schlüssel enthält. Um sie auszulesen, verwenden wir einfach die Methode getenv() aus der Klasse System.

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

Als nächstes erstellen wir einige Hilfs-record-Klassen, um die Anfragen- und Antwortstrukturen darzustellen, die von der OpenAI-API benötigt werden:


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

Nun erstellen wir eine RestClient-Instanz mit den notwendigen Headern für die Authentifizierung und den Inhaltstyp:

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

Schließlich senden wir eine POST-Anfrage an die OpenAI API, um mit dem ChatGPT-Modell einen Text zu generieren:

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

Wenn alles gut läuft, werden wir die Antwort von ChatGPT sehen:

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."

Dieses Beispiel zeigt, wie man den RestClient verwendet, um mit einer realen API, wie der von OpenAI bereitgestellten, zu interagieren, um komplexe JSON-Strukturen und Authentifizierungsanforderungen sauber und typsicher zu handhaben.

8. Fazit

In diesem Artikel haben wir den neuen RestClient kennengelernt, der in der Version 6.1 des Spring Frameworks im neuen RestClient-API zur Durchführung von HTTP-Anfragen eingeführt wurde. Wir haben gelernt, wie man einen RestClient erstellt und konfiguriert, HTTP-Anfragen (GET, POST, PUT, DELETE) durchführt, Fehler behandelt und mit realen APIs wie OpenAI arbeitet.

Wir können den vollständigen Code auf Github finden.

Verwandte Artikel

Testcontainers in Spring Boot Integrationstests
Spring Testcontainers Testing
Werte aus einer Properties-Datei in Spring abrufen
Spring Properties Basics
Rückgabe von HTTP 4XX-Fehlern in einer Spring-Anwendung
Spring HTTP