Guide sur les résultats différés au printemps

Guide sur les résultats différés au printemps

1. Vue d'ensemble

Dans ce didacticiel, nous allons examinerhow we can use the DeferredResult class in Spring MVC to perform asynchronous request processing.

Le support asynchrone a été introduit dans Servlet 3.0 et, en termes simples, il permet de traiter une requête HTTP dans un autre thread que celui du récepteur de la requête.

DeferredResult, disponible à partir de Spring 3.2, aide à décharger un calcul de longue durée d'un thread de travail http vers un thread séparé.

Bien que l'autre thread prenne certaines ressources pour le calcul, les threads de travail ne sont pas bloqués entre-temps et peuvent gérer les demandes client entrantes.

Le modèle de traitement des demandes asynchrones est très utile car il permet de bien dimensionner une application lors de fortes charges, en particulier pour les opérations intensives en IO.

2. Installer

Pour nos exemples, nous utiliserons une application Spring Boot. Pour plus de détails sur la façon de démarrer l'application, reportez-vous à nos précédentsarticle.

Ensuite, nous allons démontrer à la fois la communication synchrone et asynchrone à l'aide deDeferredResult and et comparer comment un asynchrone s'adapte mieux aux cas d'utilisation à forte charge et à forte intensité d'E / S.

3. Blocage du service REST

Commençons par développer un service REST de blocage standard:

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

Le problème ici est quethe request processing thread is blocked until the complete request is processed et le résultat sont renvoyés. En cas de calculs longs, il s'agit d'une solution sous-optimale.

Pour résoudre ce problème, nous pouvons mieux utiliser les threads de conteneur pour gérer les demandes des clients, comme nous le verrons dans la section suivante.

4. REST non bloquant utilisantDeferredResult

Pour éviter le blocage, nous utiliserons un modèle de programmation basé sur les rappels où, au lieu du résultat réel, nous retournerons unDeferredResult au conteneur de 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;
}

Le traitement de la requête est effectué dans un thread séparé et une fois terminé, nous invoquons l'opérationsetResult sur l'objetDeferredResult.

Examinons la sortie du journal pour vérifier que nos threads se comportent comme prévu:

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

En interne, le thread de conteneur est notifié et la réponse HTTP est remise au client. La connexion restera ouverte par le conteneur (servlet 3.0 ou version ultérieure) jusqu'à ce que la réponse arrive ou expire.

5. DeferredResult rappels

Nous pouvons enregistrer 3 types de rappels avec un DeferredResult: les rappels d'achèvement, de délai d'expiration et d'erreur.

Utilisons la méthodeonCompletion() pour définir un bloc de code qui est exécuté lorsqu'une requête asynchrone se termine:

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

De même, nous pouvons utiliseronTimeout() pour enregistrer du code personnalisé à invoquer une fois le délai écoulé. Afin de limiter le temps de traitement des requêtes, nous pouvons passer une valeur de timeout lors de la création de l'objetDeferredResult:

DeferredResult> deferredResult = new DeferredResult<>(500l);

deferredResult.onTimeout(() ->
  deferredResult.setErrorResult(
    ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
      .body("Request timeout occurred.")));

En cas d'expiration de délai, nous définissons un état de réponse différent via le gestionnaire de délai d'expiration enregistré avecDeferredResult.

Déclenchons une erreur de délai d'expiration en traitant une demande qui prend plus que les valeurs de délai d'expiration définies de 5 secondes:

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

Examinons les journaux:

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

Il y aura des scénarios où le calcul de longue durée échouera à cause d'une erreur ou d'une exception. Dans ce cas, nous pouvons également enregistrer un rappel deonError():

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

En cas d'erreur, lors du calcul de la réponse, nous définissons un statut de réponse et un corps de message différents via ce gestionnaire d'erreurs.

6. Conclusion

Dans cet article rapide, nous avons examiné comment Spring MVCDeferredResult facilite la création de points de terminaison asynchrones.

Comme d'habitude, le code source complet est disponibleover on Github.