Mehrere Rückgabewerte eines HTTP-Requests

Manchmal ist es notwendig, dass ein Webservice mehrere Ergebnisse nacheinander zurückliefert. Zum Beispiel, wenn ein Request abgesetzt wird, der zunächst validiert werden muss, bevor tatsächlich eine Prozessierung beginnt und die Validierung selbst eine längere Zeit dauert. Dann soll der Server nach Möglichkeit den Request direkt beenden und die Validierung im Hintergrund ablaufen. Andernfalls ist die Webseite nicht responsiv, was den Nutzer verunsichern könnte und keine gute User Experience ist. In einem Spring Boot basierten Webservice kann das zum Beispiel mit asynchroner Ausführung und long polling zusammen mit einem Deferred Result erreicht werden, wie im folgenden beschrieben.

Das Szenario

Im folgenden soll ein kleiner Webservice in Spring Boot implementiert werden, der eine Anfrage auslöst, die zwei Stadien durchläuft. Direkt bei der Anfrage wird in einen Zustand INITIALIZING gewechselt, bis nach einigen Sekunden der Zustand PHASE1 erreicht wird. Den Abschluss markiert der Zustand DONE. Die zugehörige Webseite soll jeweils den aktuellen Status anzeigen und bei einem Statuswechsel automatisch aktualisiert werden.

Eine Beispielimplementation des Webservers habe ich auf GitHub hochgeladen. Es kann nach dem Auschecken mit mvn spring-boot:run gestartet werden, der Webservice ist unter http://localhost:8080 erreichbar.

Beispiel-Service für asynchrone Ausführung und Long-Polling

Beispiel-Service für asynchrone Ausführung und Long-Polling

Bei einem Klick auf die Schaltfläche geht es los.

Die serverseitige Implementierung

Zu Beginn wird ein POST-Request über an /request geschickt. Die Methode soll sofort ein Ergebnis zurückliefern, auch wenn die auszuführende Aktion länger dauert. Dazu wird nicht der tatsächliche Ergebnistyp zurückgeliefert sondern ein DeferredResult. Die Webseite kann daraufhin direkt aktualisiert werden, da die Anfrage schnell bearbeitet wird, das tatsächliche Ergebnis ist im Ergebnis erst vorhanden, sobald setResult aufgerufen wird.

@RequestMapping(name = "/request", method = RequestMethod.POST)
@ResponseBody
public DeferredResult request() {
    // Prepare already for the first state change
    DeferredResult result = service.getStatus();
 
    // Actually let the asynchronous service do something
    service.doSomething();
 
    // Return the deferred result that will be set in the above asynchronous call
    return result;
}
Die Anfrage wurde abgesetzt.

Die Anfrage wurde abgesetzt.

Beim Long Polling kann der Webserver verspätet antworten. Typischerweise wird ein Timeout gesetzt, so dass bei einem Verbindungsabbruch eine neue Anfrage gestartet werden kann. Da unklar ist, wie lange die Antwort tatsächlich dauert, sollte mit dem Verbindungsabbruch gerechnet werden. Die Statusabfrage für das Long Polling wird über den Endpunkt /poll durchgeführt. Das Ergebnis wird vom ausführenden Service geholt und zurückgeliefert. Das zurückgelieferte Object vom Typ DeferredResult wird automatisch so lange zurückgehalten, bis der Service über einen Aufruf von setStatus() ein Ergebnis bereitstellt.

@RequestMapping(name = "/poll", method = RequestMethod.GET)
@ResponseBody
public DeferredResult requestStatus() {
    // Simply forward the request to the service
    return service.getStatus();
}

Asynchrone Ausführung der Anfrage

Damit der ganze Prozess funktioniert, muss die Arbeit des Services (service.doSomething() im Beispiel oben) asynchron ablaufen. Das heißt, nach dem Aufruf von doSomething kann direkt etwas zurückgeliefert werden. Spring Boot ermöglicht durch die Annotation @Async, dass eine Methode eines Services automatisch in einem anderen Thread asynchron ausgeführt wird. Die folgende Methode simuliert, dass der Status zunächst auf INITIALIZING gesetzt wird, anschließend eine längere Zeit gearbeitet wird und zuletzt beendet wird sobald der Status auf DONE steht.

@Async
void doSomething() {
    // Initialize the current status and notify any listener that is already present
    currentStatus = LongRequestStatus.INITIALIZING;
    publishStatus(currentStatus);
 
    // Perform some work until the final state is reached.
    do {
        currentStatus = doNecessaryWork(currentStatus);
        publishStatus(currentStatus);
    } while (currentStatus != LongRequestStatus.DONE);
}

Die Methode wird bei einem POST-Request an /request aufgerufen. Den Status der parallel ausgeführten Methode wird mit publishStatus gesetzt. Falls es in der Zwischenzeit eine eventuell vorhandene Long Polling-Anfrage (mit einem Status als DeferredResult) gab, wird der Status gesetzt.

private synchronized void publishStatus(LongRequestStatus nextToReturn) {
    if (waitingResponse != null) {
        waitingResponse.setResult(nextToReturn.toString());
    }
    waitingResponse = null;
}
Die Anfrage wird gerade Bearbeitet.

Die Anfrage wird gerade Bearbeitet.

Eine Polling-Anfrage ruft ausgehend vom Controller die Methode getStatus auf. Diese erzeugt das DeferredResult waitingResponse, das direkt beantwortet wird, wenn bereits Informationen da sind. Andernfalls wird die Anfrage gespeichert und das Ergebnis gegebenenfalls in obiger Methode publishStatus gesetzt.

public synchronized DeferredResult getStatus() {
    waitingResponse = new DeferredResult<>();
 
    // If the request completed, immediately return the status because it will never change
    if (currentStatus == LongRequestStatus.DONE) {
        waitingResponse.setResult(currentStatus.toString());
    }
 
return waitingResponse;

Die Webseite

Auf der Clientseite wird das Statusupdate mit JavaScript durchgeführt. Bei einem Click auf die Schaltfläche wird zunächst wird die Anfrage ausgeführt und bei Erfolg die Long Polling-Abfrage gestartet.

function request() {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", "/request", true);
    xhr.onreadystatechange = function() {
        if(xhr.readyState === 4 && xhr.status === 200) {
            // Do something with the response
            console.log(xhr.responseText);
 
            // Start long polling and get the next status
            poll();
        }
    }
    xhr.send();
}

Die Methode poll sendet die Anfrage an den Server um den aktuellen Status abzuholen. Die Anfrage wird solange erneut ausgeführt, bis das Endergebnis DONE zurückgeliefert wird. Als Timeout ist eine Sekunde gesetzt, was relativ kurz ist und dafür sorgt, dass jede Sekunde erneut beim Server nachgefragt wird.

function poll() {
    var xhr = new XMLHttpRequest();
 
    xhr.open("GET", "/poll", true);
 
    xhr.onreadystatechange = function() {
        if(xhr.readyState == 4 && xhr.status == 200) {
            if (xhr.responseText !== "DONE") {
                // Start the next polling request
                setTimeout(poll, 1000);
            } else {
                // Stop polling
            }
        }
    }
    xhr.send();
}
Die Anfrage wurde komplett bearbeitet.

Die Anfrage wurde komplett bearbeitet.

Was fehlt?

Das Grundgerüst für mehrere konsekutive Ergebnisse, die auf eine Anfrage zurückgegeben werden sollen steht damit. Es schließen sich nun einige Möglichkeiten der Erweiterung an.

Da Server normalerweise mehrere Nutzer haben, sollte es natürlich möglich sein, dass mehrere Anfragen parallel ablaufen. Dazu müssen für alle diese Anfragen die Stati gespeichert werden. Der erste Request kann dann ein Token (zum Beispiel eine UUID) zurückgeben, mit der anschließend beim Polling der Status der passenden Anfrage abgefragt werden kann. Gegebenenfalls muss hier natürlich noch mit Authentifizierung gearbeitet werden, so dass jeder Nutzer nur seine eigenen Requests anfragen kann.

Zuletzt geändert: 16. Februar 2019 um 0:21 Uhr.

Geschrieben von .

a.k.a.

Ich bin Diplom-Informatiker, den es von Herten im wunderschönen Ruhrgebiet nach Berlin verschlagen hat. An der TU Berlin forsche ich nun als wissenschaftlicher Mitarbeiter in der kombinatorischen Optimierung an Graphalgorithmen; nebenbei bringe ich Anfängern Programmieren bei. Ich blogge hier über alles was mich interessiert, vor allem Nerdiges und Reisen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

O:-) B-) :cry: :-* :D :-o :P ;) :O :-/ more »