Leitfaden zu DeferredResult im Frühjahr

1. Übersicht

In diesem Tutorial sehen wir uns an, wie wir die DeferredResult- Klasse in Spring MVC verwenden können, um eine asynchrone Anforderungsverarbeitung durchzuführen .

Die asynchrone Unterstützung wurde in Servlet 3.0 eingeführt und ermöglicht, einfach ausgedrückt, die Verarbeitung einer HTTP-Anforderung in einem anderen Thread als dem Anforderungsempfänger-Thread.

DeferredResult, verfügbar ab Spring 3.2, unterstützt das Auslagern einer lang laufenden Berechnung von einem http-Worker-Thread in einen separaten Thread.

Obwohl der andere Thread einige Ressourcen für die Berechnung benötigt, werden die Arbeitsthreads in der Zwischenzeit nicht blockiert und können eingehende Clientanforderungen verarbeiten.

Das asynchrone Anforderungsverarbeitungsmodell ist sehr nützlich, da es hilft, eine Anwendung bei hohen Lasten gut zu skalieren, insbesondere für E / A-intensive Vorgänge.

2. Setup

Für unsere Beispiele verwenden wir eine Spring Boot-Anwendung. Weitere Informationen zum Booten der Anwendung finden Sie in unserem vorherigen Artikel.

Als Nächstes werden wir sowohl die synchrone als auch die asynchrone Kommunikation mit DeferredResult demonstrieren und vergleichen, wie die asynchrone Kommunikation für Anwendungsfälle mit hoher Last und E / A-Intensität besser skaliert.

3. Blockieren des REST-Service

Beginnen wir mit der Entwicklung eines Standard-Blocking-REST-Service:

@GetMapping("/process-blocking") public ResponseEntity handleReqSync(Model model) { // ... return ResponseEntity.ok("ok"); }

Das Problem hierbei ist, dass der Anforderungsverarbeitungsthread blockiert wird, bis die vollständige Anforderung verarbeitet und das Ergebnis zurückgegeben wird. Bei lang laufenden Berechnungen ist dies eine nicht optimale Lösung.

Um dies zu beheben, können wir Container-Threads besser nutzen, um Client-Anforderungen zu verarbeiten, wie wir im nächsten Abschnitt sehen werden.

4. Nicht blockierende REST mit DeferredResult

Um ein Blockieren zu vermeiden, verwenden wir ein auf Rückrufen basierendes Programmiermodell, bei dem anstelle des tatsächlichen Ergebnisses ein DeferredResult an den Servlet-Container zurückgegeben wird.

@GetMapping("/async-deferredresult") public DeferredResult
    
      handleReqDefResult(Model model) { LOG.info("Received async-deferredresult request"); DeferredResult
     
       output = new DeferredResult(); ForkJoinPool.commonPool().submit(() -> { LOG.info("Processing in separate thread"); try { Thread.sleep(6000); } catch (InterruptedException e) { } output.setResult(ResponseEntity.ok("ok")); }); LOG.info("servlet thread freed"); return output; }
     
    

Die Anforderungsverarbeitung erfolgt in einem separaten Thread. Nach Abschluss rufen wir die setResult- Operation für das DeferredResult- Objekt auf.

Schauen wir uns die Protokollausgabe an, um zu überprüfen, ob sich unsere Threads wie erwartet verhalten:

[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: Received async-deferredresult request [nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: Servlet thread freed [nio-8080-exec-6] java.lang.Thread : Processing in separate thread

Intern wird der Container-Thread benachrichtigt und die HTTP-Antwort an den Client gesendet. Die Verbindung bleibt vom Container (Servlet 3.0 oder höher) geöffnet, bis die Antwort eintrifft oder eine Zeitüberschreitung auftritt.

5. DeferredResult- Rückrufe

Wir können drei Arten von Rückrufen mit einem DeferredResult registrieren: Abschluss, Timeout und Fehlerrückrufe.

Verwenden Sie die Methode onCompletion () , um einen Codeblock zu definieren, der ausgeführt wird, wenn eine asynchrone Anforderung abgeschlossen ist:

deferredResult.onCompletion(() -> LOG.info("Processing complete"));

In ähnlicher Weise können wir onTimeout () verwenden , um benutzerdefinierten Code zu registrieren, der nach Auftreten eines Timeouts aufgerufen werden soll . Um die Bearbeitungszeit für Anforderungen zu begrenzen, können wir während der Erstellung des DeferredResult- Objekts einen Zeitüberschreitungswert übergeben :

DeferredResult
    
      deferredResult = new DeferredResult(500l); deferredResult.onTimeout(() -> deferredResult.setErrorResult( ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT) .body("Request timeout occurred.")));
    

Im Falle von Zeitüberschreitungen legen wir über den bei DeferredResult registrierten Zeitüberschreitungs-Handler einen anderen Antwortstatus fest .

Lassen Sie uns einen Timeout-Fehler auslösen, indem Sie eine Anforderung verarbeiten, die länger als die definierten Timeout-Werte von 5 Sekunden dauert:

ForkJoinPool.commonPool().submit(() -> { LOG.info("Processing in separate thread"); try { Thread.sleep(6000); } catch (InterruptedException e) { ... } deferredResult.setResult(ResponseEntity.ok("OK"))); });

Schauen wir uns die Protokolle an:

[nio-8080-exec-6] com.baeldung.controller.DeferredResultController: servlet thread freed [nio-8080-exec-6] java.lang.Thread: Processing in separate thread [nio-8080-exec-6] com.baeldung.controller.DeferredResultController: Request timeout occurred

Es wird Szenarien geben, in denen die Berechnung mit langer Laufzeit aufgrund eines Fehlers oder einer Ausnahme fehlschlägt. In diesem Fall können wir auch einen onError () - Rückruf registrieren :

deferredResult.onError((Throwable t) -> { deferredResult.setErrorResult( ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("An error occurred.")); });

Im Falle eines Fehlers legen wir bei der Berechnung der Antwort über diesen Fehlerhandler einen anderen Antwortstatus und Nachrichtentext fest.

6. Fazit

In diesem kurzen Artikel haben wir uns angesehen, wie Spring MVC DeferredResult die Erstellung asynchroner Endpunkte vereinfacht.

Wie üblich ist der vollständige Quellcode auf Github verfügbar.