Guida al risultato differito in primavera

1. Panoramica

In questo tutorial, vedremo come possiamo usare la classe DeferredResult in Spring MVC per eseguire l'elaborazione delle richieste asincrone .

Il supporto asincrono è stato introdotto in Servlet 3.0 e, in poche parole, consente di elaborare una richiesta HTTP in un thread diverso dal thread del destinatario della richiesta.

DeferredResult, disponibile dalla Spring 3.2 in poi, aiuta a scaricare un calcolo di lunga durata da un thread http-worker a un thread separato.

Sebbene l'altro thread richiederà alcune risorse per il calcolo, i thread di lavoro non vengono bloccati nel frattempo e possono gestire le richieste dei client in arrivo.

Il modello di elaborazione delle richieste asincrone è molto utile in quanto aiuta a ridimensionare bene un'applicazione durante carichi elevati, soprattutto per le operazioni a uso intensivo di I / O.

2. Configurazione

Per i nostri esempi, useremo un'applicazione Spring Boot. Per maggiori dettagli su come eseguire il bootstrap dell'applicazione, fare riferimento al nostro articolo precedente.

Successivamente, dimostreremo sia la comunicazione sincrona che quella asincrona utilizzando DeferredResult e confronteremo anche il modo in cui uno asincrono si adatta meglio per casi di utilizzo con carico elevato e IO.

3. Blocco del servizio REST

Cominciamo con lo sviluppo di un servizio REST di blocco standard:

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

Il problema qui è che il thread di elaborazione della richiesta è bloccato fino a quando non viene elaborata la richiesta completa e viene restituito il risultato. In caso di calcoli di lunga durata, questa è una soluzione non ottimale.

Per risolvere questo problema, possiamo fare un uso migliore dei thread del contenitore per gestire le richieste dei client, come vedremo nella sezione successiva.

4. REST non bloccante utilizzando DeferredResult

Per evitare il blocco, utilizzeremo un modello di programmazione basato su callback dove invece del risultato effettivo, restituiremo un DeferredResult al contenitore servlet.

@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; }
     
    

L'elaborazione della richiesta viene eseguita in un thread separato e una volta completata richiamiamo l' operazione setResult sull'oggetto DeferredResult .

Diamo un'occhiata all'output del log per verificare che i nostri thread si comportino come previsto:

[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

Internamente, il thread del contenitore riceve una notifica e la risposta HTTP viene consegnata al client. La connessione rimarrà aperta dal contenitore (servlet 3.0 o successivo) fino all'arrivo della risposta o al timeout.

5. Richiami di risultati differiti

Possiamo registrare 3 tipi di callback con un DeferredResult: completamento, timeout e callback di errore.

Usiamo il metodo onCompletion () per definire un blocco di codice che viene eseguito quando una richiesta asincrona viene completata:

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

Allo stesso modo, possiamo usare onTimeout () per registrare il codice personalizzato da invocare una volta che si verifica il timeout. Per limitare il tempo di elaborazione della richiesta, possiamo passare un valore di timeout durante la creazione dell'oggetto DeferredResult :

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

In caso di timeout, stiamo impostando uno stato di risposta diverso tramite il gestore di timeout registrato con DeferredResult .

Attiviamo un errore di timeout elaborando una richiesta che richiede più dei valori di timeout definiti di 5 secondi:

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

Diamo un'occhiata ai log:

[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

Ci saranno scenari in cui il calcolo di lunga durata fallisce a causa di qualche errore o eccezione. In questo caso, possiamo anche registrare un callback onError () :

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

In caso di errore, durante il calcolo della risposta, stiamo impostando uno stato di risposta e un corpo del messaggio diversi tramite questo gestore degli errori.

6. Conclusione

In questo breve articolo, abbiamo esaminato come Spring MVC DeferredResult semplifica la creazione di endpoint asincroni.

Come al solito, il codice sorgente completo è disponibile su Github.