Перейти к основному содержимому

Руководство по DeferredResult в Spring

· 4 мин. чтения

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.foreach.controller.AsyncDeferredResultController: 
Received async-deferredresult request
[nio-8080-exec-6] com.foreach.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.foreach.controller.DeferredResultController: 
servlet thread freed
[nio-8080-exec-6] java.lang.Thread: Processing in separate thread
[nio-8080-exec-6] com.foreach.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 .