5 Möglichkeiten für HTTP-Anfragen in Java

July 21, 2020
Autor:in:
Prüfer:in:
Diane Phan
Twilion

5 Möglichkeiten für HTTP-Anfragen in Java

[Quelle des Bilds aus der Überschrift: Iron in the Butterfly Nebula, NASA Astronomy Picture of the Day, 21. Juli 2020 (angepasst)]

Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von 5 ways to make HTTP requests in Java. Während wir unsere Übersetzungsprozesse verbessern, würden wir uns über Dein Feedback an help@twilio.com freuen, solltest Du etwas bemerken, was falsch übersetzt wurde. Wir bedanken uns für hilfreiche Beiträge mit Twilio Swag :)

Das Durchführen von HTTP-Anfragen ist eine der wichtigsten Aufgaben des modernen Programmierens. Beim Erlernen einer neuen Programmiersprache gehören HTTP-Anfragen oft zu den Dingen, mit denen man sich als Erstes befasst. Java-Programmierern stehen dazu viele Möglichkeiten zur Verfügung – JDK-Kernbibliotheken sowie auch Bibliotheken von Drittanbietern. In diesem Blog stelle ich meine bevorzugten Java HTTP-Clients vor. Wenn ihr andere verwendet, ist das auch gut! Erzählt mir gerne, welches eure Favoriten sind. In diesem Blog gehe ich auf folgende Themen ein:

CORE JAVA:
  • HttpURLConnection
  • HttpClient
BELIEBTE BIBLIOTHEKEN:
  • ApacheHttpClient
  • OkHttp
  • Retrofit

Für die Codebeispiele werde ich die API Astronomy Picture of the Day aus dem NASA API-Portal nutzen, und der Code an sich ist komplett auf GitHub zu finden, in einem auf Java 11 basierten Projekt.

Core Java-APIs für das Stellen von HTTP-Anfragen

Seit Java 1.1 enthalten die Kernbibliotheken aus dem JDK einen HTTP-Client. In Java 11 ist ein neuer Client hinzugekommen. Einer dieser beiden Clients könnte eine gute Option sein, wenn uns das Hinzufügen zusätzlicher Abhängigkeiten zu einem Projekt Sorgen bereitet.

Java 1.1 HttpURLConnection

Gleich zu Beginn die große Frage: Sollen Akronyme in Class-Namen nun groß oder klein geschrieben werden? Wichtig ist, dass wir konsistent vorgehen. Wie auch immer, wir schließen nun unsere Augen und versetzen uns ins Jahr 1997 zurück. „Titanic“ brach im Kino alle Kassenrekorde und inspirierte Tausende von Memes, und die Spice Girls eroberten mit ihrem Bestseller-Album die Charts. Das zweifellos wichtigste Ereignis des Jahres war jedoch, dass Java 1.1 um HttpURLConnection erweitert wurde.  Hier zeige ich euch nun, wie wir mit HttpURLConnection eine GET -Anfrage stellen, um die APOD-Daten abzurufen:

// Create a neat value object to hold the URL
URL url = new URL("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY");

// Open a connection(?) on the URL(??) and cast the response(???)
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

// Now it's "open", we can set the request method, headers etc.
connection.setRequestProperty("accept", "application/json");

// This line makes the request
InputStream responseStream = connection.getInputStream();

// Manually converting the response body InputStream to APOD using Jackson
ObjectMapper mapper = new ObjectMapper();
APOD apod = mapper.readValue(responseStream, APOD.class);

// Finally we have the response
System.out.println(apod.title);

[vollständiger Code auf GitHub]

Das Ganze wirkt vergleichsweise umständlich, und die Reihenfolge der Aktionen ist verwirrend (warum werden die Header beispielsweise nach dem Öffnen der URL festgelegt?). Komplexere Anfragen mit POST-Bodies bzw. benutzerdefinierte Timeouts usw. sind zwar möglich, aber aus meiner Sicht ist diese API überhaupt nicht intuitiv.

Wofür würde man HttpURLConnection also eigentlich nutzen? Für alle, die Clients mit älteren Java-Versionen unterstützen und keine Abhängigkeit hinzufügen können, ist HttpURLConnection eventuell eine gute Wahl. Ich vermute, dass dies nur eine kleine Minderheit von Entwicklern betrifft, aber in älteren Setups stößt man vielleicht noch auf diese Codebasis. Wer Interesse an moderneren Ansätzen hat, sollte weiterlesen.

Java 11 HttpClient

Mehr als 20 Jahre nach der Einführung von HttpURLConnection stürmte Black Panther in die Kinocharts, und Java 11 wurde um einen neuen HTTP-Client bereichert: java.net.http.HttpClient. Dieser zeichnet sich durch eine deutlich logischere API aus und kann nicht nur HTTP/2, sondern auch Websockets verarbeiten. Zudem bietet er die Möglichkeit, über die API CompletableFuture synchrone oder asynchrone Anfragen zu stellen.

Bei HTTP-Anfragen möchte ich in 99 von 100 Fällen in der Lage sein, den Response-Body in meinen Code auszulesen. Wenn eine Bibliothek dies nicht leicht möglich macht, kommt bei mir keine Freude auf. HttpClient akzeptiert einen BodyHandler, mit dem eine HTTP-Response in eine Klasse unserer Wahl umgewandelt werden kann. Es gibt integrierte Handler: Stringbyte[] für binäre Daten, Stream<String> zum Aufteilen nach Zeilen sowie einige andere. Wir können aber auch eigene Handler definieren. Und das kann sich als hilfreich erweisen, weil es zum Beispiel keinen integrierten BodyHandler für das Parsen von JSON gibt. Ich habe (hier) mit Jackson gemäß einem Beispiel aus Java Docs einen solchen Handler geschrieben. Der Code gibt einen Supplier für die APOD-Klasse zurück, d. h. wir führen .get() aus, wenn wir das Ergebnis abrufen wollen.

Das hier ist eine synchrone Anfrage:

// create a client
var client = HttpClient.newHttpClient();

// create a request
var request = HttpRequest.newBuilder(
       URI.create("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"))
   .header("accept", "application/json")
   .build();

// use the client to send the request
var response = client.send(request, new JsonBodyHandler<>(APOD.class));

// the response:
System.out.println(response.body().get().title);

Bei einer asynchronen Anfrage werden client und request auf dieselbe Art ausgeführt, und anschließend verwenden wir den Befehl .sendAsync statt .send:

// use the client to send the request
var responseFuture = client.sendAsync(request, new JsonBodyHandler<>(APOD.class));

// We can do other things here while the request is in-flight

// This blocks until the request is complete
var response = responseFuture.get();

// the response:
System.out.println(response.body().get().title);

[vollständiger Code auf GitHub]

Java HTTP-Client-Bibliotheken von Drittanbietern

Keine Sorge, falls die integrierten Clients für eure Zwecke nicht geeignet sind! Es gibt jede Menge Bibliotheken, mit denen ihr die für euer spezifisches Projekt nötigen Aufgaben erledigen könnt.

Apache HttpClient

Die Apache Software Foundation HTTP-Clients gibt es bereits seit geraumer Zeit. Sie sind weit verbreitet und bilden die Grundlage vieler High-Level-Bibliotheken. Ihre Geschichte ist ein wenig verwirrend. Der alte Commons HttpClient wird nicht weiterentwickelt, und die neue Version (ebenfalls HttpClient genannt), läuft unter dem HttpComponents-Projekt. Version 5.0 wurde Anfang 2020 veröffentlicht und bietet nun Unterstützung für HTTP/2. Die Bibliothek unterstützt sowohl synchrone als auch asynchrone Anfragen.

Insgesamt handelt es sich eher um eine Low-Level-API, d. h., wir müssen viel selbst implementieren. Mit dem folgenden Code wird die NASA-API aufgerufen. Die Verwendung ist nicht allzu schwierig, aber ich habe die Fehlerbehandlung, die bei Produktionscode nötig wäre, größtenteils übersprungen. Und auch hier musste ich Jackson-Code verwenden, um die JSON-Antwort zu parsen. Eventuell empfiehlt es sich auch, ein Logging-Framework zu konfigurieren, um stdout-Warnungen zu vermeiden (eigentlich keine große Sache, aber doch irgendwie ärgerlich). Der Code sieht jedenfalls so aus:

ObjectMapper mapper = new ObjectMapper();

try (CloseableHttpClient client = HttpClients.createDefault()) {

   HttpGet request = new HttpGet("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY");

   APOD response = client.execute(request, httpResponse ->
       mapper.readValue(httpResponse.getEntity().getContent(), APOD.class));

   System.out.println(response.title);
}

[vollständiger Code auf GitHub]

Apache bietet zahlreiche weitere Beispiele für Anfragen des Typs sync und async (synchrone und asynchrone Anfragen).

OkHttp

OkHttp ist ein HTTP-Client von Square mit vielen nützlichen integrierten Funktionen, darunter die automatische Verarbeitung von GZIP, Antwort-Caching, erneute Versuche oder Fallbacks auf andere Hosts bei Netzwerkfehlern sowie Unterstützung für HTTP/2 und WebSocket. Die API ist gut strukturiert, allerdings gibt es keine integrierte Funktion zum Parsen von JSON-Antworten, daher habe ich auch hier wieder über Jackson Code zum Parsen von JSON hinzugefügt:

ObjectMapper mapper = new ObjectMapper();

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
   .url("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")
   .build(); // defaults to GET

Response response = client.newCall(request).execute();

APOD apod = mapper.readValue(response.body().byteStream(), APOD.class);

System.out.println(apod.title);

[vollständiger Code auf GitHub]

Das passt also, aber die wahre Leistung von OkHttp wird erst klar, wenn man es mit Retrofit kombiniert.

Retrofit

Retrofit ist eine weitere Bibliothek von Square, die auf OkHttp aufbaut. In Kombination mit den Low-Level-Funktionen von OkHttp bietet Retrofit die Möglichkeit, Java-Klassen zu erstellen. Dabei werden die HTTP-Details extrahiert, und es ergibt sich daraus eine nette Java-freundliche API.

Als Erstes müssen wir eine Schnittstelle erstellen, die deklariert, welche Methoden für den Aufruf an die APOD-API verwendet werden sollen. Anhand von Annotationen wird definiert, welchen HTTP-Anfragen sie entsprechen:

public interface APODClient {
   @GET("/planetary/apod")
   @Headers("accept: application/json")
   CompletableFuture<APOD> getApod(@Query("api_key") String apiKey);
}

Der Rückgabetyp CompletableFuture<APOD> zeigt an, dass es sich hier um einen asynchronen Client handelt. Square stellt weitere Adapter bereit, wir können aber auch einen eigenen programmieren. Eine Schnittstelle wie diese hilft, den Client für Tests zu simulieren, und das gefällt mir gut.

Nach dem Deklarieren der Schnittstelle weisen wir Retrofit an, eine Implementierung zu erstellen, mit der wir Anfragen für eine bestimmte Basis-URL stellen können. Bei Integrationstests ist es praktisch, dass man die Basis-URL wechseln kann. Der Code, mit dem wir den Client generieren, sieht wie folgt aus:

Retrofit retrofit = new Retrofit.Builder()
   .baseUrl("https://api.nasa.gov")
   .addConverterFactory(JacksonConverterFactory.create())
   .build();

APODClient apodClient = retrofit.create(APODClient.class);

CompletableFuture<APOD> response = apodClient.getApod("DEMO_KEY");

// do other stuff here while the request is in-flight

APOD apod = response.get();

System.out.println(apod.title);

[vollständiger Code auf GitHub]

API-Authentifizierung

Falls für unsere Schnittstelle mehrere Methoden existieren, die alle einen API-Schlüssel benötigen, können wir dies konfigurieren, indem wir dem zugrundeliegenden OkHttpClient einen HttpInterceptor hinzufügen. Der benutzerdefinierte Client kann dem Retrofit.Builder hinzugefügt werden.  Der Code zum Erstellen des benutzerdefinierten Client sieht wie folgt aus:

private OkHttpClient clientWithApiKey(String apiKey) {
   return new OkHttpClient.Builder()
           .addInterceptor(chain -> {
               Request originalRequest = chain.request();
               HttpUrl newUrl = originalRequest.url().newBuilder()
                   .addQueryParameter("api_key", apiKey).build();
               Request request = originalRequest.newBuilder().url(newUrl).build();
               return chain.proceed(request);
           }).build();
}

[vollständiger Code auf GitHub]

Außer für die ganz einfachen Szenarien gefällt mir diese Art von Java-API sehr gut. Klassen zu entwerfen, die Remote-APIs verkörpern, ist eine nette Abstraktion, die sich auch gut mit Dependency Injection kombinieren lässt. Und es ist wirklich genial, dass Retrofit diese Klassen auf der Basis eines anpassbaren OkHttp-Clients für uns erstellt.

Weitere HTTP-Clients für Java

Ich habe diesen Blog auf Twitter geteilt und mich sehr über die rege Diskussion zu HTTP-Clients gefreut, die sich daraus ergeben hat. Sollten die oben erwähnten Optionen nicht ganz euer Fall sein, werft einfach einen Blick auf diese Vorschläge:

  • REST Assured – ein HTTP-Client, der auf das Testen von REST-Services ausgelegt ist. Er bietet eine sogenannte Fluent Interface für das Stellen von Anfragen sowie hilfreiche Methoden, um Behauptungen zu Antworten aufzustellen.
  • cvurl – ein Wrapper für den Java 11 HttpClient, der bei komplexeren Anfragen einige der Herausforderungen beseitigt.
  • Feign – ähnlich wie Retrofit kann auch Feign anhand annotierter Schnittstellen Klassen erstellen. Feign ist sehr flexibel und bietet verschiedene Optionen zum Erstellen und Auslesen von Anfragen, Kennzahlen, erneuten Versuchen u. v. m.
  • Spring RestTemplate (synchrone) und WebClient (asynchrone) Clients – wer Spring für alle Teile seines Projekts verwendet hat, sollte konsequent bei diesem Ökosystem bleiben. Baeldung vergleicht die beiden Clients in einem Artikel.
  • MicroProfile Rest Client – ein weiterer Client des Typs „Erstellen einer Klasse anhand einer annotierten Schnittstelle“. Dieser Client ist insofern interessant, weil man über die gleiche Schnittstelle auch einen Web-Server erstellen kann. So hat man die Gewissheit, dass Client und Server aufeinander abgestimmt sind. Wenn wir also für einen bestimmten Dienst sowohl einen Server als auch einen Client erstellen wollen, könnte dies eine gute Option sein.

Zusammenfassung

Bei der Java-Entwicklung steht eine große Auswahl an HTTP-Clients zur Verfügung. Für einfache Entwicklungsszenarien würde ich den integrierten java.net.http.HttpClient empfehlen. Für komplexere Anwendungsfälle oder wenn die HTTP-APIs als Teil einer größeren Anwendung als Java-Klassen abstrahiert werden soll, bietet sich Retrofit oder Feign an.  Viel Spaß beim Programmieren, ich bin gespannt auf eure Ergebnisse!

📧 mgilliard@twilio.com

🐦 @MaximumGilliard