Guia para DeferredResult na Primavera

Guia para DeferredResult na Primavera

1. Visão geral

Neste tutorial, veremoshow we can use the DeferredResult class in Spring MVC to perform asynchronous request processing.

O suporte assíncrono foi introduzido no Servlet 3.0 e, simplificando, permite processar uma solicitação HTTP em outro thread que não seja o thread do receptor da solicitação.

DeferredResult, disponível do Spring 3.2 em diante, auxilia no descarregamento de uma computação de longa execução de um thread de trabalho http para um thread separado.

Embora o outro encadeamento precise de alguns recursos para computação, os encadeamentos de trabalho não são bloqueados nesse meio tempo e podem manipular solicitações de entrada do cliente.

O modelo de processamento de solicitação assíncrona é muito útil, pois ajuda a dimensionar bem um aplicativo durante altas cargas, especialmente para operações intensivas de E / S.

2. Configuração

Para nossos exemplos, usaremos um aplicativo Spring Boot. Para obter mais detalhes sobre como inicializar o aplicativo, consulte nossoarticle anterior.

A seguir, vamos demonstrar a comunicação síncrona e assíncrona usandoDeferredResult e também comparar como o assíncrono é melhor dimensionado para casos de uso de alta carga e IO intensivos.

3. Bloqueando serviço REST

Vamos começar com o desenvolvimento de um serviço REST de bloqueio padrão:

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

O problema aqui é quethe request processing thread is blocked until the complete request is processede o resultado é retornado. No caso de cálculos de longa duração, essa é uma solução subótima.

Para resolver isso, podemos fazer melhor uso de threads de contêiner para lidar com solicitações de clientes, como veremos na próxima seção.

4. REST sem bloqueio usandoDeferredResult

Para evitar o bloqueio, usaremos o modelo de programação baseado em callbacks onde, em vez do resultado real, retornaremos umDeferredResult para o contêiner 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;
}

O processamento da solicitação é feito em um thread separado e, uma vez concluído, invocamos a operaçãosetResult no objetoDeferredResult.

Vejamos a saída do log para verificar se nossos threads se comportam conforme o esperado:

[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

Internamente, o encadeamento do contêiner é notificado e a resposta HTTP é entregue ao cliente. A conexão permanecerá aberta pelo contêiner (servlet 3.0 ou posterior) até a resposta chegar ou atingir o tempo limite.

5. DeferredResult Callbacks

Podemos registrar 3 tipos de callbacks com um DeferredResult: conclusão, timeout e callbacks de erro.

Vamos usar o métodoonCompletion() para definir um bloco de código que é executado quando uma solicitação assíncrona é concluída:

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

Da mesma forma, podemos usaronTimeout() para registrar o código personalizado para invocar assim que o tempo limite ocorrer. Para limitar o tempo de processamento da solicitação, podemos passar um valor de tempo limite durante a criação do objetoDeferredResult:

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

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

Em caso de tempo limite, estamos definindo um status de resposta diferente por meio do manipulador de tempo limite registrado comDeferredResult.

Vamos acionar um erro de tempo limite processando uma solicitação que leva mais do que os valores de tempo limite definidos de 5 segundos:

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

Vejamos os registros:

[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

Haverá cenários em que a computação de longa execução falha devido a algum erro ou exceção. Nesse caso, também podemos registrar um retorno de chamadaonError():

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

Em caso de erro, ao calcular a resposta, estamos definindo um status de resposta e corpo de mensagem diferentes por meio deste manipulador de erros.

6. Conclusão

Neste artigo rápido, vimos como Spring MVCDeferredResult facilita a criação de endpoints assíncronos.

Como de costume, o código-fonte completo está disponívelover on Github.