«1. Обзор

В этом руководстве мы рассмотрим, как мы можем использовать класс DeferredResult в Spring MVC для выполнения асинхронной обработки запросов.

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

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

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

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

2. Настройка

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

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

3. Блокирующий сервис REST

Начнем с разработки стандартного блокирующего сервиса REST:

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

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

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

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

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

@GetMapping("/async-deferredresult")
public DeferredResult<ResponseEntity<?>> handleReqDefResult(Model model) {
    LOG.info("Received async-deferredresult request");
    DeferredResult<ResponseEntity<?>> 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.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

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

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

Мы можем зарегистрировать 3 типа обратных вызовов с DeferredResult: обратные вызовы завершения, ожидания и ошибки.

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

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

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

DeferredResult<ResponseEntity<?>> 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.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

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

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

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

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

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

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