Verarbeiten von Webhooks mit Java, Spring Cloud Function und Azure Functions

March 08, 2021
Autor:in:
Prüfer:in:
Liz Moy
Twilion

Verarbeiten von Webhooks mit Java, Spring Cloud Function und Azure Functions


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Handling Webhooks with Java, Spring Cloud Function and Azure Functions. 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 :)

Wenn du über die Twilio-API eine SMS oder eine WhatsApp-Nachricht senden möchtest, ist das dafür erforderliche Programmieren unkompliziert: Du rufst die API auf und die Nachricht wird gesendet. Beim Antworten auf eingehende Nachrichten sieht die Sache schon anders aus. Wenn die Nachricht eintrifft, muss Twilio wissen, wie sie verarbeitet werden soll. Wir verwenden dafür ein Verfahren namens Webhook. Du konfigurierst deine Twilio-Nummer mit einer URL, und die Plattform stellt eine Anfrage und liefert Details zur eingehenden Nachricht. Was als Nächstes passiert, richtet sich nach der Reaktion deiner Anwendung.

Du brauchst also für deine Anwendung eine öffentliche URL, die wiederum gehostet werden muss. In den letzten Jahren ist die Beliebtheit von serverlosen Plattformen für diese Zwecke gestiegen, weil sie deutlich weniger Kopfzerbrechen bereiten als das typische Hosting. Du erstellst Code zur Verarbeitung einer einzelnen Anfrage, und die Plattform übernimmt das Weiterleiten, Skalieren und die meisten anderen Schritte. Auch bei Java ist dieser Trend zu beobachten. Spring Cloud Function wurde konzipiert, um Java-Entwicklern ein konsistentes Programmiermodell für serverlose Plattformen bereitzustellen. Dabei können sie auch auf andere beliebte Funktionen von Spring zurückgreifen, z. B. Dependency Injection.

In diesem Blog-Beitrag zeige ich, wie man mit Spring Cloud Function eine serverlose Java-Funktion erstellt und sie auf Azure Functions bereitstellt. Die Funktion dient als Webhook, über den Twilio angewiesen wird, wie die Reaktion auf eine eingehende SMS aussehen sollte. Relativ einfach also, aber wenn du erstmal eine serverlose Funktion zum Reagieren auf SMS hast, kannst du das gesamte Potenzial von Java nutzen.

Voraussetzungen

Um mitzuentwickeln, benötigst du:

Wenn du gleich zum Ende springen möchtest, findest du den vollständigen Code für dieses Projekt hier auf GitHub. Wenn du wissen möchtest, wie der Code geschrieben wird, lies weiter!

Los geht's

Ich verwende immer den Spring Initializr, um ein neues Spring-Projekt zu starten. Mit diesem Link werden ein paar wichtige Aspekte vorab ausgewählt, darunter die Spring Cloud Function-Abhängigkeit.

Generiere das Projekt, entpacke es und öffne es in deiner IDE. Die Spring-Vorlage spart viel Zeit, und du kannst gleich mit dem Programmieren beginnen. Erstelle ein neues Paket mit dem Namen com.example.webhooks.functions und füge ihm eine Klasse namens WebhookHandler hinzu.

package com.example.webhooks.functions;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.function.Function;

@Component
public class WebhookHandler {

        @Bean
        public Function<String, String> respondToSms(){
            return String::toUpperCase;
        }
}

[Code auf GitHub]

Beachte bitte, dass die Funktion respondToSms keine Argumente benötigt und eine java.util.Function zurückgibt, die den String in einen anderen String umwandelt – und das ist die eigentliche Funktion, die die Anfragen verarbeitet. Ich habe für den Anfang einen Methodenverweis verwendet. Wenn du später etwas Komplexeres brauchst, kannst du ihn in einen Lambda-Ausdruck ändern.

Diese Funktion ist eigentlich für die Cloud gedacht, aber sie kann auch lokal ausgeführt werden. Füge dazu die Abhängigkeit spring-cloud-starter-function-web unter <dependencies> in der Datei pom.xml hinzu, die sich im Stammverzeichnis deines Projektverzeichnisses befindet:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-function-web</artifactId>
</dependency>

Führe in einem Terminal im Stammverzeichnis des Projekts ./mvnw spring-boot:run aus, warte auf das ASCII-Art Spring-Logo und teste es dann mit diesem Befehl in einem anderen Terminal-Fenster:

curl -H "Content-Type: text/plain" localhost:8080/respondToSms -d 'hello function'

 Als Antwort wird „HELLO FUNCTION“ angezeigt, ganz so, wie man es vom Befehl String::toUpperCase erwarten würde.

Screenshot des Curl-Befehls aus dem Code, mit Ausgabe in Großbuchstaben

Wenn du damit zufrieden bist, entfernst du die Abhängigkeit spring-cloud-starter-function-web, weil sie ansonsten die Bereitstellung dieses Codes auf einer serverlosen Plattform verhindern würde.

Eine noch nützlichere Funktion

Du kannst weitere Klassen hinzufügen und sie mit Spring DI-Annotationen wie @Bean und @Autowired miteinander verbinden. Darüber hinaus kannst du pom.xml um zusätzliche Abhängigkeiten erweitern, um deine Funktion noch nützlicher zu gestalten. Probiere das gerne aus – dank der in diesem Beitrag verwendeten Maven-Konfiguration werden die Funktionen alle bei der Bereitstellung auf der serverlosen Plattform funktionieren, aber ich gehe in diesem Post nicht weiter darauf ein.

Bereitstellen in Azure Functions

Es wird Zeit, dass ich mein Versprechen aus der Einleitung einhalte: Wir stellen diese Funktion in Azure Functions bereit, um dem zugehörigen Code eine öffentliche URL zuzuweisen.

Zum Bereitstellen in Azure brauchen wir einen Adapter, der die Spring Cloud Function als Azure Function verpackt. Dafür sind eine neue Abhängigkeit und eine neue Klasse erforderlich.

Du musst pom.xml die folgende Abhängigkeit hinzufügen:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-adapter-azure</artifactId>
</dependency>

Erstelle ein neues Paket mit dem Namen com.example.webhooks.azure und lege darin eine Klasse namens AzureFunctionWrapper an. Im Prinzip spielt der Name keine Rolle, aber das sind die Namen, für die ich mich entschieden habe. Diese Klasse dient zur Erweiterung von AzureSpringBootRequestHandler<String, String> – die Typenparameter entsprechen denen der Funktion respondToSms. Ebenfalls benötigt wird eine Methode, die mit @FunctionName versehen wird. Der betreffende Wert stimmt mit dem Namen der Methode der Spring Cloud Function überein, wie hier gezeigt:

public class AzureFunctionWrapper extends AzureSpringBootRequestHandler<String, String> {

        @FunctionName("respondToSms")
        public HttpResponseMessage execute(
            @HttpTrigger(
                name = "request",
                methods = {HttpMethod.GET, HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request, ExecutionContext context) {

            return request
                .createResponseBuilder(HttpStatus.OK)
                .body(handleRequest(request.getQueryParameters().get("Body"), context))
                .header("Content-Type", "text/plain")
                .build();
        }
}

[Code auf GitHub, inklusive der Importanweisungen]

Es sind nun ein paar Konfigurationen nötig, um alles miteinander zu verbinden:

  • Erstelle eine Verzeichnis namens azure in src/main und füge host.json und local.settings.json hinzu. Beide kannst du von hier kopieren. Du brauchst diese Dateien nicht zu bearbeiten.
  • Füge diesen großen Brocken XML in pom.xml unter </dependencies> hinzu und ersetze dabei die Abschnitte <dependencyManagement> und  <build>, die sich bereits dort befinden. Dadurch wird die Konfiguration für das Maven Plugin für Azure Functions hinzugefügt. Damit kannst du deine Funktion über die Befehlszeile programmieren und bereitstellen. Eine Bearbeitung ist nicht erforderlich.

Abschließend fügst du dem Abschnitt <properties> von pom.xml einige Werte hinzu:

<spring.boot.wrapper.version>1.0.26.RELEASE</spring.boot.wrapper.version>

<azure.functions.java.library.version>1.4.0</azure.functions.java.library.version>
<azure.functions.maven.plugin.version>1.9.2</azure.functions.maven.plugin.version>
<stagingDirectory>${project.build.directory}/azure-functions/${functionAppName}</stagingDirectory>

<!-- customize these three properties depending to control the Azure deployment -->
<functionResourceGroup>twilio-spring-function-resource-group</functionResourceGroup>
<functionAppName>twilio-spring-function</functionAppName>
<functionAppRegion>westeurope</functionAppRegion>

<!-- this property has to match the name of the class from the Spring Initializr -->
<start-class>com.example.webhooks.HandlingTwilioWebhooksUsingSpringCloudFunctionApplication</start-class>

[Code auf GitHub ansehen]

Nach der Installation der Azure Functions Core Tools kannst du diese Funktion erneut lokal ausführen, wobei du diesmal statt der Spring-Tools die Azure-Tools verwendest. Der Befehl lautet:

./mvnw clean package azure-functions:run

Hinweis: Mit diesem Befehl werden zahlreiche Abhängigkeiten heruntergeladen, daher kann seine Ausführung eine Weile dauern. Der Befehl ist abgeschlossen, wenn du in der Ausgabe Folgendes siehst: respondToSms: [GET,POST] http://localhost:7071/api/respondToSms. Bei Maven werden die heruntergeladenen Abhängigkeiten zwischengespeichert, d. h., nach dem ersten Ausführen laufen alle weiteren Ausführungen deutlich schneller.

Teste nun Folgendes:

curl 'http://localhost:7071/api/respondToSms?Body=hello+azure+function'

Hinweis: Die Anfrage sieht anders aus als zuvor – so stellt Azure die Funktion nach der Bereitstellung zur Verfügung. Die Antwort erfolgt natürlich weiterhin über String::toUpperCase:

Screenshot des Curl-Befehls aus dem Code, mit Ausgabe in Großbuchstaben

Bereitstellen in Azure

Die gesamte Konfiguration für den Upload und die Konfiguration in Azure liegt nun vor, du musst also nur den folgenden Befehl ausführen:

./mvnw clean package azure-functions:deploy

Eventuell wirst du gebeten, dich über deinen Browser bei Azure anzumelden. Wenn die Bereitstellung abgeschlossen ist, siehst du die vollständige öffentliche URL der Funktion:

Screenshot des letzten Teils der Maven-Ausgabe mit „BUILD SUCCESS“ in grüner Fettschrift.

Über curl sendest du genau wie zuvor eine Anfrage an die Funktion in Azure – du ersetzt dabei einfach die Localhost-URL durch die öffentliche Azure-URL.

Screenshot des Curl-Befehls gegen die öffentliche Azure-URL.

Es scheint zu funktionieren – perfekt!

Serverlose Kaltstarts

Ist dir aufgefallen, dass die Reaktion beim ersten Mal recht langsam war? Das kann bei serverlosen Plattformen passieren, wenn eine Funktion eine Zeit lang nicht aufgerufen wurde. Dieses Phänomen wird auch als „Kaltstart“ bezeichnet. Die Häufigkeit und das Ausmaß der Kaltstarts hängen von zahlreichen Faktoren ab. Twilio wartet bei einer Webhook-HTTP-Anfrage 15 Sekunden lang auf eine Antwort. Bei mir ist es noch nicht vorgekommen, dass eine Azure Function so langsam war, dass es bei Twilio einen Timeout gab, und diese Studie zeigt, dass der Großteil der Java-Funktionen in unter 15 Sekunden ausgeführt wird. Wenn du das Kaltstartproblem ganz vermeiden möchtest, könnte der Azure Function Premium Plan eine Lösung sein. Er bietet garantierte Warmstarts, allerdings gegen eine Gebühr. Alle anderen Aspekte dieses Blog-Beitrags werden bequem von der kostenlosen Version von Azure abgedeckt.

Konfigurieren von Twilio für den Einsatz deiner SMS-Antwortfunktion

Falls du noch keines hast, erstelle ein kostenloses Twilio-Konto und hol dir eine Telefonnummer. Auf der Seite zum Konfigurieren der Telefonnummer wählst du als Verhalten für „a message comes in“ eine GET-Anfrage an die Azure Functions-URL aus:

Der Abschnitt der Twilio-Konsole, in dem wir die Webhook-URL für eingehende SMS auswählen.

Das war's  Teste deine Funktion jetzt mit einer SMS:

Screenshot der Android Messaging-App, die eine ausgehende SMS mit „Hello Azure“ und den Erhalt des gleichen Texts als Antwort zeigt, aber in Großbuchstaben.

Zusammenfassung

Falls du mitentwickelt hast, herzlichen Glückwunsch! Du hast jetzt eine funktionierende Spring Cloud Function in Azure, die mit Twilio integriert ist und Antworten auf eine SMS liefert. Das kannst du nun als Grundlage für weitere nützliche Funktionen nutzen! Twilio übermittelt neben dem Body auch die  From-Nummer der eingehenden Nachricht und andere Metadaten, sodass du personalisierte Antworten erstellen, Informationen in deiner Kundendatenbank oder anderen Web-APIs suchen kannst – lass deiner Kreativität freien Lauf.

Wenn du mit serverlosem Java und Twilio arbeitest, würde ich gerne mehr darüber erfahren – ich freue mich, von dir zu hören. Viel Spaß beim Programmieren!

 mgilliard@twilio.com

 @MaximumGilliard