Руководство по DeferredResult весной

Руководство по DeferredResult весной

1. обзор

В этом руководстве мы рассмотримhow we can use the DeferredResult class in Spring MVC to perform asynchronous request processing.

Асинхронная поддержка была введена в Servlet 3.0 и, проще говоря, она позволяет обрабатывать HTTP-запрос в другом потоке, а не в потоке получателя запроса.

DeferredResult,, доступный начиная с Spring 3.2 и новее, помогает выгрузить длительные вычисления из потока http-worker в отдельный поток.

Хотя другой поток потребует некоторые ресурсы для вычислений, рабочие потоки тем временем не блокируются и могут обрабатывать входящие клиентские запросы.

Модель обработки асинхронных запросов очень полезна, поскольку она помогает хорошо масштабировать приложение во время высоких нагрузок, особенно для интенсивных операций ввода-вывода.

2. Настроить

В наших примерах мы будем использовать приложение Spring Boot. Для получения дополнительной информации о том, как запустить приложение, обратитесь к нашим предыдущимarticle.

Далее мы продемонстрируем как синхронную, так и асинхронную связь с использованиемDeferredResult and, а также сравним, насколько асинхронный лучше масштабируется для случаев использования с высокой нагрузкой и интенсивным вводом-выводом.

3. Блокировка службы REST

Начнем с разработки стандартной REST-службы блокировки:

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

Проблема здесь в том, что возвращаетсяthe request processing thread is blocked until the complete request is processed и результат. В случае длительных вычислений это неоптимальное решение.

Чтобы решить эту проблему, мы можем лучше использовать потоки контейнеров для обработки клиентских запросов, как мы увидим в следующем разделе.

4. Неблокирующий REST с использованиемDeferredResult

Чтобы избежать блокировки, мы будем использовать модель программирования на основе обратных вызовов, в которой вместо фактического результата мы вернемDeferredResult в контейнер сервлета.

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

Обработка запроса выполняется в отдельном потоке, и по завершении мы вызываем операциюsetResult для объектаDeferredResult.

Давайте посмотрим на вывод журнала, чтобы убедиться, что наши потоки ведут себя должным образом:

[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

Внутри контейнерный поток уведомляется, и HTTP-ответ доставляется клиенту. Контейнер будет оставаться открытым (сервлет 3.0 или новее) до тех пор, пока ответ не придет или не истечет время ожидания.

5. DeferredResult Обратные вызовы

Мы можем зарегистрировать 3 типа обратных вызовов с помощью DeferredResult: завершение, тайм-аут и обратные вызовы при ошибках.

Давайте воспользуемся методомonCompletion(), чтобы определить блок кода, который выполняется после завершения асинхронного запроса:

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

Точно так же мы можем использоватьonTimeout() для регистрации пользовательского кода, который будет запускаться при истечении времени ожидания. Чтобы ограничить время обработки запроса, мы можем передать значение тайм-аута во время создания объектаDeferredResult:

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

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

В случае тайм-аутов мы устанавливаем другой статус ответа с помощью обработчика тайм-аута, зарегистрированного вDeferredResult.

Давайте вызовем ошибку тайм-аута, обработав запрос, который занимает больше заданных значений тайм-аута в 5 секунд:

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

Посмотрим на журналы:

[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

Будут сценарии, в которых длительные вычисления не будут выполнены из-за какой-либо ошибки или исключения. В этом случае мы также можем зарегистрировать обратный вызовonError():

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

В случае ошибки при вычислении ответа мы устанавливаем другой статус ответа и тело сообщения с помощью этого обработчика ошибок.

6. Заключение

В этой быстрой статье мы рассмотрели, как Spring MVCDeferredResult упрощает создание асинхронных конечных точек.

Как обычно, доступен полный исходный кодover on Github.