Asynchrone API-Anfragen in Java über CompletableFutures

November 12, 2020
Autor:in:
Prüfer:in:

Asynchrone API-Anfragen in Java über CompletableFutures


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von How to make asynchronous API requests in Java using CompletableFutures. 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 :)

Java 8 wurde 2014 veröffentlicht, und mit der Veröffentlichung wurden zahlreiche neue Sprachfunktionen wie Lambda-Ausdrücke und die Streams-API eingeführt. Seit dem Jahr 2014 hat sich allerdings viel verändert: Java ist mittlerweile schon bei Version 15 angelangt, aber wie Branchenumfragen zeigen, wird Version 8 immer noch am häufigsten verwendet. Nur einige wenige Entwickler nutzen Version 7 oder älter.

Im Oktober dieses Jahres wurde mit Version 8.0.0 die Twilio Java-Hilfebibliothek für die Verwendung von Java 8-Funktionen aktualisiert. Diese neue Hauptversion spiegelt die Tatsache wider, dass die Bibliothek keine Unterstützung mehr für Java 7 bietet.

Eine Java 8-API, die manchmal übersehen wird, ist die CompletionStage-API, auf die normalerweise über die CompletableFuture-Klasse zugegriffen wird. Mit der CompletionStage-API können Programmierer Pipelines für asynchrone Datenvorgänge definieren. Die API übernimmt dabei auch die Verarbeitung des asynchronen Verhaltens. Wir definieren damit also, was geschehen soll, und Java kümmert sich darum, wann das gewünschte Ereignis eintreten kann.

In diesem Beitrag erkläre ich, wie wir CompletableFutures mit der neuen Twilio-Hilfebibliothek verwenden können. Das gleiche Prinzip lässt sich auch auf jeden anderen asynchronen Code übertragen. Der HttpClient von Java 11 verfügt beispielsweise über asynchrone Methoden, die CompletableFuture-Instanzen zurückgeben.

Synchroner und asynchroner Code

Wenn wir die Twilio-API aufrufen, um eine SMS zu senden, sieht der Code in etwa so aus:

Message msg = Message.creator(
        MY_CELLPHONE_NUMBER,
        MY_TWILIO_NUMBER,
        "Hoot Hoot 🦉")
    .create();

[vollständiger Code auf GitHub]

Dieser Code ruft die Twilio-API auf, reiht die SMS in die Warteschlange ein und gibt ein Message-Objekt zurück, das Informationen zur Antwort einschließlich die SID der Nachricht enthält. Diese SID kann später verwendet werden, um nachzuschlagen, was mit der Nachricht passiert ist. Die tatsächliche Arbeit findet im Hintergrund statt, wie zum Beispiel das Senden von Anfrage und Antwort über das Internet. Auf meinem Computer dauert das Ausführen dieses Aufrufs etwa eine Sekunde, und dieser Code wartet, bis die Message verfügbar ist, bevor fortgefahren wird. Das nennen wir einen synchronen (oder „blockierenden“) Aufruf.

Während die API-Anfrage jedoch in Bearbeitung ist, könnte unser Code andere Aufgaben ausführen. Wie wäre es also, wenn wir den Aufruf asynchron machen? Das würde bedeuten, dass unser Code weiterarbeiten könnte und wir die Message einfach später abrufen, wenn wir sie wirklich brauchen. Um das zu erreichen, ersetzen wir einfach .create() durch .createAsync().

Futures

Die Methode .createAsync() gibt eine Future zurück. Ähnlich wie bei Promises in anderen Sprachen handelt es sich bei „Futures“ um Objekte, die ein Ergebnis enthalten, sobald es bereitsteht. Die Arbeit findet in einem Hintergrundthread statt, und wenn wir das Ergebnis brauchen, können wir eine Methode am Future aufrufen, um das Ergebnis abzurufen. Wenn wir diese Methode aufrufen, müssen wir möglicherweise noch warten, aber der Code konnte in der Zwischenzeit andere Aufgaben ausführen.

Ab Version 8.0.0 der Twilio-Hilfebibliothek ist der zurückgegebene „Future“-Typ eine CompletableFuture. Das Ergebnis wird bei diesem Typ mit der Methode .join() abgerufen. Der Code könnte in etwa so aussehen:

CompletableFuture<Message> msgFuture = Message.creator(
        MY_CELLPHONE_NUMBER,
        MY_TWILIO_NUMBER,
        "Hoot Hoot 🦉")
    .createAsync();

System.out.println("This is printed while the request is taking place");

Message msg = msgFuture.join(); // you might have to wait here
System.out.println("Message SID is " + msg.getSid());
Ausgabe des obenstehenden Codes: „This is printed...“ über „Message SID is...“

So weit, so gut. Aber was die CompletionStage-API so besonders macht, ist die Tatsache, dass wir Code-Pipelines erstellen können. Bei diesen Pipelines wird jede Stufe dann ausgeführt, sobald sie bereit ist, ohne dass wir jedes kleine Detail des asynchronen Verhaltens selbst codieren müssen. Das ähnelt zwar der Verwendung von Rückrufen in anderen Sprachen, ist aber bei Weitem flexibler, wie wir anhand von Beispielen noch sehen werden.

Beispiele

Zugegeben, diese Beschreibung mag etwas komplex klingen. Deshalb hier ein paar Beispiele zur Veranschaulichung:

Sequentielle Verkettung der Berechnung

Mit .thenApply() können wir nach dem Abschluss des API-Aufrufs Code ausführen. Die Methode .thenApply() benötigt einen Lambda-Ausdruck oder einen Methodenverweis, der den Wert umwandelt und eine weitere CompletionStage zurückgibt. Dadurch können wir mehrere Aufgaben bei Bedarf miteinander verketten. Wenn wir das Endergebnis abrufen möchten, können wir erneut .join() aufrufen:

CompletableFuture<String> resultFuture =
    Message.creator(MY_CELLPHONE_NUMBER, MY_TWILIO_NUMBER, "Hoot Hoot 🦉")
        .createAsync()
        .thenApply(msg ->    // .thenApply can take a lambda
            writeToDatabase(msg.getTo(), msg.getSid(), msg.getDateCreated())
        ).thenApply(         // .thenApply can also take a method reference
            DatabaseResult::getValue);

System.out.println("Making the API call then writing the result to the DB happen in the background");

System.out.println("The final result was " + resultFuture.join());

[vollständiger Code auf GitHub]

Parallele Ausführung

Erweitern wir unser vorhergehendes Beispiel. Angenommen, wir müssen mehrere API-Anfragen stellen. Diese sind nicht voneinander abhängig, deshalb spielt die Reihenfolge, in der sie ausgeführt werden, keine Rolle. Allerdings müssen wir wissen, wann alle Anfragen abgeschlossen wurden, um z. B. die Ereignisse in eine Datenbank zu schreiben.

Mit CompletableFuture.allOf() können wir planen, dass Code nach dem Abschluss mehrerer CompletionStages ausgeführt wird. Der Lambda-Ausdruck, den wir an .allOf() übergeben, benötigt keine Argumente. Wir verwenden .join() im Text des Lambda-Ausdrucks, um die Ergebnisse jeder Stufe abzurufen:

// makeApiRequestThenStoreResult contains the same code as the previous example
CompletableFuture<String> result1 = makeApiRequestThenStoreResult(CUSTOMER_1);
CompletableFuture<String> result2 = makeApiRequestThenStoreResult(CUSTOMER_2);

// those calls are all happening in the background

CompletableFuture.allOf(result1, result2)
        .thenRun(() ->
            System.out.printf("The final results were: %s and %s",
                              result1.join(), result2.join()));

[vollständiger Code auf GitHub]

Fehlerbehandlung bei CompletionStages

Falls Ausnahmen im asynchronen Code ausgelöst werden, werden diese von der CompletionStage-API aufgegriffen. Für die Behandlung dieser Ausnahmen haben wir mehrere Möglichkeiten. Wenn wir sie überhaupt nicht behandeln, dann könnte durch den Aufruf der Methode .join() eine CompletionException ausgelöst werden, die die ursprüngliche Ausnahme als Ursache enthält.

Eine bessere Möglichkeit der Wiederherstellung ist die Verwendung der Methode .handle(). Dabei stellen wir einen Lambda-Ausdruck bereit, der zwei Argumente benötigt: ein Ergebnis und eine Ausnahme. Wenn die Ausnahme ungleich Null ist, können wir sie hier behandeln. .handle() gibt eine CompletableFuture zurück. Wir können also mit der Verkettung fortfahren oder mit .join() das Ergebnis abrufen:

CompletableFuture<String> msgFuture = makeApiRequestThenStoreResult(MY_CELLPHONE_NUMBER);

msgFuture.handle((s, ex) ->{
    if (ex != null){
        return "Failed: " + ex.getMessage();
    } else {
        return s;
    }
});

// all of the above happens in the background

System.out.println("The final result is " + msgFuture.join());

[vollständiger Code auf GitHub]

Die vollständige CompletionStage-API

Diese kurzen Beispiele kratzen nur an der Oberfläche der CompletionStage-API.

Kratzen an der Oberfläche (oder eher Streicheln)

Es gibt Dutzende von Methoden, um asynchrone Aktionen auf verschiedene Weise miteinander zu verketten oder zu kombinieren.

Weitere Beispiele zu den Verwendungsmöglichkeiten von CompletableFutures findest du in der offiziellen Dokumentation oder in dieser praktischen Liste mit 20 Beispielen.

Zusammenfassung

Die CompletionStage-API von Java 8 gibt uns Java-Entwicklern leistungsstarke Tools an die Hand, um komplexe asynchrone Vorgänge zu definieren, und ist nur eine der zahlreichen Ergänzungen in der neuen Twilio-Java-Hilfebibliothek.

Wenn du Twilio und Java verwendest, würde ich eine Aktualisierung auf die neue Hilfebibliothek empfehlen. Und wenn du mit Twilio und Java entwickelst, würde ich mich freuen, davon zu hören. Ich bin gespannt, von deinen Entwicklungen zu hören.

🐦@MaximumGilliard

📧 mgilliard@twilio.com